I’ll check the route handler and confirm the limiter API first.
Components
Each component on its own, live and keyboard accessible, with its npm and shadcn install. Its docs page has the props, keys and theming hooks. The frames follow the theme switch in the header.
<DiffReview />
Accept or reject agent edits hunk by hunk or file by file, comment on lines, and send the agent what you applied, rejected and said. Large files are diffed in a worker and render only what is near the screen.
Review changes
4 files · 4 hunks · +21 −1
Comment On line 7
Read the window from an env var, so staging can use a longer one.
2/6 reviewed · 0 of 4 files viewed · 1 comment · unreviewed hunks are skipped
Keyboard: J or K to move between hunks, Shift J or Shift K between files, A to accept, R to reject, U to reset, Alt A or Alt R for the whole file, Shift A or Shift R for every hunk, Shift with the up or down arrow to select lines, C to comment on them or on the hunk, E to show more unchanged lines, V to mark the file viewed, Ctrl Enter to apply.
npm i signoff-ui aiimport { DiffReview, reviewToolOutput } from 'signoff-ui';
// Once per app (with Tailwind v4: @import 'signoff-ui/tailwind.css'; in your CSS)
import 'signoff-ui/styles.css';npx shadcn@latest add https://signoff-ui.vercel.app/r/diff-review.jsonimport { DiffReview } from '@/components/signoff-ui/diff-review';useDiffReview(options)
The same review in markup of its own: this list uses the site’s classes and none of the kit’s, and the hook brings the keyboard, decisions, comments and the result.
- @@ -1,6 +1,7 @@not decided
+ import { ratelimit } from '@/lib/ratelimit'; - @@ -11,6 +12,15 @@not decided
+ const ip = req.headers.get('x-forwarded-for') ?? 'anonymous'; + const { success, reset } = await ratelimit.limit(ip); + if (!success) { + return new Response('Too many requests', { + status: 429, + headers: { 'Retry-After': String(Math.ceil((reset - Date.now()) / 1000)) }, + }); + } + - @@ -18,7 +28,7 @@not decided
- model: openai('gpt-4o'), + model: openai('gpt-4.1-mini'),
J and K move, A and R decide, C writes a note.
npm i signoff-ui aiimport { useDiffReview, reviewToolOutput } from 'signoff-ui';
// Once per app (with Tailwind v4: @import 'signoff-ui/tailwind.css'; in your CSS)
import 'signoff-ui/styles.css';npx shadcn@latest add https://signoff-ui.vercel.app/r/use-diff-review.jsonimport { useDiffReview } from '@/components/signoff-ui/use-diff-review';<ApprovalCard />
Approve or deny a tool call once, for the session or always, with what will run, its risk, the arguments editable first, a reason and a key for each choice.
Approval required
High riskRun command
Installs a package from the npm registry and updates package.json and pnpm-lock.yaml.
npm i signoff-ui aiimport { ApprovalCard, ToolApprovalCard, ToolApprovalBatch } from 'signoff-ui';
// Once per app (with Tailwind v4: @import 'signoff-ui/tailwind.css'; in your CSS)
import 'signoff-ui/styles.css';npx shadcn@latest add https://signoff-ui.vercel.app/r/approval-card.jsonimport { ApprovalCard, ToolApprovalCard, ToolApprovalBatch } from '@/components/signoff-ui/approval-card';useApprovalPolicy()
Approval rules: answer once, for this session or always, by tool and argument pattern. The next call a rule covers is answered without asking, and every decision goes to the audit trail.
Approval required
Run command
npm i signoff-ui aiimport { useApprovalPolicy, webStorageRules, setToolInput } from 'signoff-ui';
// Once per app (with Tailwind v4: @import 'signoff-ui/tailwind.css'; in your CSS)
import 'signoff-ui/styles.css';npx shadcn@latest add https://signoff-ui.vercel.app/r/use-approval-policy.jsonimport { useApprovalPolicy, webStorageRules } from '@/components/signoff-ui/use-approval-policy';<ToolCallTimeline />
Every tool state with durations, a waterfall, and expandable input and output.
ENOENT: no such file or directory, open 'middleware.ts'
npm i signoff-ui aiimport { ToolCallTimeline } from 'signoff-ui';
// Once per app (with Tailwind v4: @import 'signoff-ui/tailwind.css'; in your CSS)
import 'signoff-ui/styles.css';npx shadcn@latest add https://signoff-ui.vercel.app/r/tool-call-timeline.jsonimport { ToolCallTimeline } from '@/components/signoff-ui/tool-call-timeline';<AgentStatus />
Run state in one pill, announced through a live region.
npm i signoff-ui aiimport { AgentStatus, deriveAgentState } from 'signoff-ui';
// Once per app (with Tailwind v4: @import 'signoff-ui/tailwind.css'; in your CSS)
import 'signoff-ui/styles.css';npx shadcn@latest add https://signoff-ui.vercel.app/r/agent-status.jsonimport { AgentStatus } from '@/components/signoff-ui/agent-status';
import { deriveAgentState } from '@/components/signoff-ui/lib/ai';<RunMeter />
Tokens, estimated cost and latency; compact for headers, expanded for panels.
Run
mock-agent-1- Tokens
- 41.1k
- Est. cost
- $0.056
- Input 38.7k(28.8k cached)
- Output 2.41k(96 reasoning)
- TTFT
- 684ms
- Total
- 21.8s
- Cache hit
- 74%
$0.032 input · $0.024 output · $2.5/$10 per 1M
Pass totalUsage from streamText through message metadata, and timing from useRunTiming(status, messages). Cost is an estimate from the pricing you supply.
npm i signoff-ui aiimport { RunMeter, useRunTiming } from 'signoff-ui';
// Once per app (with Tailwind v4: @import 'signoff-ui/tailwind.css'; in your CSS)
import 'signoff-ui/styles.css';npx shadcn@latest add https://signoff-ui.vercel.app/r/run-meter.jsonimport { RunMeter } from '@/components/signoff-ui/run-meter';
import { useRunTiming } from '@/components/signoff-ui/lib/hooks';<Sources />
Citations as compact chips or cards; inline [n] markers link to them.
Sources · 3
npm i signoff-ui aiimport { Sources, getSourceParts } from 'signoff-ui';
// Once per app (with Tailwind v4: @import 'signoff-ui/tailwind.css'; in your CSS)
import 'signoff-ui/styles.css';npx shadcn@latest add https://signoff-ui.vercel.app/r/sources.jsonimport { Sources } from '@/components/signoff-ui/sources';
import { getSourceParts } from '@/components/signoff-ui/lib/ai';Theming
Every color, radius and font is a CSS variable. Light is the default, .dark switches, and any subtree can override tokens.
ENOENT: no such file or directory, open 'middleware.ts'
ENOENT: no such file or directory, open 'middleware.ts'
ENOENT: no such file or directory, open 'middleware.ts'
Labels
Every word the components show or announce is a label. Here the approval and the review are in Portuguese, from one SignoffLabelsProvider.
Aprovação necessária
Risco altoExecutar comando
Instala um pacote do registo npm e atualiza o package.json.
Rever alterações
1 ficheiro · 3 blocos · +11 −1
0/3 revistos
Teclado: J ou K para mudar de bloco, Shift J ou Shift K para mudar de ficheiro, A para aceitar, R para rejeitar, U para repor, Alt A ou Alt R para o ficheiro inteiro, Shift A ou Shift R para todos os blocos, Shift com a seta para cima ou para baixo para selecionar linhas, C para as comentar ou comentar o bloco, E para mostrar mais linhas inalteradas, V para marcar o ficheiro como visto, Ctrl Enter para aplicar.
<AgentMessage />
A whole assistant UIMessage: reasoning, streaming markdown, grouped tool calls and sources.
npm i signoff-ui aiimport { AgentMessage } from 'signoff-ui';
// Once per app (with Tailwind v4: @import 'signoff-ui/tailwind.css'; in your CSS)
import 'signoff-ui/styles.css';npx shadcn@latest add https://signoff-ui.vercel.app/r/agent-message.jsonimport { AgentMessage } from '@/components/signoff-ui/agent-message';<Markdown />
Streaming-safe markdown: syntax that hasn’t closed yet never flashes raw, and no raw HTML.
The limiter belongs at the top of the handler, before streamText opens the stream:
- Read the client's IP from
x-forwarded-for. - Call
ratelimit.limit(ip)and return a 429 when it fails.
| Window | Requests | Burst |
|---|---|---|
| Sliding, 10 s | 10 | smoothed |
| Fixed, 10 s | 10 | up to 20 at the edge |
const { success } = await limit(ip);if (!success) return tooMany();npm i signoff-ui aiimport { Markdown } from 'signoff-ui';
// Once per app (with Tailwind v4: @import 'signoff-ui/tailwind.css'; in your CSS)
import 'signoff-ui/styles.css';npx shadcn@latest add https://signoff-ui.vercel.app/r/markdown.jsonimport { Markdown } from '@/components/signoff-ui/markdown';<Reasoning />
Open while the model thinks, then one line: “Thought for 1.7s”.
npm i signoff-ui aiimport { Reasoning } from 'signoff-ui';
// Once per app (with Tailwind v4: @import 'signoff-ui/tailwind.css'; in your CSS)
import 'signoff-ui/styles.css';npx shadcn@latest add https://signoff-ui.vercel.app/r/reasoning.jsonimport { Reasoning } from '@/components/signoff-ui/reasoning';useAgUiAgent(agent)
AG-UI agents (LangGraph, CrewAI, Mastra, Pydantic AI) through the same components. This one is a real @ag-ui/client agent replaying a LangGraph-style run: steps, streamed tool arguments, and an interrupt that resumes the run when you answer it.
npm i signoff-ui ai @ag-ui/clientimport { useAgUiAgent } from 'signoff-ui/ag-ui';
// Once per app (with Tailwind v4: @import 'signoff-ui/tailwind.css'; in your CSS)
import 'signoff-ui/styles.css';npx shadcn@latest add https://signoff-ui.vercel.app/r/ag-ui.json
npm i @ag-ui/clientimport { useAgUiAgent } from '@/components/signoff-ui/use-ag-ui-agent';Agent Client Protocol
An ACP prompt turn through the same components, with signoff-ui/acp: a scripted agent sends its session updates and asks permission to edit a file, whose diff the card shows, and to run the tests. The card offers the options the agent sent.