I’ll check the route handler and confirm the limiter API first.
Components
Each component in isolation, rendered from AI SDK 6 and 7 message parts (or an AG-UI agent and an ACP turn, at the end), with how to install it from npm or as a shadcn registry item. Everything here is interactive and keyboard accessible; each component's docs page has its props, keyboard and theming hooks. The frames follow the site's theme: switch it in the header to see either palette.
<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.
Pick a command above to see its approval.
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.