Components

Each component in isolation, rendered from AI SDK v7 message parts. Everything here is interactive and keyboard accessible.

<AgentStatus />

Run state in one pill, announced through a live region.

Thinking
Workingread_file3.42s
Waiting for approvalrun_command
Done21.8s
Rate limited by provider

<ToolCallTimeline />

Every tool state with durations, a waterfall, and expandable input and output.

  1. ENOENT: no such file or directory, open 'middleware.ts'

  2. !
Read file finished in 450 milliseconds

<ApprovalCard />

Human-in-the-loop approval with risk, preview and Y / N shortcuts.

Approval required

High risk

Run command

Installs a package from the npm registry and updates package.json and pnpm-lock.yaml.

~/acme/chat-app
pnpm add @upstash/ratelimit

<DiffReview />

Accept or reject agent edits hunk by hunk, unified or split.

Review changes

2 files · 4 hunks · +21 −1

Added:lib/ratelimit.ts+10 −0
@@ -1,0 +1,10 @@accepted
Added: import { Ratelimit } from '@upstash/ratelimit';
Added: import { redis } from '@/lib/redis';
Added:
Added: /** 10 requests per 10 seconds per key, sliding window. */
Added: export const ratelimit = new Ratelimit({
Added: redis,
Added: limiter: Ratelimit.slidingWindow(10, '10 s'),
Added: prefix: 'ratelimit:chat',
Added: analytics: true,
Added: });
Modified:app/api/chat/route.ts+11 −1
@@ -1,6 +1,7 @@
import { convertToModelMessages, streamText, type UIMessage } from 'ai';
import { openai } from '@ai-sdk/openai';
import { tools } from '@/lib/tools';
Added: import { ratelimit } from '@/lib/ratelimit';
export const maxDuration = 30;
@@ -11,6 +12,15 @@
].join(' ');
export async function POST(req: Request) {
Added: const ip = req.headers.get('x-forwarded-for') ?? 'anonymous';
Added: const { success, reset } = await ratelimit.limit(ip);
Added: if (!success) {
Added: return new Response('Too many requests', {
Added: status: 429,
Added: headers: { 'Retry-After': String(Math.ceil((reset - Date.now()) / 1000)) },
Added: });
Added: }
Added:
const { messages }: { messages: UIMessage[] } = await req.json();
if (messages.length === 0) return new Response('Empty conversation', { status: 400 });
const modelMessages = convertToModelMessages(messages);
@@ -18,7 +28,7 @@rejected
console.info('chat', { turns: messages.length, last: lastUserMessage?.id });
const result = streamText({
Removed: model: openai('gpt-4o'),
Added: model: openai('gpt-4.1-mini'),
system: SYSTEM,
messages: modelMessages,
tools,

2/4 reviewed · unreviewed hunks are skipped

Keyboard: J or K to move between hunks, A to accept, R to reject, U to reset, Shift A or Shift R for all hunks, Ctrl Enter to apply.

<RunMeter />

Tokens, estimated cost and latency; compact for headers, expanded for panels.

variant="expanded"

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

variant="compact"
38.7k input tokens, 2.41k output tokens, estimated cost $0.056, time to first token 684ms, total 21.8s

Pass totalUsage from streamText through message metadata, and timing from useRunTiming(status). Cost is an estimate from the pricing you supply.

<Sources />

Citations as compact chips or cards; inline [n] markers link to them.

<Theming />

Every color, radius and font is a CSS variable. Light is the default, .dark switches, and any subtree can override tokens.

Dark
Waiting for approvalrun_command
  1. ENOENT: no such file or directory, open 'middleware.ts'

Light
Waiting for approvalrun_command
  1. ENOENT: no such file or directory, open 'middleware.ts'

Custom tokens
Waiting for approvalrun_command
  1. ENOENT: no such file or directory, open 'middleware.ts'

<AgentMessage />

A whole assistant UIMessage: reasoning, streaming markdown, grouped tool calls and sources.