{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "agent-status",
  "title": "Agent Status",
  "author": "Diogo Esteves <https://github.com/dgesteves>",
  "description": "Run state pill (thinking, working, waiting for approval, done, error) announced through an aria-live region.",
  "dependencies": [
    "ai",
    "clsx",
    "tailwind-merge"
  ],
  "files": [
    {
      "path": "packages/agent-ui-kit/src/agent-status.tsx",
      "content": "'use client';\n\nimport type { ComponentPropsWithoutRef, ReactNode } from 'react';\nimport { AGENT_STATE_LABEL, type AgentState } from './lib/ai';\nimport { formatDuration } from './lib/format';\nimport { useHydrated, useNow } from './lib/hooks';\nimport { AlertIcon, CheckIcon, SparkIcon, SpinnerIcon } from './lib/icons';\nimport { LiveRegion, useDebouncedValue } from './lib/primitives';\nimport { cn } from './lib/utils';\n\nexport type { AgentState };\n\nexport interface AgentStatusProps extends Omit<ComponentPropsWithoutRef<'div'>, 'children'> {\n  state: AgentState;\n  /** Overrides the default label for the state. */\n  label?: string | undefined;\n  /** Secondary detail, e.g. the tool being run. */\n  detail?: string | undefined;\n  /** Epoch ms when the run started. Shows a live elapsed timer while active. */\n  startedAt?: number | undefined;\n  /** Fixed elapsed time to show (e.g. the final run duration). Takes precedence over `startedAt`. */\n  elapsedMs?: number | undefined;\n  /** Announce state changes to assistive tech. Default `true`. */\n  announce?: boolean;\n  size?: 'sm' | 'md';\n}\n\nconst ACTIVE: AgentState[] = ['thinking', 'working'];\n\nfunction Indicator({ state }: { state: AgentState }) {\n  switch (state) {\n    case 'thinking':\n      return <SparkIcon size={14} className=\"text-aui-accent motion-safe:animate-pulse\" />;\n    case 'working':\n      return <SpinnerIcon size={14} className=\"text-aui-accent motion-safe:animate-aui-spin\" />;\n    case 'awaiting-approval':\n      return (\n        <span className=\"relative flex size-3.5 items-center justify-center\">\n          <span className=\"bg-aui-hot motion-safe:animate-aui-ping absolute inline-flex size-2 rounded-full\" />\n          <span className=\"bg-aui-hot relative inline-flex size-2 rounded-full\" />\n        </span>\n      );\n    case 'done':\n      return <CheckIcon size={14} className=\"text-aui-accent-fg\" />;\n    case 'stopped':\n      return (\n        <span className=\"flex size-3.5 items-center justify-center\">\n          <span className=\"bg-aui-fg-muted size-2 rounded-[2px]\" />\n        </span>\n      );\n    case 'error':\n      return <AlertIcon size={14} className=\"text-aui-hot-fg\" />;\n    default:\n      return (\n        <span className=\"flex size-3.5 items-center justify-center\">\n          <span className=\"bg-aui-fg-subtle size-2 rounded-full\" />\n        </span>\n      );\n  }\n}\n\n/**\n * A compact, live status pill for an agent run. State changes are announced\n * through a polite live region (assertive for errors and approval requests),\n * debounced so rapid transitions do not flood screen readers.\n */\nexport function AgentStatus({\n  state,\n  label,\n  detail,\n  startedAt,\n  elapsedMs,\n  announce = true,\n  size = 'md',\n  className,\n  ...props\n}: AgentStatusProps) {\n  const active = ACTIVE.includes(state);\n  const now = useNow(active && startedAt !== undefined && elapsedMs === undefined, 100);\n  const hydrated = useHydrated();\n  const elapsed = elapsedMs ?? (startedAt !== undefined && hydrated ? Math.max(0, now - startedAt) : undefined);\n  const text = label ?? AGENT_STATE_LABEL[state];\n  const spoken = detail ? `${text}: ${detail}` : text;\n  const urgent = state === 'error' || state === 'awaiting-approval';\n  const debounced = useDebouncedValue(spoken, 350);\n  const debouncedUrgent = useDebouncedValue(urgent, 350);\n\n  return (\n    <div\n      data-aui\n      data-slot=\"agent-status\"\n      data-state={state}\n      className={cn(\n        'font-aui-sans inline-flex w-fit max-w-full items-center gap-2 rounded-full border font-medium whitespace-nowrap transition-colors',\n        size === 'sm' ? 'h-7 px-2.5 text-xs' : 'h-8 px-3 text-[13px]',\n        state === 'awaiting-approval' || state === 'error'\n          ? 'border-aui-hot/40 bg-aui-hot/10 text-aui-fg'\n          : active\n            ? 'border-aui-accent/30 bg-aui-accent/[0.07] text-aui-fg'\n            : 'border-aui-border bg-aui-surface text-aui-fg-muted',\n        className,\n      )}\n      {...props}\n    >\n      <Indicator state={state} />\n      <span\n        className={cn(\n          state === 'thinking' &&\n            'motion-safe:animate-aui-shimmer motion-safe:bg-[linear-gradient(90deg,var(--aui-fg)_0%,var(--aui-fg)_40%,var(--aui-accent-fg)_50%,var(--aui-fg)_60%,var(--aui-fg)_100%)] motion-safe:bg-[length:200%_100%] motion-safe:bg-clip-text motion-safe:text-transparent',\n        )}\n      >\n        {text}\n      </span>\n      {detail && (\n        <span className=\"font-aui-mono text-aui-fg-muted min-w-0 truncate text-[0.92em] font-normal\">{detail}</span>\n      )}\n      {elapsed !== undefined && state !== 'idle' && (\n        <span className=\"font-aui-mono text-aui-fg-subtle text-[0.92em] font-normal tabular-nums\">\n          {formatDuration(elapsed)}\n        </span>\n      )}\n      {announce && (\n        <>\n          <LiveRegion>{debouncedUrgent ? '' : debounced}</LiveRegion>\n          <LiveRegion politeness=\"assertive\">{debouncedUrgent ? debounced : ''}</LiveRegion>\n        </>\n      )}\n    </div>\n  );\n}\n\nexport function AgentStatusDot({ state, className }: { state: AgentState; className?: string }): ReactNode {\n  return (\n    <span data-aui className={cn('inline-flex', className)} aria-hidden=\"true\">\n      <Indicator state={state} />\n    </span>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/agent-ui/agent-status.tsx"
    },
    {
      "path": "packages/agent-ui-kit/src/lib/ai.ts",
      "content": "/**\n * Thin, dependency-free helpers over AI SDK v7 `UIMessage` parts.\n * Only types are imported from `ai`, so nothing from the SDK runtime ends up in the bundle.\n */\nimport type {\n  ChatStatus,\n  DynamicToolUIPart,\n  LanguageModelUsage,\n  ReasoningUIPart,\n  SourceDocumentUIPart,\n  SourceUrlUIPart,\n  TextUIPart,\n  ToolUIPart,\n  UIMessage,\n  UITools,\n} from 'ai';\n\n/**\n * Fields AI SDK 7 added to a tool part's `approval`: whether a `toolApproval` policy decided it,\n * and why the tool asked. AI SDK 6 has neither, so they are read defensively.\n */\ntype ApprovalExtras = { isAutomatic?: unknown; requestReason?: unknown } | undefined;\n\n/** Whether a policy decided the approval rather than a person. Always false on AI SDK 6. */\nexport function isAutomaticApproval(approval: object | undefined): boolean {\n  return (approval as ApprovalExtras)?.isAutomatic === true;\n}\n\n/** The reason the tool gave for asking (AI SDK 7's `requestReason`), if any. */\nexport function getApprovalRequestReason(approval: object | undefined): string | undefined {\n  const reason = (approval as ApprovalExtras)?.requestReason;\n  return typeof reason === 'string' ? reason : undefined;\n}\n\nexport type { ChatStatus, UIMessage };\n\n/** Any part of any `UIMessage`. */\nexport type AnyUIPart = UIMessage['parts'][number];\n/** A static (`tool-${name}`) or dynamic tool part. */\nexport type ToolPart = ToolUIPart<UITools> | DynamicToolUIPart;\n/** The AI SDK v7 tool part states. */\nexport type ToolState = ToolPart['state'];\nexport type SourcePart = SourceUrlUIPart | SourceDocumentUIPart;\nexport type { ReasoningUIPart, SourceDocumentUIPart, SourceUrlUIPart, TextUIPart };\n\n/** Token usage, compatible with the AI SDK `LanguageModelUsage` shape. */\nexport type RunUsage = {\n  inputTokens?: number | undefined;\n  outputTokens?: number | undefined;\n  totalTokens?: number | undefined;\n  inputTokenDetails?: Partial<LanguageModelUsage['inputTokenDetails']> | undefined;\n  outputTokenDetails?: Partial<LanguageModelUsage['outputTokenDetails']> | undefined;\n};\n\nexport const TOOL_STATES = [\n  'input-streaming',\n  'input-available',\n  'approval-requested',\n  'approval-responded',\n  'output-available',\n  'output-error',\n  'output-denied',\n] as const satisfies readonly ToolState[];\n\nexport function isToolPart(part: { type: string }): part is ToolPart {\n  return part.type === 'dynamic-tool' || part.type.startsWith('tool-');\n}\n\nexport function isSourcePart(part: { type: string }): part is SourcePart {\n  return part.type === 'source-url' || part.type === 'source-document';\n}\n\n/** Same semantics as the SDK's `getToolName`, without the runtime import. */\nexport function getToolPartName(part: ToolPart): string {\n  return part.type === 'dynamic-tool' ? part.toolName : part.type.slice('tool-'.length);\n}\n\n/**\n * A UI-level phase derived from the SDK tool state. Several SDK states collapse\n * into one visual phase (e.g. an approved call that has not returned yet is \"running\").\n */\nexport type ToolPhase = 'streaming' | 'running' | 'awaiting-approval' | 'success' | 'error' | 'denied';\n\nexport function getToolPhase(\n  part: Pick<ToolPart, 'state'> & {\n    approval?: { approved?: boolean; isAutomatic?: boolean } | undefined;\n    preliminary?: boolean | undefined;\n  },\n): ToolPhase {\n  switch (part.state) {\n    case 'input-streaming':\n      return 'streaming';\n    case 'input-available':\n      return 'running';\n    case 'approval-requested':\n      // A policy decision (`isAutomatic`) streams as a request immediately followed by its response.\n      return part.approval?.isAutomatic ? 'running' : 'awaiting-approval';\n    case 'approval-responded':\n      return part.approval?.approved ? 'running' : 'denied';\n    case 'output-available':\n      return part.preliminary ? 'running' : 'success';\n    case 'output-error':\n      return 'error';\n    case 'output-denied':\n      return 'denied';\n  }\n}\n\nexport const TOOL_PHASE_LABEL: Record<ToolPhase, string> = {\n  streaming: 'Preparing',\n  running: 'Running',\n  'awaiting-approval': 'Needs approval',\n  success: 'Done',\n  error: 'Failed',\n  denied: 'Denied',\n};\n\nexport function isSettledPhase(phase: ToolPhase): boolean {\n  return phase === 'success' || phase === 'error' || phase === 'denied';\n}\n\nexport function getToolParts(parts: readonly AnyUIPart[]): ToolPart[] {\n  return parts.filter(isToolPart);\n}\n\n/**\n * Calls that only the model or a tool can still finish: input still streaming, running, or partial\n * output. Once the run has ended, these were cut off. An approved call waiting for the app to send\n * the continuation (`approval-responded`) is not one: `useChat` sends it after a render.\n */\nexport function isInterruptibleToolPart(part: ToolPart): boolean {\n  return (\n    part.state === 'input-streaming' ||\n    part.state === 'input-available' ||\n    (part.state === 'output-available' && part.preliminary === true)\n  );\n}\n\nexport type ApprovalStatus = 'pending' | 'approved' | 'denied';\n\n/** The approval state of a tool part, or `undefined` for parts outside the approval flow. */\nexport function getApprovalStatus(part: ToolPart): ApprovalStatus | undefined {\n  if (!part.approval) return undefined;\n  if (part.state === 'approval-requested') return 'pending';\n  if (part.state === 'output-denied') return 'denied';\n  return part.approval.approved ? 'approved' : part.approval.approved === false ? 'denied' : 'pending';\n}\n\nexport interface ToolTiming {\n  /** First time the call was observed (input started streaming). */\n  startedAt?: number | undefined;\n  /** When execution began: input complete, or approval granted. */\n  runningAt?: number | undefined;\n  /** When the call settled (output, error or denial). */\n  endedAt?: number | undefined;\n}\n\nexport type ToolTimings = Readonly<Record<string, ToolTiming>>;\n\n/** Pure reducer behind `useToolTimings`, exported for tests and custom stores. */\nexport function observeToolTimings(prev: ToolTimings, tools: readonly ToolPart[], now: number): ToolTimings {\n  let next: Record<string, ToolTiming> | undefined;\n  const write = (id: string, value: ToolTiming) => {\n    next ??= { ...prev };\n    next[id] = value;\n  };\n  for (const tool of tools) {\n    const phase = getToolPhase(tool);\n    const current = (next ?? prev)[tool.toolCallId];\n    if (!current) {\n      if (isSettledPhase(phase)) write(tool.toolCallId, {});\n      else write(tool.toolCallId, { startedAt: now, runningAt: phase === 'running' ? now : undefined });\n      continue;\n    }\n    if (current.startedAt === undefined || current.endedAt !== undefined) continue;\n    if (phase === 'running' && current.runningAt === undefined) write(tool.toolCallId, { ...current, runningAt: now });\n    else if (isSettledPhase(phase))\n      write(tool.toolCallId, { ...current, runningAt: current.runningAt ?? now, endedAt: now });\n  }\n  return next ?? prev;\n}\n\n/** Collect sources, de-duplicated by URL (or source id for documents). */\nexport function getSourceParts(parts: readonly AnyUIPart[]): SourcePart[] {\n  const seen = new Set<string>();\n  const out: SourcePart[] = [];\n  for (const part of parts) {\n    if (!isSourcePart(part)) continue;\n    const key = part.type === 'source-url' ? part.url : `doc:${part.sourceId}`;\n    if (seen.has(key)) continue;\n    seen.add(key);\n    out.push(part);\n  }\n  return out;\n}\n\n/** A source in app-level shape. AI SDK `source-url` / `source-document` parts are accepted too. */\nexport interface SourceItem {\n  id: string;\n  url?: string | undefined;\n  title?: string | undefined;\n  /** Optional snippet shown in the cards variant. */\n  description?: string | undefined;\n  /** For documents. */\n  filename?: string | undefined;\n  mediaType?: string | undefined;\n}\n\nexport function toSourceItem(source: SourcePart | SourceItem): SourceItem {\n  if ('type' in source && isSourcePart(source as SourcePart)) {\n    const part = source as SourcePart;\n    return part.type === 'source-url'\n      ? { id: part.sourceId, url: part.url, title: part.title }\n      : { id: part.sourceId, title: part.title, filename: part.filename, mediaType: part.mediaType };\n  }\n  return source as SourceItem;\n}\n\nexport type AgentState = 'idle' | 'thinking' | 'working' | 'awaiting-approval' | 'done' | 'stopped' | 'error';\n\nexport const AGENT_STATE_LABEL: Record<AgentState, string> = {\n  idle: 'Idle',\n  thinking: 'Thinking',\n  working: 'Working',\n  'awaiting-approval': 'Waiting for approval',\n  done: 'Done',\n  stopped: 'Stopped',\n  error: 'Error',\n};\n\nexport interface DerivedAgentState {\n  state: AgentState;\n  /** Short human detail, e.g. \"read_file\" or \"Writing response\". */\n  detail?: string | undefined;\n}\n\n/**\n * Map `useChat` status plus the latest assistant message to a single agent state.\n * Human-in-the-loop waits (approvals, client-side tools awaiting output) take\n * precedence over transport status, because the run cannot progress without the user.\n *\n * A run that ended (`ready`) with work still unfinished in the message is `stopped`, not `done`:\n * text or reasoning still streaming, or a tool call still preparing, running or partial. That is\n * what `stop()` leaves behind, and what an AG-UI run cancelled or aborted mid-way does. A\n * client-side tool the app runs itself is the exception: name it in `clientTools`.\n */\nexport function deriveAgentState({\n  status,\n  message,\n  pendingClientTools = [],\n  clientTools = [],\n}: {\n  status: ChatStatus;\n  message?: Pick<UIMessage, 'role' | 'parts'> | undefined;\n  /** Names of client-side tools whose `input-available` state means \"waiting for the user\". */\n  pendingClientTools?: readonly string[];\n  /**\n   * Names of client-side tools the app runs itself (`onToolCall`, then `addToolOutput`). `useChat`\n   * is `ready` while one runs: its `input-available` call then reads `working` with the tool's name\n   * as the detail, not `stopped`, until the output lands and the run continues.\n   */\n  clientTools?: readonly string[];\n}): DerivedAgentState {\n  if (status === 'error') return { state: 'error' };\n  const parts = message?.role === 'assistant' ? message.parts : [];\n  const tools = getToolParts(parts);\n  const approval = tools.find((t) => t.state === 'approval-requested' && !isAutomaticApproval(t.approval));\n  if (approval) return { state: 'awaiting-approval', detail: getToolPartName(approval) };\n  const clientWait = tools.find(\n    (t) => t.state === 'input-available' && pendingClientTools.includes(getToolPartName(t)),\n  );\n  if (clientWait && status !== 'submitted') return { state: 'awaiting-approval', detail: getToolPartName(clientWait) };\n  if (status === 'submitted') return { state: 'thinking' };\n  if (status === 'streaming') {\n    const last = parts.at(-1);\n    if (!last || last.type === 'step-start' || last.type === 'reasoning') return { state: 'thinking' };\n    if (isToolPart(last)) return { state: 'working', detail: getToolPartName(last) };\n    if (last.type === 'text') return { state: 'working', detail: 'Writing response' };\n    return { state: 'working' };\n  }\n  if (status === 'ready') {\n    const running = tools.find((t) => t.state === 'input-available' && clientTools.includes(getToolPartName(t)));\n    if (running) return { state: 'working', detail: getToolPartName(running) };\n    if (parts.some(isUnfinishedPart)) return { state: 'stopped' };\n  }\n  return parts.length > 0 ? { state: 'done' } : { state: 'idle' };\n}\n\n/** A part the run left unfinished: text or reasoning still streaming, or an interrupted tool call. */\nfunction isUnfinishedPart(part: AnyUIPart): boolean {\n  if (part.type === 'text' || part.type === 'reasoning') return part.state === 'streaming';\n  return isToolPart(part) && isInterruptibleToolPart(part);\n}\n",
      "type": "registry:lib",
      "target": "components/agent-ui/lib/ai.ts"
    },
    {
      "path": "packages/agent-ui-kit/src/lib/format.ts",
      "content": "/** Formatting helpers shared by the components. All output is locale-stable. */\n\n/*\n * Each unit is chosen by the value as it will be printed, not the raw value: otherwise rounding\n * produces \"1000ms\", \"60.0s\" or \"1000.0k\" instead of moving up to the next unit.\n */\n\nexport function formatDuration(ms: number | undefined | null): string {\n  if (ms == null || !Number.isFinite(ms) || ms < 0) return '–';\n  if (Math.round(ms) < 1000) return `${Math.round(ms)}ms`;\n  const centis = (ms / 1000).toFixed(2);\n  if (Number(centis) < 10) return `${centis}s`;\n  const tenths = (ms / 1000).toFixed(1);\n  if (Number(tenths) < 60) return `${tenths}s`;\n  const seconds = Math.round(ms / 1000);\n  return `${Math.floor(seconds / 60)}m ${String(seconds % 60).padStart(2, '0')}s`;\n}\n\n/** Spoken form for screen readers, e.g. \"1.2 seconds\". */\nexport function formatDurationLong(ms: number | undefined | null): string {\n  if (ms == null || !Number.isFinite(ms) || ms < 0) return 'unknown duration';\n  if (Math.round(ms) < 1000) return `${Math.round(ms)} milliseconds`;\n  const tenths = (ms / 1000).toFixed(1);\n  if (Number(tenths) < 60) return `${tenths} seconds`;\n  const total = Math.round(ms / 1000);\n  const minutes = Math.floor(total / 60);\n  const rest = total % 60;\n  return `${minutes} minute${minutes === 1 ? '' : 's'} ${rest} second${rest === 1 ? '' : 's'}`;\n}\n\nexport function formatTokens(n: number | undefined | null): string {\n  if (n == null || !Number.isFinite(n)) return '–';\n  if (Math.abs(Math.round(n)) < 1000) return String(Math.round(n));\n  const hundredths = (n / 1000).toFixed(2);\n  if (Math.abs(Number(hundredths)) < 10) return `${hundredths}k`;\n  const tenths = (n / 1000).toFixed(1);\n  if (Math.abs(Number(tenths)) < 1000) return `${tenths}k`;\n  return `${(n / 1_000_000).toFixed(2)}M`;\n}\n\nexport function formatCost(usd: number | undefined | null): string {\n  if (usd == null || !Number.isFinite(usd)) return '–';\n  if (usd === 0) return '$0.00';\n  const four = usd.toFixed(4);\n  if (Math.abs(Number(four)) < 0.01) return `$${four}`;\n  const three = usd.toFixed(3);\n  if (Math.abs(Number(three)) < 1) return `$${three}`;\n  return `$${usd.toFixed(2)}`;\n}\n\n/** `read_file` / `readFile` / `read-file` → \"Read file\". */\nexport function humanizeToolName(name: string): string {\n  const spaced = name\n    .replace(/[_-]+/g, ' ')\n    .replace(/([a-z0-9])([A-Z])/g, '$1 $2')\n    .trim()\n    .toLowerCase();\n  return spaced.charAt(0).toUpperCase() + spaced.slice(1);\n}\n\nexport function getHostname(url: string): string {\n  try {\n    return new URL(url).hostname.replace(/^www\\./, '');\n  } catch {\n    return url;\n  }\n}\n\n/** Compact one-line preview of an arbitrary tool input. */\nexport function summarizeValue(value: unknown, max = 72): string | undefined {\n  if (value == null) return undefined;\n  let text: string;\n  if (typeof value === 'string') text = value;\n  else if (typeof value === 'number' || typeof value === 'boolean') text = String(value);\n  else if (typeof value === 'object') {\n    const entries = Object.entries(value as Record<string, unknown>);\n    if (entries.length === 0) return undefined;\n    // Prefer the first string-ish field: it is usually the most descriptive (query, path, command).\n    const primary = entries.find(([, v]) => typeof v === 'string');\n    text = primary\n      ? String(primary[1])\n      : entries.map(([k, v]) => `${k}: ${typeof v === 'object' ? '…' : String(v)}`).join(', ');\n  } else return undefined;\n  text = text.replace(/\\s+/g, ' ').trim();\n  return text.length > max ? `${text.slice(0, max - 1)}…` : text;\n}\n\nexport function safeStringify(value: unknown, space = 2): string {\n  if (value === undefined) return 'undefined';\n  if (typeof value === 'string') return JSON.stringify(value);\n  const seen = new WeakSet<object>();\n  try {\n    return (\n      JSON.stringify(\n        value,\n        (_key, v: unknown) => {\n          if (typeof v === 'bigint') return `${v.toString()}n`;\n          if (typeof v === 'object' && v !== null) {\n            if (seen.has(v)) return '[Circular]';\n            seen.add(v);\n          }\n          return v;\n        },\n        space,\n      ) ?? String(value)\n    );\n  } catch {\n    return String(value);\n  }\n}\n",
      "type": "registry:lib",
      "target": "components/agent-ui/lib/format.ts"
    },
    {
      "path": "packages/agent-ui-kit/src/lib/highlight.ts",
      "content": "/**\n * A deliberately small, line-oriented syntax tokenizer. It covers the languages\n * agents edit most (TS/JS/JSON/CSS/shell/YAML/Python) well enough for review UI\n * without shipping a grammar engine. Multi-line constructs are tokenized per line.\n */\nexport type TokenKind = 'plain' | 'keyword' | 'string' | 'number' | 'comment' | 'punctuation' | 'property';\n\nexport interface Token {\n  text: string;\n  kind: TokenKind;\n}\n\nconst JS_KEYWORDS = new Set(\n  (\n    'abstract as async await break case catch class const continue debugger declare default delete do else enum export extends ' +\n    'false finally for from function get if implements import in instanceof interface let new null of private protected public ' +\n    'readonly return satisfies set static super switch this throw true try type typeof undefined var void while with yield'\n  ).split(' '),\n);\nconst PY_KEYWORDS = new Set(\n  'and as assert async await break class continue def del elif else except False finally for from global if import in is lambda None nonlocal not or pass raise return True try while with yield'.split(\n    ' ',\n  ),\n);\nconst SH_KEYWORDS = new Set(\n  'if then else elif fi for in do done while case esac function export local return'.split(' '),\n);\nconst LITERALS = new Set(['true', 'false', 'null']);\n\nconst HASH_COMMENT = new Set(['sh', 'py', 'yaml']);\n\nconst RULES: Array<[RegExp, TokenKind | 'word']> = [\n  [/^\\/\\/.*/, 'comment'],\n  [/^\\/\\*.*?(\\*\\/|$)/, 'comment'],\n  [/^\"(?:[^\"\\\\]|\\\\.)*\"?/, 'string'],\n  [/^'(?:[^'\\\\]|\\\\.)*'?/, 'string'],\n  [/^`(?:[^`\\\\]|\\\\.)*`?/, 'string'],\n  [/^-?(?:0x[\\da-f]+|\\d[\\d_]*(?:\\.\\d+)?(?:e[+-]?\\d+)?)\\b/i, 'number'],\n  [/^[A-Za-z_$][\\w$-]*/, 'word'],\n  [/^\\s+/, 'plain'],\n  [/^[{}()[\\];,.<>/=+\\-*!?:&|%^~@#]/, 'punctuation'],\n];\n\nexport function tokenizeLine(line: string, language = 'text'): Token[] {\n  if (language === 'text' || language === 'md' || !line) return [{ text: line, kind: 'plain' }];\n  const tokens: Token[] = [];\n  const keywords = language === 'py' ? PY_KEYWORDS : language === 'sh' ? SH_KEYWORDS : JS_KEYWORDS;\n  let rest = line;\n  while (rest.length > 0) {\n    if (HASH_COMMENT.has(language) && rest.startsWith('#')) {\n      tokens.push({ text: rest, kind: 'comment' });\n      break;\n    }\n    let matched = false;\n    for (const [re, kind] of RULES) {\n      if (\n        (kind === 'comment' && HASH_COMMENT.has(language)) ||\n        (language === 'css' && kind === 'comment' && re.source.startsWith('^\\\\/\\\\/'))\n      )\n        continue;\n      const m = re.exec(rest);\n      if (!m) continue;\n      const text = m[0];\n      let resolved: TokenKind;\n      if (kind === 'word') {\n        if (language === 'json') resolved = LITERALS.has(text) ? 'keyword' : 'plain';\n        else if (language === 'css') resolved = /^\\s*:/.test(rest.slice(text.length)) ? 'property' : 'plain';\n        else if (language === 'yaml')\n          resolved = /^\\s*:/.test(rest.slice(text.length)) ? 'property' : LITERALS.has(text) ? 'keyword' : 'plain';\n        else resolved = keywords.has(text) ? 'keyword' : 'plain';\n        // Identifiers may not contain '-' outside CSS/YAML/shell; give the dash back.\n        if (text.includes('-') && !['css', 'yaml', 'sh'].includes(language)) {\n          const word = text.slice(0, text.indexOf('-'));\n          tokens.push({ text: word, kind: keywords.has(word) ? 'keyword' : 'plain' });\n          rest = rest.slice(word.length);\n          matched = true;\n          break;\n        }\n      } else if (\n        kind === 'string' &&\n        (language === 'json' || language === 'yaml') &&\n        /^\\s*:/.test(rest.slice(text.length))\n      ) {\n        resolved = 'property';\n      } else {\n        resolved = kind;\n      }\n      pushToken(tokens, text, resolved);\n      rest = rest.slice(text.length);\n      matched = true;\n      break;\n    }\n    if (!matched) {\n      pushToken(tokens, rest[0]!, 'plain');\n      rest = rest.slice(1);\n    }\n  }\n  return tokens;\n}\n\nfunction pushToken(tokens: Token[], text: string, kind: TokenKind) {\n  const prev = tokens.at(-1);\n  if (prev && prev.kind === kind) prev.text += text;\n  else tokens.push({ text, kind });\n}\n\nexport interface HighlightedPiece extends Token {\n  changed: boolean;\n}\n\n/** Split syntax tokens along word-diff boundaries so both can be rendered at once. */\nexport function mergeTokensWithSegments(\n  tokens: readonly Token[],\n  segments: ReadonlyArray<{ text: string; changed: boolean }> | undefined,\n): HighlightedPiece[] {\n  if (!segments || segments.length === 0) return tokens.map((t) => ({ ...t, changed: false }));\n  const out: HighlightedPiece[] = [];\n  let ti = 0;\n  let tOffset = 0;\n  for (const seg of segments) {\n    let remaining = seg.text.length;\n    while (remaining > 0 && ti < tokens.length) {\n      const token = tokens[ti]!;\n      const available = token.text.length - tOffset;\n      const take = Math.min(available, remaining);\n      out.push({ text: token.text.slice(tOffset, tOffset + take), kind: token.kind, changed: seg.changed });\n      remaining -= take;\n      tOffset += take;\n      if (tOffset >= token.text.length) {\n        ti++;\n        tOffset = 0;\n      }\n    }\n  }\n  return out;\n}\n\nexport const TOKEN_CLASS: Record<TokenKind, string> = {\n  plain: '',\n  keyword: 'text-aui-code-keyword',\n  string: 'text-aui-code-string',\n  number: 'text-aui-code-number',\n  comment: 'text-aui-code-comment italic',\n  punctuation: 'text-aui-code-punctuation',\n  property: 'text-aui-code-keyword',\n};\n",
      "type": "registry:lib",
      "target": "components/agent-ui/lib/highlight.ts"
    },
    {
      "path": "packages/agent-ui-kit/src/lib/hooks.ts",
      "content": "'use client';\n\nimport { useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore } from 'react';\nimport type { ChatStatus } from 'ai';\nimport { getToolParts, observeToolTimings, type AnyUIPart, type ToolTimings } from './ai';\n\nexport type { ToolTiming, ToolTimings } from './ai';\n\nconst useIsoLayoutEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect;\n\nconst REDUCED_MOTION = '(prefers-reduced-motion: reduce)';\n\nfunction subscribeReducedMotion(callback: () => void) {\n  if (typeof window === 'undefined' || !window.matchMedia) return () => {};\n  const mql = window.matchMedia(REDUCED_MOTION);\n  mql.addEventListener('change', callback);\n  return () => mql.removeEventListener('change', callback);\n}\n\n/** Tracks the user's `prefers-reduced-motion` setting. */\nexport function usePrefersReducedMotion(): boolean {\n  return useSyncExternalStore(\n    subscribeReducedMotion,\n    () => (typeof window !== 'undefined' && window.matchMedia ? window.matchMedia(REDUCED_MOTION).matches : false),\n    () => false,\n  );\n}\n\n/**\n * Current time in ms, re-rendering on an interval while `active`.\n * The value can lag by up to one interval; clamp derived durations at 0.\n *\n * 0 on the server: reading the clock there makes the render impure (Next.js `cacheComponents`\n * fails the build on it), and clock-derived output waits for hydration anyway (`useHydrated`).\n */\nexport function useNow(active: boolean, intervalMs = 100): number {\n  const [now, setNow] = useState(() => (typeof window === 'undefined' ? 0 : Date.now()));\n  useEffect(() => {\n    if (!active) return;\n    const tick = () => setNow(Date.now());\n    const first = setTimeout(tick, 0);\n    const id = setInterval(tick, intervalMs);\n    return () => {\n      clearTimeout(first);\n      clearInterval(id);\n    };\n  }, [active, intervalMs]);\n  return now;\n}\n\n/** A tiny external store: subscribe/getSnapshot plus an `update` that only notifies on change. */\nfunction createStore<T>(initial: T) {\n  let state = initial;\n  const listeners = new Set<() => void>();\n  return {\n    get: () => state,\n    set(next: T) {\n      if (next === state) return;\n      state = next;\n      listeners.forEach((l) => l());\n    },\n    subscribe(listener: () => void) {\n      listeners.add(listener);\n      return () => listeners.delete(listener);\n    },\n  };\n}\n\n/**\n * Measures tool-call timings client-side by watching AI SDK state transitions.\n * Calls that are already settled when first observed (e.g. restored history)\n * get no timing rather than a misleading zero.\n */\nexport function useToolTimings(parts: readonly AnyUIPart[]): ToolTimings {\n  const [store] = useState(() => createStore<ToolTimings>({}));\n  const snapshot = useSyncExternalStore(store.subscribe, store.get, store.get);\n  useIsoLayoutEffect(() => {\n    store.set(observeToolTimings(store.get(), getToolParts(parts), Date.now()));\n  }, [parts, store]);\n  return snapshot;\n}\n\nexport interface RunTiming {\n  /** When the run was first seen in flight: its first request, or later if it was already running. */\n  startedAt?: number | undefined;\n  /** When the run's first response arrived. Undefined while unknown, as `ttftMs`. */\n  firstTokenAt?: number | undefined;\n  /** Milliseconds the agent spent working, excluding time waiting on the user. */\n  activeMs: number;\n  /**\n   * Time to first token of the run: from submitting its first request to the first streamed status.\n   * A first request that ends without a `streaming` status (its response arrived in one burst) counts\n   * its end as the first token. Undefined until then, and when it cannot be known: the run was already\n   * streaming when first seen, its first request failed, or (given the messages) it ended with no reply.\n   */\n  ttftMs?: number | undefined;\n  isRunning: boolean;\n}\n\n/** What `useRunTiming` reads of a message. AI SDK `UIMessage`s and `useAgUiAgent`'s messages fit. */\nexport interface RunTimingMessage {\n  id: string;\n  role: string;\n}\n\ninterface RunTimingState {\n  /** The run's key, and the last status seen. */\n  key?: unknown;\n  status?: ChatStatus | undefined;\n  startedAt?: number | undefined;\n  /** When the run's first request was submitted, while its first token can still be timed. */\n  submittedAt?: number | undefined;\n  firstTokenAt?: number | undefined;\n  accumulatedMs: number;\n  segmentStart?: number | undefined;\n}\n\ninterface RunTimingInput {\n  status: ChatStatus;\n  now: number;\n  /** A new run starts when it changes. */\n  key: unknown;\n  /** Whether a request submitted after the last one ended starts a new run. */\n  newRunOnSubmit: boolean;\n  /** Whether a message follows the last user message. Undefined without the messages. */\n  replied: boolean | undefined;\n}\n\n/**\n * Derives run latency from `useChat` status transitions: time to first token, and active time. A run\n * can span several requests (e.g. continuing after an approval); time spent waiting on the human\n * between requests is excluded from `activeMs`.\n *\n * Pass the chat's `messages` (`useChat().messages`, or `useAgUiAgent`'s): a run then starts with each\n * user message (or a regenerated reply) and spans its approval round trips. Without them, each request\n * submitted after the last one ended starts a new run, so a turn that pauses for approval is timed per\n * request. A key of your own instead (any value but an array) starts a new run only when it changes.\n */\nexport function useRunTiming(status: ChatStatus, messages?: readonly RunTimingMessage[]): RunTiming;\nexport function useRunTiming(status: ChatStatus, resetKey: unknown): RunTiming;\nexport function useRunTiming(status: ChatStatus, messagesOrKey?: unknown): RunTiming {\n  const [store] = useState(() => createStore<RunTimingState>({ accumulatedMs: 0 }));\n  const state = useSyncExternalStore(store.subscribe, store.get, store.get);\n  let key = messagesOrKey;\n  let replied: boolean | undefined;\n  if (Array.isArray(messagesOrKey)) {\n    const messages = messagesOrKey as readonly RunTimingMessage[];\n    let lastUser = messages.length - 1;\n    while (lastUser >= 0 && messages[lastUser]?.role !== 'user') lastUser--;\n    key = messages[lastUser]?.id;\n    replied = lastUser < messages.length - 1;\n  }\n  // With the messages, a request submitted with no reply after the last user message is a new turn\n  // (or a regenerated reply); one that continues a reply, after an approval, is the same run.\n  const newRunOnSubmit = messagesOrKey === undefined || replied === false;\n  useIsoLayoutEffect(() => {\n    store.set(reduceRunTiming(store.get(), { status, now: Date.now(), key, newRunOnSubmit, replied }));\n  }, [status, key, newRunOnSubmit, replied, store]);\n  const isRunning = state.segmentStart !== undefined;\n  const now = useNow(isRunning, 100);\n  return {\n    startedAt: state.startedAt,\n    firstTokenAt: state.firstTokenAt,\n    activeMs: state.accumulatedMs + (state.segmentStart !== undefined ? Math.max(0, now - state.segmentStart) : 0),\n    ttftMs:\n      state.submittedAt !== undefined && state.firstTokenAt !== undefined\n        ? state.firstTokenAt - state.submittedAt\n        : undefined,\n    isRunning,\n  };\n}\n\nconst isInFlight = (status: ChatStatus | undefined) => status === 'submitted' || status === 'streaming';\n\nfunction reduceRunTiming(prev: RunTimingState, input: RunTimingInput): RunTimingState {\n  const { status, now, key, newRunOnSubmit, replied } = input;\n  const submitted = status === 'submitted' && !isInFlight(prev.status);\n  const s: RunTimingState =\n    !Object.is(key, prev.key) || (submitted && newRunOnSubmit) ? { key, accumulatedMs: 0 } : prev;\n  if (isInFlight(status)) {\n    const startedAt = s.startedAt ?? now;\n    // The first token is timed only from the run's first request, seen from its submission: a run\n    // first seen already streaming has an unknown time to first token, not 0.\n    const submittedAt = s.startedAt === undefined ? (status === 'submitted' ? now : undefined) : s.submittedAt;\n    const segmentStart = s.segmentStart ?? now;\n    const firstTokenAt = status === 'streaming' && submittedAt !== undefined ? (s.firstTokenAt ?? now) : s.firstTokenAt;\n    if (\n      s === prev &&\n      status === s.status &&\n      startedAt === s.startedAt &&\n      submittedAt === s.submittedAt &&\n      segmentStart === s.segmentStart &&\n      firstTokenAt === s.firstTokenAt\n    )\n      return s;\n    return { ...s, status, startedAt, submittedAt, segmentStart, firstTokenAt };\n  }\n  if (s.segmentStart === undefined) return status === s.status && s === prev ? s : { ...s, status };\n  // useChat can go straight from submitted to ready when a response arrives in one burst; then the\n  // response is the first token, unless the messages show that none arrived (it was stopped).\n  const burst = s.firstTokenAt === undefined && s.submittedAt !== undefined && status === 'ready' && replied !== false;\n  const firstTokenAt = burst ? now : s.firstTokenAt;\n  return {\n    ...s,\n    status,\n    firstTokenAt,\n    // Once the first request has ended, a later one (after an approval wait) is not the first token.\n    submittedAt: firstTokenAt === undefined ? undefined : s.submittedAt,\n    accumulatedMs: s.accumulatedMs + (now - s.segmentStart),\n    segmentStart: undefined,\n  };\n}\n\n/** Eases a number toward its target; jumps immediately under reduced motion. */\nexport function useAnimatedNumber(value: number, durationMs = 450): number {\n  const reduced = usePrefersReducedMotion();\n  const [display, setDisplay] = useState(value);\n  const displayRef = useRef(value);\n  useEffect(() => {\n    if (reduced || typeof requestAnimationFrame === 'undefined') {\n      displayRef.current = value;\n      return;\n    }\n    const from = displayRef.current;\n    if (from === value) return;\n    const start = performance.now();\n    let frame = 0;\n    const tick = (t: number) => {\n      const p = Math.min(1, (t - start) / durationMs);\n      const eased = 1 - Math.pow(1 - p, 3);\n      const v = from + (value - from) * eased;\n      displayRef.current = v;\n      setDisplay(v);\n      if (p < 1) frame = requestAnimationFrame(tick);\n    };\n    frame = requestAnimationFrame(tick);\n    return () => cancelAnimationFrame(frame);\n  }, [value, durationMs, reduced]);\n  return reduced || typeof requestAnimationFrame === 'undefined' ? value : display;\n}\n\nexport interface ActivityWindow {\n  startedAt?: number | undefined;\n  endedAt?: number | undefined;\n}\n\n/** Records when `active` last turned on and off. Restarts when it turns on again. */\nexport function useActivityWindow(active: boolean): ActivityWindow {\n  const [store] = useState(() => createStore<ActivityWindow>({}));\n  const state = useSyncExternalStore(store.subscribe, store.get, store.get);\n  useIsoLayoutEffect(() => {\n    const cur = store.get();\n    const now = Date.now();\n    if (active && (cur.startedAt === undefined || cur.endedAt !== undefined)) store.set({ startedAt: now });\n    else if (!active && cur.startedAt !== undefined && cur.endedAt === undefined) store.set({ ...cur, endedAt: now });\n  }, [active, store]);\n  return state;\n}\n\nconst noopSubscribe = () => () => {};\nconst detectMac = () =>\n  typeof navigator !== 'undefined' && /Mac|iPhone|iPad|iPod/.test(navigator.platform || navigator.userAgent);\n\n/** Platform check that is safe to use during hydration (server renders the non-Mac variant). */\nexport function useIsMac(): boolean {\n  return useSyncExternalStore(noopSubscribe, detectMac, () => false);\n}\n\nconst hydratedSnapshot = () => true;\nconst serverSnapshot = () => false;\n\n/**\n * False during SSR and hydration, true afterwards. Clock-derived values (live\n * durations, waterfall widths) render only once hydrated so server and client markup match.\n */\nexport function useHydrated(): boolean {\n  return useSyncExternalStore(noopSubscribe, hydratedSnapshot, serverSnapshot);\n}\n",
      "type": "registry:lib",
      "target": "components/agent-ui/lib/hooks.ts"
    },
    {
      "path": "packages/agent-ui-kit/src/lib/icons.tsx",
      "content": "import type { SVGProps } from 'react';\n\ntype IconProps = SVGProps<SVGSVGElement> & { size?: number };\n\nfunction Svg({ size = 16, children, ...props }: IconProps) {\n  return (\n    <svg\n      width={size}\n      height={size}\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth={2}\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      aria-hidden=\"true\"\n      focusable=\"false\"\n      {...props}\n    >\n      {children}\n    </svg>\n  );\n}\n\nexport const CheckIcon = (p: IconProps) => (\n  <Svg {...p}>\n    <path d=\"M5 12.5 9.5 17 19 7.5\" />\n  </Svg>\n);\nexport const XIcon = (p: IconProps) => (\n  <Svg {...p}>\n    <path d=\"M6.5 6.5l11 11M17.5 6.5l-11 11\" />\n  </Svg>\n);\nexport const ChevronIcon = (p: IconProps) => (\n  <Svg {...p}>\n    <path d=\"M9 6l6 6-6 6\" />\n  </Svg>\n);\nexport const BanIcon = (p: IconProps) => (\n  <Svg {...p}>\n    <circle cx=\"12\" cy=\"12\" r=\"8.5\" />\n    <path d=\"M6 6l12 12\" />\n  </Svg>\n);\nexport const AlertIcon = (p: IconProps) => (\n  <Svg {...p}>\n    <path d=\"M12 3.5 21.5 20h-19L12 3.5Z\" />\n    <path d=\"M12 10v4.5M12 17.25v.25\" />\n  </Svg>\n);\nexport const ShieldIcon = (p: IconProps) => (\n  <Svg {...p}>\n    <path d=\"M12 3 19.5 6v5.5c0 4.6-3.1 8-7.5 9.5-4.4-1.5-7.5-4.9-7.5-9.5V6L12 3Z\" />\n    <path d=\"M12 8.5v4M12 15.5v.25\" />\n  </Svg>\n);\nexport const SparkIcon = (p: IconProps) => (\n  <Svg {...p}>\n    <path d=\"M12 3.5c.6 4.4 2.6 6.9 7.5 8.5-4.9 1.6-6.9 4.1-7.5 8.5-.6-4.4-2.6-6.9-7.5-8.5 4.9-1.6 6.9-4.1 7.5-8.5Z\" />\n  </Svg>\n);\nexport const CopyIcon = (p: IconProps) => (\n  <Svg {...p}>\n    <rect x=\"8.5\" y=\"8.5\" width=\"12\" height=\"12\" rx=\"2.5\" />\n    <path d=\"M15.5 5.5v-.5a2 2 0 0 0-2-2H5.5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h.5\" />\n  </Svg>\n);\nexport const ExternalIcon = (p: IconProps) => (\n  <Svg {...p}>\n    <path d=\"M14 4h6v6M20 4l-9 9\" />\n    <path d=\"M18 14v4.5a1.5 1.5 0 0 1-1.5 1.5h-11A1.5 1.5 0 0 1 4 18.5v-11A1.5 1.5 0 0 1 5.5 6H10\" />\n  </Svg>\n);\nexport const FileIcon = (p: IconProps) => (\n  <Svg {...p}>\n    <path d=\"M14 3.5H7A1.5 1.5 0 0 0 5.5 5v14A1.5 1.5 0 0 0 7 20.5h10a1.5 1.5 0 0 0 1.5-1.5V8L14 3.5Z\" />\n    <path d=\"M14 3.5V8h4.5\" />\n  </Svg>\n);\nexport const ImageIcon = (p: IconProps) => (\n  <Svg {...p}>\n    <rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\" />\n    <circle cx=\"9\" cy=\"9\" r=\"2\" />\n    <path d=\"m21 15-3.1-3.1a2 2 0 0 0-2.8 0L6 21\" />\n  </Svg>\n);\nexport const ToolIcon = (p: IconProps) => (\n  <Svg {...p}>\n    <path d=\"M13 3 5.5 13.5h6L10.5 21 18.5 10.5h-6L13 3Z\" />\n  </Svg>\n);\nexport const TerminalIcon = (p: IconProps) => (\n  <Svg {...p}>\n    <rect x=\"3\" y=\"4.5\" width=\"18\" height=\"15\" rx=\"2.5\" />\n    <path d=\"M7.5 9.5 10.5 12l-3 2.5M12.5 15h4\" />\n  </Svg>\n);\nexport const ClockIcon = (p: IconProps) => (\n  <Svg {...p}>\n    <circle cx=\"12\" cy=\"12\" r=\"8.5\" />\n    <path d=\"M12 7.5V12l3 2\" />\n  </Svg>\n);\nexport const ArrowUpIcon = (p: IconProps) => (\n  <Svg {...p}>\n    <path d=\"M12 19V5M6 11l6-6 6 6\" />\n  </Svg>\n);\nexport const ArrowDownIcon = (p: IconProps) => (\n  <Svg {...p}>\n    <path d=\"M12 5v14M6 13l6 6 6-6\" />\n  </Svg>\n);\nexport const UndoIcon = (p: IconProps) => (\n  <Svg {...p}>\n    <path d=\"M9 14 4 9l5-5\" />\n    <path d=\"M4 9h10.5a5.5 5.5 0 0 1 0 11H11\" />\n  </Svg>\n);\n\n/** Indeterminate spinner: a quarter arc on a faint track. */\nexport const SpinnerIcon = ({ className, ...p }: IconProps) => (\n  <Svg {...p} className={className}>\n    <circle cx=\"12\" cy=\"12\" r=\"8.5\" opacity=\"0.25\" />\n    <path d=\"M12 3.5a8.5 8.5 0 0 1 8.5 8.5\" />\n  </Svg>\n);\n",
      "type": "registry:lib",
      "target": "components/agent-ui/lib/icons.tsx"
    },
    {
      "path": "packages/agent-ui-kit/src/lib/primitives.tsx",
      "content": "'use client';\n\nimport {\n  createElement,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n  type ComponentPropsWithoutRef,\n  type ReactNode,\n} from 'react';\nimport { CheckIcon, CopyIcon } from './icons';\nimport { safeStringify } from './format';\nimport { TOKEN_CLASS, tokenizeLine } from './highlight';\nimport { cn } from './utils';\n\n/**\n * An <img> for images from messages, written without JSX. Next.js' `no-img-element` lint rule\n * flags the element and suggests next/image, which cannot load these URLs without every host\n * configured; a comment disabling that rule would itself be an error where ESLint has no Next.js\n * plugin. The repository's ESLint config checks `Img` for alt text like an <img>.\n */\nexport function Img({ alt, ...props }: ComponentPropsWithoutRef<'img'> & { alt: string }) {\n  return createElement('img', { alt, ...props });\n}\n\n/** Keyboard key hint. Purely visual: shortcuts are also described in text for assistive tech. */\nexport function Kbd({ className, ...props }: ComponentPropsWithoutRef<'kbd'>) {\n  return (\n    <kbd\n      className={cn(\n        'border-aui-border-strong bg-aui-bg/40 font-aui-mono text-aui-fg-muted inline-flex h-5 min-w-5 items-center justify-center rounded-[5px] border px-1 text-[11px] leading-none font-medium',\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\n/** A visually hidden live region. Render it unconditionally so updates are announced. */\nexport function LiveRegion({\n  children,\n  politeness = 'polite',\n}: {\n  children?: ReactNode;\n  politeness?: 'polite' | 'assertive';\n}) {\n  return (\n    <div\n      className=\"sr-only\"\n      role={politeness === 'assertive' ? 'alert' : 'status'}\n      aria-live={politeness}\n      aria-atomic=\"true\"\n    >\n      {children}\n    </div>\n  );\n}\n\n/** Returns `value` after it has been stable for `delayMs`. */\nexport function useDebouncedValue<T>(value: T, delayMs: number): T {\n  const [debounced, setDebounced] = useState(value);\n  useEffect(() => {\n    const id = setTimeout(() => setDebounced(value), delayMs);\n    return () => clearTimeout(id);\n  }, [value, delayMs]);\n  return debounced;\n}\n\nexport function CopyButton({ text, label = 'Copy', className }: { text: string; label?: string; className?: string }) {\n  const [copied, setCopied] = useState(false);\n  const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n  useEffect(() => () => clearTimeout(timer.current), []);\n  return (\n    <button\n      type=\"button\"\n      onClick={async () => {\n        try {\n          await navigator.clipboard.writeText(text);\n          setCopied(true);\n          clearTimeout(timer.current);\n          timer.current = setTimeout(() => setCopied(false), 1500);\n        } catch {\n          // Clipboard can be unavailable (permissions, insecure context); fail quietly.\n        }\n      }}\n      aria-label={copied ? 'Copied' : label}\n      className={cn(\n        'text-aui-fg-subtle hover:bg-aui-surface-2 hover:text-aui-fg focus-visible:outline-aui-ring inline-flex size-7 cursor-pointer items-center justify-center rounded-md transition-colors focus-visible:outline-2 focus-visible:outline-offset-1',\n        className,\n      )}\n    >\n      {copied ? <CheckIcon size={14} className=\"text-aui-accent-fg\" /> : <CopyIcon size={14} />}\n    </button>\n  );\n}\n\n/** Syntax-tinted, line-tokenized code. */\nexport function CodeLines({ code, language }: { code: string; language: string }) {\n  const lines = useMemo(() => code.split('\\n'), [code]);\n  return (\n    <code>\n      {lines.map((line, i) => (\n        <span key={i} className=\"block min-h-[1lh]\">\n          {tokenizeLine(line, language).map((t, j) => (\n            <span key={j} className={TOKEN_CLASS[t.kind] || undefined}>\n              {t.text}\n            </span>\n          ))}\n        </span>\n      ))}\n    </code>\n  );\n}\n\nexport interface JsonViewProps {\n  value: unknown;\n  /** Accessible name for the code block, e.g. \"Input\". */\n  label: string;\n  /** Lines shown before \"Show all\" collapses the rest. */\n  collapseAfter?: number;\n  className?: string;\n}\n\n/** Read-only JSON with light syntax tinting, a copy button and long-output folding. */\nexport function JsonView({ value, label, collapseAfter = 24, className }: JsonViewProps) {\n  const text = useMemo(() => (typeof value === 'string' ? value : safeStringify(value)), [value]);\n  const language = typeof value === 'string' ? 'text' : 'json';\n  const lineCount = useMemo(() => text.split('\\n').length, [text]);\n  const [showAll, setShowAll] = useState(false);\n  const folded = !showAll && lineCount > collapseAfter;\n  const shown = folded ? text.split('\\n').slice(0, collapseAfter).join('\\n') : text;\n  return (\n    <div data-slot=\"json-view\" className={cn('group/json relative', className)}>\n      <pre\n        role=\"group\"\n        aria-label={label}\n        tabIndex={0}\n        className=\"border-aui-border bg-aui-bg/60 font-aui-mono text-aui-fg focus-visible:outline-aui-ring max-h-80 overflow-auto rounded-lg border px-3 py-2.5 text-xs leading-5 whitespace-pre focus-visible:outline-2 focus-visible:outline-offset-1\"\n      >\n        <CodeLines code={shown} language={language} />\n      </pre>\n      <div className=\"absolute top-1.5 right-1.5 opacity-0 transition-opacity group-focus-within/json:opacity-100 group-hover/json:opacity-100 [@media(hover:none)]:opacity-100\">\n        <CopyButton text={text} label={`Copy ${label.toLowerCase()}`} />\n      </div>\n      {lineCount > collapseAfter && (\n        <button\n          type=\"button\"\n          onClick={() => setShowAll((v) => !v)}\n          className=\"font-aui-mono text-aui-fg-subtle hover:text-aui-fg focus-visible:outline-aui-ring mt-1.5 cursor-pointer rounded text-[11px] underline-offset-2 hover:underline focus-visible:outline-2 focus-visible:outline-offset-1\"\n        >\n          {showAll ? 'Show less' : `Show all ${lineCount} lines`}\n        </button>\n      )}\n    </div>\n  );\n}\n\nexport const focusRing = 'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-aui-ring';\n",
      "type": "registry:lib",
      "target": "components/agent-ui/lib/primitives.tsx"
    },
    {
      "path": "packages/agent-ui-kit/src/lib/utils.ts",
      "content": "import { clsx, type ClassValue } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\n/** Merge class names, resolving Tailwind utility conflicts (last one wins). */\nexport function cn(...inputs: ClassValue[]): string {\n  return twMerge(clsx(inputs));\n}\n\n/** True when a keyboard event originates from a text-entry control. */\nexport function isTypingTarget(target: EventTarget | null): boolean {\n  if (!(target instanceof HTMLElement)) return false;\n  if (target.isContentEditable) return true;\n  const tag = target.tagName;\n  if (tag === 'TEXTAREA' || tag === 'SELECT') return true;\n  if (tag === 'INPUT') {\n    const type = (target as HTMLInputElement).type;\n    return !['button', 'checkbox', 'radio', 'submit', 'reset'].includes(type);\n  }\n  return false;\n}\n\n/** True for a promise or any other thenable, e.g. what an async event handler returns. */\nexport function isPromiseLike(value: unknown): value is PromiseLike<unknown> {\n  return (\n    value !== null &&\n    (typeof value === 'object' || typeof value === 'function') &&\n    typeof (value as { then?: unknown }).then === 'function'\n  );\n}\n\n/** True when a key event carries a modifier we should not intercept. */\nexport function hasModifier(event: { altKey: boolean; ctrlKey: boolean; metaKey: boolean }) {\n  return event.altKey || event.ctrlKey || event.metaKey;\n}\n\n/** Heading levels a component title can render as. */\nexport type HeadingLevel = 2 | 3 | 4 | 5 | 6;\n",
      "type": "registry:lib",
      "target": "components/agent-ui/lib/utils.ts"
    }
  ],
  "cssVars": {
    "theme": {
      "color-aui-bg": "var(--aui-bg)",
      "color-aui-surface": "var(--aui-surface)",
      "color-aui-surface-2": "var(--aui-surface-2)",
      "color-aui-border": "var(--aui-border)",
      "color-aui-border-strong": "var(--aui-border-strong)",
      "color-aui-fg": "var(--aui-fg)",
      "color-aui-fg-muted": "var(--aui-fg-muted)",
      "color-aui-fg-subtle": "var(--aui-fg-subtle)",
      "color-aui-accent": "var(--aui-accent)",
      "color-aui-accent-fg": "var(--aui-accent-fg)",
      "color-aui-on-accent": "var(--aui-on-accent)",
      "color-aui-hot": "var(--aui-hot)",
      "color-aui-hot-fg": "var(--aui-hot-fg)",
      "color-aui-on-hot": "var(--aui-on-hot)",
      "color-aui-warn": "var(--aui-warn)",
      "color-aui-warn-fg": "var(--aui-warn-fg)",
      "color-aui-ring": "var(--aui-ring)",
      "color-aui-add": "var(--aui-add-bg)",
      "color-aui-add-strong": "var(--aui-add-strong)",
      "color-aui-del": "var(--aui-del-bg)",
      "color-aui-del-strong": "var(--aui-del-strong)",
      "color-aui-chart-input": "var(--aui-chart-input)",
      "color-aui-chart-output": "var(--aui-chart-output)",
      "color-aui-code-keyword": "var(--aui-code-keyword)",
      "color-aui-code-string": "var(--aui-code-string)",
      "color-aui-code-number": "var(--aui-code-number)",
      "color-aui-code-comment": "var(--aui-code-comment)",
      "color-aui-code-punctuation": "var(--aui-code-punctuation)",
      "radius-aui": "var(--aui-radius)",
      "font-aui-sans": "var(--aui-font-sans)",
      "font-aui-mono": "var(--aui-font-mono)",
      "animate-aui-shimmer": "aui-shimmer 2.2s linear infinite",
      "animate-aui-ping": "aui-ping 1.6s cubic-bezier(0, 0, 0.2, 1) infinite",
      "animate-aui-spin": "aui-spin 0.9s linear infinite",
      "animate-aui-blink": "aui-blink 1.1s steps(2, start) infinite",
      "animate-aui-enter": "aui-enter 0.32s cubic-bezier(0.2, 0.7, 0.2, 1) both",
      "animate-aui-indeterminate": "aui-indeterminate 1.4s ease-in-out infinite",
      "animate-aui-expand": "aui-expand 0.22s cubic-bezier(0.2, 0.7, 0.2, 1)",
      "animate-aui-collapse": "aui-collapse 0.18s ease-in"
    },
    "light": {
      "aui-bg": "#ffffff",
      "aui-surface": "#f6f7f9",
      "aui-surface-2": "#eef0f3",
      "aui-border": "#e2e5ea",
      "aui-border-strong": "#cdd2d9",
      "aui-fg": "#0d0f12",
      "aui-fg-muted": "#4b5563",
      "aui-fg-subtle": "#5f6875",
      "aui-accent": "#0e7490",
      "aui-accent-fg": "#155e75",
      "aui-on-accent": "#ffffff",
      "aui-hot": "#be185d",
      "aui-hot-fg": "#9d174d",
      "aui-on-hot": "#ffffff",
      "aui-warn": "#b45309",
      "aui-warn-fg": "#92400e",
      "aui-ring": "#0e7490",
      "aui-add-bg": "rgb(8 145 178 / 0.08)",
      "aui-add-strong": "rgb(8 145 178 / 0.16)",
      "aui-del-bg": "rgb(190 24 93 / 0.07)",
      "aui-del-strong": "rgb(190 24 93 / 0.14)",
      "aui-chart-input": "#0891b2",
      "aui-chart-output": "#db2777",
      "aui-code-keyword": "#155e75",
      "aui-code-string": "#9a3412",
      "aui-code-number": "#9d174d",
      "aui-code-comment": "#525b67",
      "aui-code-punctuation": "#4b5563",
      "aui-radius": "0.75rem",
      "aui-font-sans": "var(--font-sans, var(--font-geist-sans, ui-sans-serif, system-ui, sans-serif))",
      "aui-font-mono": "var(--font-mono, var(--font-geist-mono, ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace))"
    },
    "dark": {
      "aui-bg": "#0d0f12",
      "aui-surface": "#181c22",
      "aui-surface-2": "#1e232a",
      "aui-border": "#262b33",
      "aui-border-strong": "#353c47",
      "aui-fg": "#e8eaed",
      "aui-fg-muted": "#a1a9b4",
      "aui-fg-subtle": "#8b94a0",
      "aui-accent": "#22d3ee",
      "aui-accent-fg": "#67e8f9",
      "aui-on-accent": "#0d0f12",
      "aui-hot": "#f0468a",
      "aui-hot-fg": "#f472a8",
      "aui-on-hot": "#0d0f12",
      "aui-warn": "#fbbf24",
      "aui-warn-fg": "#fcd34d",
      "aui-ring": "#67e8f9",
      "aui-add-bg": "rgb(34 211 238 / 0.08)",
      "aui-add-strong": "rgb(34 211 238 / 0.14)",
      "aui-del-bg": "rgb(240 70 138 / 0.08)",
      "aui-del-strong": "rgb(240 70 138 / 0.26)",
      "aui-chart-input": "#11a2bc",
      "aui-chart-output": "#f0468a",
      "aui-code-keyword": "#67e8f9",
      "aui-code-string": "#f5c27a",
      "aui-code-number": "#f9a8d4",
      "aui-code-comment": "#a1a9b4",
      "aui-code-punctuation": "#a1a9b4"
    }
  },
  "css": {
    "@keyframes aui-shimmer": {
      "from": {
        "background-position": "200% 0"
      },
      "to": {
        "background-position": "-200% 0"
      }
    },
    "@keyframes aui-ping": {
      "0%": {
        "transform": "scale(1)",
        "opacity": "0.7"
      },
      "80%, 100%": {
        "transform": "scale(2.2)",
        "opacity": "0"
      }
    },
    "@keyframes aui-spin": {
      "to": {
        "transform": "rotate(360deg)"
      }
    },
    "@keyframes aui-blink": {
      "to": {
        "visibility": "hidden"
      }
    },
    "@keyframes aui-enter": {
      "from": {
        "opacity": "0",
        "transform": "translateY(4px)"
      },
      "to": {
        "opacity": "1",
        "transform": "none"
      }
    },
    "@keyframes aui-expand": {
      "from": {
        "height": "0",
        "opacity": "0"
      },
      "to": {
        "height": "var(--radix-collapsible-content-height)",
        "opacity": "1"
      }
    },
    "@keyframes aui-collapse": {
      "from": {
        "height": "var(--radix-collapsible-content-height)",
        "opacity": "1"
      },
      "to": {
        "height": "0",
        "opacity": "0"
      }
    },
    "@keyframes aui-indeterminate": {
      "0%": {
        "transform": "translateX(-100%)"
      },
      "100%": {
        "transform": "translateX(250%)"
      }
    }
  },
  "type": "registry:component"
}