{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "agent-run",
  "title": "Agent Run",
  "description": "Animated brand-neutral AI agent run — a Thinking status and short plan, tool calls whose spinners close into checks while a timer counts real seconds, then a markdown answer with citations and a code block that streams in irregular token bursts. Light/dark themes.",
  "dependencies": ["remotion"],
  "files": [
    {
      "path": "registry/remocn/agent-run/index.tsx",
      "content": "\"use client\";\n\nimport { type CSSProperties, useMemo } from \"react\";\nimport { Easing, random, useCurrentFrame, useVideoConfig } from \"remotion\";\n\nexport type AgentRunTheme = \"light\" | \"dark\";\n\nexport type AgentRunIcon =\n  | \"chart\"\n  | \"database\"\n  | \"search\"\n  | \"book\"\n  | \"globe\"\n  | \"terminal\"\n  | \"file\";\n\nexport interface AgentRunStep {\n  name: string;\n  detail?: string;\n  duration: number;\n  icon?: AgentRunIcon;\n  sources?: readonly string[];\n}\n\nexport interface AgentRunScript {\n  title?: string;\n  prompt: string;\n  plan?: readonly string[];\n  steps?: readonly AgentRunStep[];\n  answer: string;\n}\n\nexport interface AgentRunProps {\n  run?: AgentRunScript;\n  theme?: AgentRunTheme;\n  accentColor?: string;\n  compression?: number;\n  seed?: number;\n  speed?: number;\n  className?: string;\n}\n\nexport interface AgentRunOptions {\n  seed?: number;\n  compression?: number;\n  width?: number;\n  height?: number;\n}\n\nexport interface AgentRunPlayback {\n  fps?: number;\n  speed?: number;\n}\n\nexport const defaultAgentRun: AgentRunScript = {\n  title: \"Checkout conversion drop\",\n  prompt: \"Why did checkout conversion drop last week?\",\n  plan: [\n    \"Compare the checkout funnel with the week before\",\n    \"Break the drop down by browser and step\",\n    \"Check what shipped and what the error logs say\",\n  ],\n  steps: [\n    {\n      icon: \"chart\",\n      name: \"Query analytics\",\n      detail: \"checkout funnel, last 7 days vs prior 7\",\n      duration: 1.4,\n    },\n    {\n      icon: \"database\",\n      name: \"Run SQL\",\n      detail: \"conversion by browser and step\",\n      duration: 2.3,\n    },\n    {\n      icon: \"search\",\n      name: \"Search logs\",\n      detail: \"Apple Pay errors on Safari\",\n      duration: 1.1,\n    },\n    {\n      icon: \"book\",\n      name: \"Read 12 sources\",\n      duration: 3.2,\n      sources: [\n        \"Checkout funnel\",\n        \"Browser report\",\n        \"Release 4.12\",\n        \"PR #2186\",\n        \"webkit.org\",\n        \"developer.mozilla.org\",\n        \"Payments runbook\",\n        \"Error log\",\n        \"Incident notes\",\n        \"Apple Pay guide\",\n        \"Support tickets\",\n        \"Status page\",\n      ],\n    },\n  ],\n  answer: [\n    \"## Apple Pay broke on Safari in release 4.12\",\n    \"\",\n    \"Checkout conversion fell from 3.4% to 2.7% last week, a 21% drop [1]. All of it comes from the payment step on Safari, 41% of checkouts [2].\",\n    \"\",\n    \"- Safari conversion halved, from 3.5% to 1.8%\",\n    \"- Chrome and Firefox held steady at 3.3%\",\n    '- 4.12 moved the card form into an iframe without `allow=\"payment\"` [3]',\n    \"\",\n    \"```tsx\",\n    \"<iframe\",\n    \"  src={cardFormUrl}\",\n    '  allow=\"payment\"',\n    \"/>\",\n    \"```\",\n    \"\",\n    \"**Fix:** add the attribute and ship 4.12.1 today.\",\n  ].join(\"\\n\"),\n};\n\nexport const agentRunDefaults: Required<Omit<AgentRunProps, \"className\">> = {\n  run: defaultAgentRun,\n  theme: \"light\",\n  accentColor: \"#2563eb\",\n  compression: 3,\n  seed: 1,\n  speed: 1,\n};\n\nconst PANEL_IN = 14;\nconst PANEL_RISE = 14;\nconst BUBBLE_AT = 4;\nconst BUBBLE_IN = 10;\nconst BUBBLE_RISE = 8;\nconst RUN_START = 16;\nconst STATUS_IN = 6;\nconst THINK_LEAD = 14;\nconst THINK_TAIL = 8;\nconst SWAP = 8;\nconst SWAP_RISE = 4;\nconst TOOL_GAP = 5;\nconst TOOL_MIN = 12;\nconst TOOL_MAX = 120;\nconst ANSWER_GAP = 8;\nconst ROW_IN = 8;\nconst ROW_FADE = 6;\nconst LABEL_DELAY = 2;\nconst LABEL_FADE = 6;\nconst SPIN = 12;\nconst ARC = 0.28;\nconst RESOLVE = 8;\nconst CHECK_FROM = 3;\nconst CHECK_TO = 10;\nconst CHIP_LEAD = 4;\nconst CHIP_SPAN = 0.7;\nconst FADE = 5;\nconst MORPH = 8;\nconst POP = 8;\nconst CODE_OPEN = 8;\nconst LINE_GROW = 5;\nconst SCROLL = 24;\nconst SCROLL_LEAD = 16;\nconst SETTLE = 16;\nconst HOLD = 45;\nconst SHIMMER_PERIOD = 30;\nconst SHIMMER_BAND = 3;\nconst SHIMMER_BASE = 0.45;\nconst DOT_BACK = 1.7;\nconst CITE_BACK = 2;\nconst CHIP_BACK = 1.7;\nconst MIN_COMPRESSION = 0.25;\nconst MAX_COMPRESSION = 20;\n\nconst SIZE_WEIGHTS = [8, 16, 22, 22, 17, 15];\nconst GAP_WEIGHTS = [42, 33, 16, 9];\nconst PAUSE_CHANCE = 0.06;\nconst PAUSE_MIN = 4;\nconst PAUSE_SPREAD = 4;\nconst BLOCK_MIN = 2;\nconst BLOCK_SPREAD = 3;\n\nconst REF_HEIGHT = 720;\nconst PANEL_W = 800;\nconst MARGIN_X = 40;\nconst MARGIN_Y = 40;\nconst MIN_PANEL_H = 360;\nconst MAX_PANEL_H = 960;\nconst HEADER_H = 48;\nconst PAD_X = 32;\nconst PAD_TOP = 24;\nconst SAFE = 40;\nconst COLUMN = PANEL_W - 2 * PAD_X;\nconst MEASURE = 680;\nconst BODY_SIZE = 16;\nconst BODY_LINE = 26;\nconst BUBBLE_SIZE = 16;\nconst BUBBLE_LINE = 24;\nconst BUBBLE_PAD_Y = 10;\nconst BUBBLE_PAD_X = 16;\nconst BUBBLE_WIDTH = 520;\nconst BUBBLE_GAP = 22;\nconst STATUS_H = 22;\nconst STATUS_GAP = 10;\nconst PLAN_SIZE = 15;\nconst PLAN_LINE = 24;\nconst PLAN_GAP = 4;\nconst PLAN_INDENT = 36;\nconst PLAN_WIDTH = 640;\nconst PLAN_AFTER = 20;\nconst ROW_H = 40;\nconst CHIPS_H = 34;\nconst TOOLS_AFTER = 24;\nconst ITEM_INDENT = 22;\nconst ITEM_GAP = 4;\nconst BLOCK_GAP = 12;\nconst HEADING_GAP = 8;\nconst CODE_GAP = 14;\nconst CODE_SIZE = 14;\nconst CODE_LINE = 22;\nconst CODE_HEAD = 34;\nconst CODE_PAD = 10;\nconst CHIP_SIZE = 12.5;\nconst CHIP_GAP = 6;\nconst CHIP_MAX = 6;\nconst CHIP_ROOM = COLUMN - 2 - 40 - 14;\nconst CIRCLE = 2 * Math.PI * 10;\nconst CHECK_LENGTH = 6 * Math.SQRT2;\n\nconst HEADINGS: Record<1 | 2, { size: number; line: number }> = {\n  1: { size: 22, line: 32 },\n  2: { size: 20, line: 30 },\n};\n\nconst SANS =\n  'Inter, \"Geist\", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif';\nconst MONO =\n  '\"JetBrains Mono\", \"Geist Mono\", ui-monospace, SFMono-Regular, Menlo, Consolas, \"Liberation Mono\", monospace';\n\nconst EXPO = Easing.bezier(0.16, 1, 0.3, 1);\n\nconst finite = (value: number | undefined, fallback: number) =>\n  typeof value === \"number\" && Number.isFinite(value) ? value : fallback;\nconst clamp = (value: number, min: number, max: number) =>\n  Math.min(max, Math.max(min, value));\nconst progress = (value: number) =>\n  Number.isNaN(value) ? 0 : clamp(value, 0, 1);\nconst round = (value: number) => Math.round(value * 1000) / 1000;\nconst expo = (value: number) => EXPO(progress(value));\nconst easeOut = (value: number) => 1 - (1 - progress(value)) ** 3;\nconst smoothstep = (value: number) => {\n  const t = progress(value);\n  return t * t * (3 - 2 * t);\n};\nconst backOut = (value: number, overshoot: number) => {\n  const t = progress(value) - 1;\n  return 1 + (overshoot + 1) * t ** 3 + overshoot * t ** 2;\n};\nconst rateOf = (speed: number | undefined) =>\n  Math.max(0, finite(speed, agentRunDefaults.speed));\nconst seedOf = (seed: number | undefined) =>\n  finite(seed, agentRunDefaults.seed);\nconst compressionOf = (compression: number | undefined) =>\n  clamp(\n    finite(compression, agentRunDefaults.compression),\n    MIN_COMPRESSION,\n    MAX_COMPRESSION,\n  );\n\nexport function formatAgentRunSeconds(seconds: number) {\n  const value = Math.max(0, finite(seconds, 0));\n  if (value < 60) return `${value.toFixed(1)}s`;\n  const minutes = Math.floor(value / 60);\n  const rest = Math.floor(value - minutes * 60);\n  return `${minutes}m ${String(rest).padStart(2, \"0\")}s`;\n}\n\nconst isLetter = (code: number) =>\n  (code >= 65 && code <= 90) || (code >= 97 && code <= 122) || code >= 192;\nconst isDigit = (code: number) => code >= 48 && code <= 57;\n\nexport function tokenizeAgentRun(text: string): number[] {\n  const ends: number[] = [];\n  let index = 0;\n  while (index < text.length) {\n    if (text[index] === \"\\n\") {\n      index += 1;\n      ends.push(index);\n      continue;\n    }\n    while (\n      index < text.length &&\n      (text[index] === \" \" || text[index] === \"\\t\")\n    ) {\n      index += 1;\n    }\n    if (index < text.length && text[index] !== \"\\n\") {\n      const code = text.charCodeAt(index);\n      if (isLetter(code)) {\n        while (index < text.length && isLetter(text.charCodeAt(index))) {\n          index += 1;\n        }\n      } else if (isDigit(code)) {\n        const stop = Math.min(text.length, index + 3);\n        while (index < stop && isDigit(text.charCodeAt(index))) {\n          index += 1;\n        }\n      } else {\n        index += 1;\n      }\n    }\n    ends.push(index);\n  }\n  return ends;\n}\n\nexport interface AgentRunChunk {\n  index: number;\n  at: number;\n  start: number;\n  end: number;\n  tokens: number;\n  block: boolean;\n}\n\nfunction pick(weights: readonly number[], value: number) {\n  const total = weights.reduce((sum, weight) => sum + weight, 0);\n  let cursor = value * total;\n  for (const [index, weight] of weights.entries()) {\n    if (cursor < weight) return index;\n    cursor -= weight;\n  }\n  return weights.length - 1;\n}\n\nexport function getAgentRunChunks(\n  text: string,\n  seed = agentRunDefaults.seed,\n  stream = \"answer\",\n): AgentRunChunk[] {\n  const ends = tokenizeAgentRun(text);\n  const startOf = (token: number) => (token > 0 ? ends[token - 1] : 0);\n  const isNewline = (token: number) =>\n    token < ends.length && text[startOf(token)] === \"\\n\";\n  const key = `agent-run-${seedOf(seed)}-${stream}`;\n  const chunks: AgentRunChunk[] = [];\n  let token = 0;\n  let at = 0;\n  let block = false;\n  while (token < ends.length) {\n    const index = chunks.length;\n    const size = pick(SIZE_WEIGHTS, random(`${key}-size-${index}`)) + 1;\n    let stop = Math.min(ends.length, token + size);\n    let cut = false;\n    for (let cursor = token; cursor < stop; cursor++) {\n      if (isNewline(cursor) && isNewline(cursor + 1)) {\n        stop = cursor + 2;\n        cut = true;\n        break;\n      }\n    }\n    if (!cut && isNewline(stop - 1) && isNewline(stop)) {\n      stop += 1;\n      cut = true;\n    }\n    while (cut && isNewline(stop)) stop += 1;\n    if (index > 0) {\n      at += pick(GAP_WEIGHTS, random(`${key}-gap-${index}`)) + 1;\n      if (random(`${key}-pause-${index}`) < PAUSE_CHANCE) {\n        const hold = random(`${key}-hold-${index}`) * PAUSE_SPREAD;\n        at += PAUSE_MIN + Math.floor(hold);\n      }\n      if (block) {\n        const extra = random(`${key}-block-${index}`) * BLOCK_SPREAD;\n        at += BLOCK_MIN + Math.floor(extra);\n      }\n    }\n    chunks.push({\n      index,\n      at,\n      start: startOf(token),\n      end: ends[stop - 1],\n      tokens: stop - token,\n      block,\n    });\n    block = cut;\n    token = stop;\n  }\n  return chunks;\n}\n\nfunction lastArrival(chunks: readonly AgentRunChunk[]) {\n  return chunks.length > 0 ? chunks[chunks.length - 1].at : 0;\n}\n\nfunction chunkIndexAt(chunks: readonly AgentRunChunk[], offset: number) {\n  let low = 0;\n  let high = chunks.length - 1;\n  let found = chunks.length;\n  while (low <= high) {\n    const middle = (low + high) >> 1;\n    if (chunks[middle].end > offset) {\n      found = middle;\n      high = middle - 1;\n    } else {\n      low = middle + 1;\n    }\n  }\n  return found;\n}\n\nexport function getAgentRunArrival(\n  chunks: readonly AgentRunChunk[],\n  offset: number,\n) {\n  const index = chunkIndexAt(chunks, offset);\n  return index < chunks.length ? chunks[index].at : Number.POSITIVE_INFINITY;\n}\n\nexport function getAgentRunReveal(\n  chunks: readonly AgentRunChunk[],\n  local: number,\n) {\n  let low = 0;\n  let high = chunks.length - 1;\n  let revealed = 0;\n  while (low <= high) {\n    const middle = (low + high) >> 1;\n    if (chunks[middle].at <= local) {\n      revealed = chunks[middle].end;\n      low = middle + 1;\n    } else {\n      high = middle - 1;\n    }\n  }\n  return revealed;\n}\n\nexport type AgentRunInlineKind = \"text\" | \"bold\" | \"code\" | \"cite\";\n\nexport interface AgentRunInline {\n  kind: AgentRunInlineKind;\n  text: string;\n  raw: number;\n  end: number;\n}\n\nexport interface AgentRunCodeLine {\n  text: string;\n  raw: number;\n}\n\nexport type AgentRunBlock =\n  | {\n      kind: \"heading\";\n      level: 1 | 2;\n      raw: number;\n      body: number;\n      end: number;\n      inline: AgentRunInline[];\n    }\n  | { kind: \"paragraph\"; raw: number; end: number; inline: AgentRunInline[] }\n  | {\n      kind: \"item\";\n      raw: number;\n      body: number;\n      end: number;\n      inline: AgentRunInline[];\n    }\n  | {\n      kind: \"code\";\n      raw: number;\n      open: number;\n      end: number;\n      lang: string;\n      langRaw: number;\n      lines: AgentRunCodeLine[];\n    };\n\nfunction parseInline(source: string, base: number): AgentRunInline[] {\n  const out: AgentRunInline[] = [];\n  let text = \"\";\n  let textStart = 0;\n  const flush = (at: number) => {\n    if (text.length > 0) {\n      out.push({ kind: \"text\", text, raw: base + textStart, end: base + at });\n    }\n    text = \"\";\n  };\n  let index = 0;\n  while (index < source.length) {\n    if (source.startsWith(\"**\", index)) {\n      const close = source.indexOf(\"**\", index + 2);\n      if (close > index + 2) {\n        flush(index);\n        out.push({\n          kind: \"bold\",\n          text: source.slice(index + 2, close),\n          raw: base + index + 2,\n          end: base + close + 2,\n        });\n        index = close + 2;\n        textStart = index;\n        continue;\n      }\n    }\n    if (source[index] === \"`\") {\n      const close = source.indexOf(\"`\", index + 1);\n      if (close > index + 1) {\n        flush(index);\n        out.push({\n          kind: \"code\",\n          text: source.slice(index + 1, close),\n          raw: base + index + 1,\n          end: base + close + 1,\n        });\n        index = close + 1;\n        textStart = index;\n        continue;\n      }\n    }\n    if (source[index] === \"[\") {\n      const match = /^\\[(\\d{1,3})\\]/.exec(source.slice(index));\n      if (match) {\n        flush(index);\n        out.push({\n          kind: \"cite\",\n          text: match[1],\n          raw: base + index,\n          end: base + index + match[0].length,\n        });\n        index += match[0].length;\n        textStart = index;\n        continue;\n      }\n    }\n    if (text.length === 0) textStart = index;\n    text += source[index];\n    index += 1;\n  }\n  flush(index);\n  return out;\n}\n\nconst HEADING_LINE = /^(#{1,3})[ \\t]+(.*)$/;\nconst ITEM_LINE = /^[-*][ \\t]+(.*)$/;\n\nfunction isStructural(line: string) {\n  return (\n    line.startsWith(\"```\") || HEADING_LINE.test(line) || ITEM_LINE.test(line)\n  );\n}\n\nexport function parseAgentRunMarkdown(markdown: string): AgentRunBlock[] {\n  const lines: AgentRunCodeLine[] = [];\n  let offset = 0;\n  for (const text of markdown.split(\"\\n\")) {\n    lines.push({ text, raw: offset });\n    offset += text.length + 1;\n  }\n  const blocks: AgentRunBlock[] = [];\n  let index = 0;\n  while (index < lines.length) {\n    const line = lines[index];\n    if (line.text.trim() === \"\") {\n      index += 1;\n      continue;\n    }\n    if (line.text.startsWith(\"```\")) {\n      const rest = line.text.slice(3);\n      const body: AgentRunCodeLine[] = [];\n      let cursor = index + 1;\n      while (cursor < lines.length && !lines[cursor].text.startsWith(\"```\")) {\n        body.push(lines[cursor]);\n        cursor += 1;\n      }\n      const closed = cursor < lines.length;\n      const last = closed ? lines[cursor] : (body[body.length - 1] ?? line);\n      blocks.push({\n        kind: \"code\",\n        raw: line.raw,\n        open: line.raw + 3,\n        end: last.raw + last.text.length,\n        lang: rest.trim(),\n        langRaw: line.raw + 3 + (rest.length - rest.trimStart().length),\n        lines: body,\n      });\n      index = closed ? cursor + 1 : cursor;\n      continue;\n    }\n    const heading = HEADING_LINE.exec(line.text);\n    if (heading) {\n      const body = line.raw + line.text.length - heading[2].length;\n      blocks.push({\n        kind: \"heading\",\n        level: heading[1].length === 1 ? 1 : 2,\n        raw: line.raw,\n        body,\n        end: line.raw + line.text.length,\n        inline: parseInline(heading[2], body),\n      });\n      index += 1;\n      continue;\n    }\n    const item = ITEM_LINE.exec(line.text);\n    if (item) {\n      const body = line.raw + line.text.length - item[1].length;\n      blocks.push({\n        kind: \"item\",\n        raw: line.raw,\n        body,\n        end: line.raw + line.text.length,\n        inline: parseInline(item[1], body),\n      });\n      index += 1;\n      continue;\n    }\n    let cursor = index;\n    while (\n      cursor < lines.length &&\n      lines[cursor].text.trim() !== \"\" &&\n      (cursor === index || !isStructural(lines[cursor].text))\n    ) {\n      cursor += 1;\n    }\n    const last = lines[cursor - 1];\n    const end = last.raw + last.text.length;\n    const source = markdown.slice(line.raw, end).replace(/\\n/g, \" \");\n    blocks.push({\n      kind: \"paragraph\",\n      raw: line.raw,\n      end,\n      inline: parseInline(source, line.raw),\n    });\n    index = cursor;\n  }\n  return blocks;\n}\n\nconst NARROW = \" .,:;'!|iljI`\";\nconst THIN = \"ftr()[]{}/\\\\-\";\nconst SYMBOL = \"<>=+~#$&→\";\nconst WIDE = \"mw…\";\nconst WIDEST = \"MW@%—\";\n\nfunction charWidth(char: string) {\n  if (NARROW.includes(char)) return 0.3;\n  if (THIN.includes(char)) return 0.44;\n  if (WIDEST.includes(char)) return 1;\n  if (WIDE.includes(char)) return 0.9;\n  if (SYMBOL.includes(char)) return 0.68;\n  if (char >= \"0\" && char <= \"9\") return 0.66;\n  if (char >= \"A\" && char <= \"Z\") return 0.74;\n  return 0.62;\n}\n\nexport function estimateAgentRunWidth(\n  text: string,\n  size: number,\n  mono = false,\n) {\n  let em = 0;\n  for (const char of text) em += mono ? 0.6 : charWidth(char);\n  return em * size;\n}\n\nexport type AgentRunPieceKind =\n  | AgentRunInlineKind\n  | \"punct\"\n  | \"string\"\n  | \"comment\";\n\nexport interface AgentRunPiece {\n  kind: AgentRunPieceKind;\n  text: string;\n  raw: number;\n  end: number;\n}\n\nexport interface AgentRunTextLine {\n  y: number;\n  height: number;\n  raw: number;\n  pieces: AgentRunPiece[];\n}\n\nfunction pieceWidth(piece: AgentRunPiece, size: number) {\n  if (piece.kind === \"cite\") {\n    return 20 + 7 * Math.max(0, piece.text.length - 1);\n  }\n  if (piece.kind === \"code\") {\n    return estimateAgentRunWidth(piece.text, size * 0.875, true) + 10;\n  }\n  return estimateAgentRunWidth(piece.text, size);\n}\n\ninterface Atom {\n  pieces: AgentRunPiece[];\n  width: number;\n  space: boolean;\n}\n\nfunction atomsOf(inline: readonly AgentRunInline[], size: number): Atom[] {\n  const atoms: Atom[] = [];\n  let word: AgentRunPiece[] = [];\n  let wordWidth = 0;\n  const add = (piece: AgentRunPiece) => {\n    word.push(piece);\n    wordWidth += pieceWidth(piece, size);\n  };\n  const close = () => {\n    if (word.length > 0) {\n      atoms.push({ pieces: word, width: wordWidth, space: false });\n    }\n    word = [];\n    wordWidth = 0;\n  };\n  for (const segment of inline) {\n    if (segment.kind === \"cite\" || segment.kind === \"code\") {\n      add({ ...segment });\n      continue;\n    }\n    let index = 0;\n    while (index < segment.text.length) {\n      const space = segment.text[index] === \" \";\n      let stop = index;\n      while (\n        stop < segment.text.length &&\n        (segment.text[stop] === \" \") === space\n      ) {\n        stop += 1;\n      }\n      const piece: AgentRunPiece = {\n        kind: segment.kind,\n        text: segment.text.slice(index, stop),\n        raw: segment.raw + index,\n        end: segment.raw + stop,\n      };\n      if (space) {\n        close();\n        atoms.push({\n          pieces: [piece],\n          width: pieceWidth(piece, size),\n          space: true,\n        });\n      } else {\n        add(piece);\n      }\n      index = stop;\n    }\n  }\n  close();\n  return atoms;\n}\n\nfunction mergePieces(pieces: readonly AgentRunPiece[]) {\n  const merged: AgentRunPiece[] = [];\n  for (const piece of pieces) {\n    const last = merged.at(-1);\n    const text = piece.kind === \"text\" || piece.kind === \"bold\";\n    if (text && last?.kind === piece.kind && last.end === piece.raw) {\n      merged[merged.length - 1] = {\n        ...last,\n        text: last.text + piece.text,\n        end: piece.end,\n      };\n    } else {\n      merged.push({ ...piece });\n    }\n  }\n  return merged;\n}\n\nexport function wrapAgentRunInline(\n  inline: readonly AgentRunInline[],\n  size: number,\n  maxWidth: number,\n): AgentRunPiece[][] {\n  const lines: AgentRunPiece[][] = [];\n  let line: AgentRunPiece[] = [];\n  let width = 0;\n  let pending: Atom | undefined;\n  for (const atom of atomsOf(inline, size)) {\n    if (atom.space) {\n      if (line.length > 0) pending = atom;\n      continue;\n    }\n    const gap = pending ? pending.width : 0;\n    if (line.length > 0 && width + gap + atom.width > maxWidth) {\n      lines.push(line);\n      line = [...atom.pieces];\n      width = atom.width;\n    } else {\n      if (pending && line.length > 0) {\n        line.push(...pending.pieces);\n        width += gap;\n      }\n      line.push(...atom.pieces);\n      width += atom.width;\n    }\n    pending = undefined;\n  }\n  if (line.length > 0) lines.push(line);\n  return lines.map(mergePieces);\n}\n\nfunction plainInline(text: string, raw: number): AgentRunInline[] {\n  return text.length > 0\n    ? [{ kind: \"text\", text, raw, end: raw + text.length }]\n    : [];\n}\n\nfunction textLines(\n  pieces: AgentRunPiece[][],\n  lineHeight: number,\n  raw: number,\n): AgentRunTextLine[] {\n  if (pieces.length === 0) {\n    return [{ y: 0, height: lineHeight, raw, pieces: [] }];\n  }\n  return pieces.map((line, index) => ({\n    y: index * lineHeight,\n    height: lineHeight,\n    raw: line[0]?.raw ?? raw,\n    pieces: line,\n  }));\n}\n\nconst PUNCT = \"<>{}()[]/=;,.:+-*&|!?\";\n\nexport function getAgentRunCodePieces(line: AgentRunCodeLine): AgentRunPiece[] {\n  const pieces: AgentRunPiece[] = [];\n  const push = (kind: AgentRunPieceKind, from: number, to: number) => {\n    if (to <= from) return;\n    const last = pieces.at(-1);\n    if (last?.kind === kind && last.end === line.raw + from) {\n      pieces[pieces.length - 1] = {\n        ...last,\n        text: last.text + line.text.slice(from, to),\n        end: line.raw + to,\n      };\n      return;\n    }\n    pieces.push({\n      kind,\n      text: line.text.slice(from, to),\n      raw: line.raw + from,\n      end: line.raw + to,\n    });\n  };\n  let index = 0;\n  while (index < line.text.length) {\n    const char = line.text[index];\n    if (line.text.startsWith(\"//\", index)) {\n      push(\"comment\", index, line.text.length);\n      break;\n    }\n    if (char === '\"' || char === \"'\" || char === \"`\") {\n      const close = line.text.indexOf(char, index + 1);\n      const stop = close === -1 ? line.text.length : close + 1;\n      push(\"string\", index, stop);\n      index = stop;\n      continue;\n    }\n    if (PUNCT.includes(char)) {\n      push(\"punct\", index, index + 1);\n      index += 1;\n      continue;\n    }\n    push(\"text\", index, index + 1);\n    index += 1;\n  }\n  return pieces;\n}\n\nexport function getAgentRunFrame(width?: number, height?: number) {\n  const frameWidth = Math.max(1, finite(width, 1280));\n  const frameHeight = Math.max(1, finite(height, 720));\n  const scale = Math.min(\n    frameHeight / REF_HEIGHT,\n    frameWidth / (PANEL_W + 2 * MARGIN_X),\n  );\n  const panelHeight = clamp(\n    frameHeight / scale - 2 * MARGIN_Y,\n    MIN_PANEL_H,\n    MAX_PANEL_H,\n  );\n  return {\n    width: frameWidth,\n    height: frameHeight,\n    unit: round(scale),\n    panelWidth: PANEL_W,\n    panelHeight: round(panelHeight),\n    viewport: round(panelHeight - HEADER_H - 2),\n  };\n}\n\nexport type AgentRunFrame = ReturnType<typeof getAgentRunFrame>;\n\nexport interface AgentRunSegment {\n  kind: \"think\" | \"gap\" | \"tool\" | \"answer\";\n  from: number;\n  to: number;\n  realFrom: number;\n  real: number;\n}\n\nexport interface AgentRunToolSpan {\n  index: number;\n  start: number;\n  end: number;\n  span: number;\n  duration: number;\n  realStart: number;\n}\n\nfunction planTextOf(run: AgentRunScript) {\n  return (run.plan ?? []).join(\"\\n\");\n}\n\nexport function getAgentRunTimeline(\n  run: AgentRunScript = defaultAgentRun,\n  options: AgentRunOptions = {},\n) {\n  const seed = seedOf(options.seed);\n  const compression = compressionOf(options.compression);\n  const perFrame = compression / 30;\n  const planText = planTextOf(run);\n  const planChunks =\n    planText.length > 0 ? getAgentRunChunks(planText, seed, \"plan\") : [];\n  const planStart = RUN_START + THINK_LEAD;\n  const planEnd = planStart + lastArrival(planChunks);\n  const thinkEnd = planEnd + THINK_TAIL;\n  const segments: AgentRunSegment[] = [];\n  let real = 0;\n  const push = (\n    kind: AgentRunSegment[\"kind\"],\n    from: number,\n    to: number,\n    seconds: number,\n  ) => {\n    segments.push({ kind, from, to, realFrom: real, real: seconds });\n    real += seconds;\n  };\n  push(\"think\", RUN_START, thinkEnd, (thinkEnd - RUN_START) * perFrame);\n  const thinkSeconds = real;\n  const tools: AgentRunToolSpan[] = [];\n  let cursor = thinkEnd;\n  for (const [index, step] of (run.steps ?? []).entries()) {\n    push(\"gap\", cursor, cursor + TOOL_GAP, TOOL_GAP * perFrame);\n    cursor += TOOL_GAP;\n    const duration = Math.max(0, finite(step.duration, 0));\n    const span = clamp(Math.round(duration / perFrame), TOOL_MIN, TOOL_MAX);\n    const realStart = real;\n    push(\"tool\", cursor, cursor + span, duration);\n    tools.push({\n      index,\n      start: cursor,\n      end: cursor + span,\n      span,\n      duration,\n      realStart,\n    });\n    cursor += span;\n  }\n  push(\"gap\", cursor, cursor + ANSWER_GAP, ANSWER_GAP * perFrame);\n  const answerStart = cursor + ANSWER_GAP;\n  const answerChunks = getAgentRunChunks(run.answer, seed, \"answer\");\n  const answerEnd = answerStart + lastArrival(answerChunks);\n  push(\"answer\", answerStart, answerEnd, (answerEnd - answerStart) * perFrame);\n  return {\n    compression,\n    runStart: RUN_START,\n    planStart,\n    planEnd,\n    thinkEnd,\n    thinkSeconds,\n    tools,\n    answerStart,\n    answerEnd,\n    runEnd: answerEnd,\n    settled: answerEnd + SETTLE,\n    realTotal: real,\n    segments,\n    planChunks,\n    answerChunks,\n  };\n}\n\nexport type AgentRunTimeline = ReturnType<typeof getAgentRunTimeline>;\n\nexport const agentRunLength = getAgentRunTimeline().settled;\n\nexport function getAgentRunDuration(\n  run: AgentRunScript = defaultAgentRun,\n  options: AgentRunOptions & Pick<AgentRunPlayback, \"speed\"> = {},\n) {\n  const rate = rateOf(options.speed);\n  if (rate === 0) return HOLD;\n  const { settled } = getAgentRunTimeline(run, options);\n  return Math.ceil(round(settled / rate)) + HOLD;\n}\n\nexport function getAgentRunTime(\n  frame: number,\n  playback: AgentRunPlayback = {},\n) {\n  const fps = Math.max(1, finite(playback.fps, 30));\n  return Math.max(0, finite(frame, 0)) * (30 / fps) * rateOf(playback.speed);\n}\n\nexport function getAgentRunElapsed(time: number, timeline: AgentRunTimeline) {\n  if (!(time > timeline.runStart)) return 0;\n  for (const segment of timeline.segments) {\n    if (segment.to > segment.from && time < segment.to) {\n      const share = (time - segment.from) / (segment.to - segment.from);\n      return segment.realFrom + share * segment.real;\n    }\n  }\n  return timeline.realTotal;\n}\n\nexport interface AgentRunChipLayout {\n  label: string;\n  icon: \"globe\" | \"file\";\n}\n\nexport interface AgentRunRowLayout {\n  index: number;\n  y: number;\n  height: number;\n  divider: boolean;\n  icon: AgentRunIcon;\n  name: string;\n  detail: string;\n  duration: string;\n  chips: AgentRunChipLayout[];\n  more: number;\n}\n\nexport interface AgentRunPlanItemLayout {\n  index: number;\n  y: number;\n  raw: number;\n  lines: AgentRunTextLine[];\n}\n\nexport interface AgentRunPlanSection {\n  y: number;\n  height: number;\n  items: AgentRunPlanItemLayout[];\n}\n\nexport interface AgentRunToolsSection {\n  y: number;\n  height: number;\n  rows: AgentRunRowLayout[];\n}\n\nexport type AgentRunBlockLayout =\n  | {\n      kind: \"heading\";\n      index: number;\n      y: number;\n      height: number;\n      level: 1 | 2;\n      raw: number;\n      end: number;\n      mark: string;\n      markWidth: number;\n      lines: AgentRunTextLine[];\n    }\n  | {\n      kind: \"paragraph\" | \"item\";\n      index: number;\n      y: number;\n      height: number;\n      raw: number;\n      lines: AgentRunTextLine[];\n    }\n  | {\n      kind: \"code\";\n      index: number;\n      y: number;\n      height: number;\n      raw: number;\n      open: number;\n      lang: string;\n      langRaw: number;\n      lines: AgentRunTextLine[];\n    };\n\nconst DOMAIN = /^[a-z0-9-]+(\\.[a-z0-9-]+)+(\\/\\S*)?$/i;\n\nfunction chipWidth(label: string) {\n  return 7 + 12 + 5 + estimateAgentRunWidth(label, CHIP_SIZE) + 9 + 2;\n}\n\nfunction moreWidth(count: number) {\n  return 9 + estimateAgentRunWidth(`+${count}`, CHIP_SIZE) + 9 + 2;\n}\n\nexport function fitAgentRunChips(sources: readonly string[]) {\n  const chips: AgentRunChipLayout[] = [];\n  let used = 0;\n  for (const [index, label] of sources.entries()) {\n    if (chips.length >= CHIP_MAX) break;\n    const width = chipWidth(label) + (chips.length > 0 ? CHIP_GAP : 0);\n    const rest = sources.length - index - 1;\n    const reserve = rest > 0 ? CHIP_GAP + moreWidth(rest) : 0;\n    if (used + width + reserve > CHIP_ROOM) break;\n    chips.push({ label, icon: DOMAIN.test(label) ? \"globe\" : \"file\" });\n    used += width;\n  }\n  return { chips, more: sources.length - chips.length };\n}\n\nconst lineText = (line: readonly AgentRunPiece[]) =>\n  line.map((piece) => piece.text).join(\"\");\n\nfunction blockGap(\n  previous: AgentRunBlock[\"kind\"] | undefined,\n  next: AgentRunBlock[\"kind\"],\n) {\n  if (previous === undefined) return 0;\n  if (previous === \"item\" && next === \"item\") return ITEM_GAP;\n  if (previous === \"code\" || next === \"code\") return CODE_GAP;\n  if (previous === \"heading\") return HEADING_GAP;\n  return BLOCK_GAP;\n}\n\nexport function getAgentRunLayout(run: AgentRunScript = defaultAgentRun) {\n  let y = PAD_TOP;\n  const prompt = plainInline(run.prompt, 0);\n  const wrappedPrompt = wrapAgentRunInline(prompt, BUBBLE_SIZE, BUBBLE_WIDTH);\n  const promptLines = wrappedPrompt.map(lineText);\n  const bubble = {\n    y,\n    height: Math.max(1, promptLines.length) * BUBBLE_LINE + 2 * BUBBLE_PAD_Y,\n    lines: promptLines,\n  };\n  y += bubble.height + BUBBLE_GAP;\n  const status = { y, height: STATUS_H };\n  y += STATUS_H;\n  let plan: AgentRunPlanSection | null = null;\n  const planLines = run.plan ?? [];\n  if (planLines.length > 0) {\n    y += STATUS_GAP;\n    const items: AgentRunPlanItemLayout[] = [];\n    let cursor = 0;\n    let raw = 0;\n    for (const [index, text] of planLines.entries()) {\n      if (index > 0) cursor += PLAN_GAP;\n      const inline = plainInline(text, raw);\n      const wrapped = wrapAgentRunInline(inline, PLAN_SIZE, PLAN_WIDTH);\n      const lines = textLines(wrapped, PLAN_LINE, raw);\n      items.push({ index, y: cursor, raw, lines });\n      cursor += lines.length * PLAN_LINE;\n      raw += text.length + 1;\n    }\n    plan = { y, height: cursor, items };\n    y += cursor;\n  }\n  let tools: AgentRunToolsSection | null = null;\n  const steps = run.steps ?? [];\n  if (steps.length > 0) {\n    y += PLAN_AFTER;\n    const rows: AgentRunRowLayout[] = [];\n    let cursor = 0;\n    for (const [index, step] of steps.entries()) {\n      const sources = step.sources ?? [];\n      const fit = fitAgentRunChips(sources);\n      const height =\n        ROW_H + (index > 0 ? 1 : 0) + (sources.length > 0 ? CHIPS_H : 0);\n      rows.push({\n        index,\n        y: cursor,\n        height,\n        divider: index > 0,\n        icon: step.icon ?? (sources.length > 0 ? \"book\" : \"terminal\"),\n        name: step.name,\n        detail: step.detail ?? \"\",\n        duration: formatAgentRunSeconds(finite(step.duration, 0)),\n        chips: fit.chips,\n        more: fit.more,\n      });\n      cursor += height;\n    }\n    tools = { y, height: cursor + 2, rows };\n    y += cursor + 2;\n  }\n  const parsed = parseAgentRunMarkdown(run.answer);\n  const blocks: AgentRunBlockLayout[] = [];\n  if (parsed.length > 0) y += tools ? TOOLS_AFTER : PLAN_AFTER;\n  let previous: AgentRunBlock[\"kind\"] | undefined;\n  for (const [index, block] of parsed.entries()) {\n    y += blockGap(previous, block.kind);\n    previous = block.kind;\n    if (block.kind === \"heading\") {\n      const spec = HEADINGS[block.level];\n      const wrapped = wrapAgentRunInline(block.inline, spec.size, MEASURE);\n      const lines = textLines(wrapped, spec.line, block.body);\n      const mark = run.answer.slice(block.raw, block.body);\n      blocks.push({\n        kind: \"heading\",\n        index,\n        y,\n        height: lines.length * spec.line,\n        level: block.level,\n        raw: block.raw,\n        end: block.end,\n        mark,\n        markWidth: round(estimateAgentRunWidth(mark, BODY_SIZE)),\n        lines,\n      });\n      y += lines.length * spec.line;\n      continue;\n    }\n    if (block.kind === \"code\") {\n      const lines = block.lines.map((line, row) => ({\n        y: row * CODE_LINE,\n        height: CODE_LINE,\n        raw: line.raw,\n        pieces: getAgentRunCodePieces(line),\n      }));\n      const height = CODE_HEAD + 2 * CODE_PAD + lines.length * CODE_LINE;\n      blocks.push({\n        kind: \"code\",\n        index,\n        y,\n        height,\n        raw: block.raw,\n        open: block.open,\n        lang: block.lang,\n        langRaw: block.langRaw,\n        lines,\n      });\n      y += height;\n      continue;\n    }\n    const width = block.kind === \"item\" ? MEASURE - ITEM_INDENT : MEASURE;\n    const body = block.kind === \"item\" ? block.body : block.raw;\n    const wrapped = wrapAgentRunInline(block.inline, BODY_SIZE, width);\n    const lines = textLines(wrapped, BODY_LINE, body);\n    blocks.push({\n      kind: block.kind,\n      index,\n      y,\n      height: lines.length * BODY_LINE,\n      raw: block.raw,\n      lines,\n    });\n    y += lines.length * BODY_LINE;\n  }\n  return { bubble, status, plan, tools, blocks, height: y };\n}\n\nexport type AgentRunLayout = ReturnType<typeof getAgentRunLayout>;\n\nexport interface AgentRunScrollStep {\n  at: number;\n  delta: number;\n}\n\nexport function getAgentRunGrowth(\n  layout: AgentRunLayout,\n  timeline: AgentRunTimeline,\n) {\n  const events: { at: number; bottom: number }[] = [\n    { at: BUBBLE_AT, bottom: layout.bubble.y + layout.bubble.height },\n    { at: RUN_START, bottom: layout.status.y + layout.status.height },\n  ];\n  const { plan, tools } = layout;\n  if (plan) {\n    for (const item of plan.items) {\n      for (const line of item.lines) {\n        const at = getAgentRunArrival(timeline.planChunks, line.raw);\n        events.push({\n          at: timeline.planStart + at,\n          bottom: plan.y + item.y + line.y + line.height,\n        });\n      }\n    }\n  }\n  if (tools) {\n    for (const row of tools.rows) {\n      const span = timeline.tools[row.index];\n      events.push({ at: span.start, bottom: tools.y + row.y + row.height + 2 });\n    }\n  }\n  const arrival = (offset: number) =>\n    timeline.answerStart + getAgentRunArrival(timeline.answerChunks, offset);\n  for (const block of layout.blocks) {\n    if (block.kind === \"code\") {\n      events.push({\n        at: arrival(block.open - 1),\n        bottom: block.y + CODE_HEAD + 2 * CODE_PAD,\n      });\n      for (const line of block.lines) {\n        events.push({\n          at: arrival(line.raw),\n          bottom: block.y + CODE_HEAD + 2 * CODE_PAD + line.y + line.height,\n        });\n      }\n      continue;\n    }\n    for (const [row, line] of block.lines.entries()) {\n      const start = row === 0 ? block.raw : line.raw;\n      events.push({\n        at: arrival(start),\n        bottom: block.y + line.y + line.height,\n      });\n    }\n  }\n  return events\n    .filter((event) => Number.isFinite(event.at))\n    .sort((a, b) => a.at - b.at);\n}\n\nexport function getAgentRunScrollSteps(\n  layout: AgentRunLayout,\n  timeline: AgentRunTimeline,\n  viewport: number,\n): AgentRunScrollStep[] {\n  const steps: AgentRunScrollStep[] = [];\n  let target = 0;\n  for (const event of getAgentRunGrowth(layout, timeline)) {\n    const next = Math.max(target, event.bottom + SAFE - viewport);\n    if (next > target) {\n      steps.push({ at: event.at, delta: round(next - target) });\n    }\n    target = next;\n  }\n  return steps;\n}\n\nexport function getAgentRunScroll(\n  time: number,\n  steps: readonly AgentRunScrollStep[],\n) {\n  let total = 0;\n  for (const step of steps) {\n    const since = time - step.at + SCROLL_LEAD;\n    if (since <= 0) break;\n    total += step.delta * smoothstep(since / SCROLL);\n  }\n  return total;\n}\n\nexport function getAgentRunModel(\n  run: AgentRunScript = defaultAgentRun,\n  options: AgentRunOptions = {},\n) {\n  const stage = getAgentRunFrame(options.width, options.height);\n  const timeline = getAgentRunTimeline(run, options);\n  const layout = getAgentRunLayout(run);\n  const scroll = getAgentRunScrollSteps(layout, timeline, stage.viewport);\n  return { run, stage, timeline, layout, scroll };\n}\n\nexport type AgentRunModel = ReturnType<typeof getAgentRunModel>;\n\nexport interface AgentRunSpinnerState {\n  angle: number;\n  arc: number;\n  done: number;\n  check: number;\n  resolved: boolean;\n}\n\nexport function getAgentRunSpinner(\n  time: number,\n  start: number,\n  end: number,\n): AgentRunSpinnerState {\n  const spinning = clamp(time - start, 0, Math.max(0, end - start));\n  const since = time - end;\n  const settle = progress(since / RESOLVE);\n  const coast = SPIN * RESOLVE * (settle - (settle * settle) / 2);\n  return {\n    angle: round(spinning * SPIN + coast - 90),\n    arc: round(ARC + (1 - ARC) * expo(since / RESOLVE)),\n    done: round(smoothstep(since / RESOLVE)),\n    check: round(easeOut((since - CHECK_FROM) / (CHECK_TO - CHECK_FROM))),\n    resolved: since >= 0,\n  };\n}\n\nexport function getAgentRunShimmer(time: number, length: number) {\n  const cycle = (time - RUN_START) / SHIMMER_PERIOD;\n  const phase = cycle - Math.floor(cycle);\n  const center = -SHIMMER_BAND + phase * (length + 2 * SHIMMER_BAND);\n  return Array.from({ length }, (_, index) => {\n    const distance = Math.abs(index + 0.5 - center) / SHIMMER_BAND;\n    const band = distance < 1 ? (1 - distance * distance) ** 2 : 0;\n    return round(SHIMMER_BASE + (1 - SHIMMER_BASE) * band);\n  });\n}\n\nexport interface AgentRunFragment {\n  text: string;\n  opacity: number;\n}\n\nexport interface AgentRunSpan {\n  kind: AgentRunPieceKind;\n  fragments: AgentRunFragment[];\n  opacity: number;\n  scale: number;\n}\n\nexport interface AgentRunLineState {\n  key: string;\n  y: number;\n  height: number;\n  spans: AgentRunSpan[];\n}\n\nconst fadeOf = (since: number) => round(easeOut((since + 1) / FADE));\n\nfunction spansOf(\n  pieces: readonly AgentRunPiece[],\n  chunks: readonly AgentRunChunk[],\n  local: number,\n  revealed: number,\n): AgentRunSpan[] {\n  const spans: AgentRunSpan[] = [];\n  for (const piece of pieces) {\n    if (piece.kind === \"cite\") {\n      if (revealed < piece.end) continue;\n      const since = local - getAgentRunArrival(chunks, piece.end - 1);\n      spans.push({\n        kind: \"cite\",\n        fragments: [{ text: piece.text, opacity: 1 }],\n        opacity: round(easeOut((since + 1) / 4)),\n        scale: round(0.6 + 0.4 * backOut((since + 1) / POP, CITE_BACK)),\n      });\n      continue;\n    }\n    const visible = clamp(revealed - piece.raw, 0, piece.text.length);\n    if (visible === 0) continue;\n    const fragments: AgentRunFragment[] = [];\n    let offset = 0;\n    while (offset < visible) {\n      const index = chunkIndexAt(chunks, piece.raw + offset);\n      const chunk = chunks[index];\n      const stop = chunk ? Math.min(visible, chunk.end - piece.raw) : visible;\n      const opacity = chunk ? fadeOf(local - chunk.at) : 1;\n      const text = piece.text.slice(offset, stop);\n      const last = fragments.at(-1);\n      if (last?.opacity === 1 && opacity === 1) {\n        fragments[fragments.length - 1] = { text: last.text + text, opacity };\n      } else {\n        fragments.push({ text, opacity });\n      }\n      offset = Math.max(stop, offset + 1);\n    }\n    spans.push({ kind: piece.kind, fragments, opacity: 1, scale: 1 });\n  }\n  return spans;\n}\n\nexport type AgentRunBlockState =\n  | {\n      kind: \"heading\";\n      key: string;\n      y: number;\n      level: 1 | 2;\n      styled: boolean;\n      morph: number;\n      mark: string;\n      markWidth: number;\n      lines: AgentRunLineState[];\n    }\n  | { kind: \"paragraph\"; key: string; y: number; lines: AgentRunLineState[] }\n  | {\n      kind: \"item\";\n      key: string;\n      y: number;\n      dot: number;\n      dotOpacity: number;\n      lines: AgentRunLineState[];\n    }\n  | {\n      kind: \"code\";\n      key: string;\n      y: number;\n      height: number;\n      opacity: number;\n      lang: string;\n      lines: AgentRunLineState[];\n    };\n\nfunction blockState(\n  block: AgentRunBlockLayout,\n  text: string,\n  chunks: readonly AgentRunChunk[],\n  local: number,\n  revealed: number,\n): AgentRunBlockState | null {\n  const key = `block-${block.index}`;\n  const lineStates = (from: number) => {\n    const lines: AgentRunLineState[] = [];\n    for (const [row, line] of block.lines.entries()) {\n      if (revealed <= (row === 0 ? from : line.raw)) break;\n      lines.push({\n        key: `${key}-line-${row}`,\n        y: line.y,\n        height: line.height,\n        spans: spansOf(line.pieces, chunks, local, revealed),\n      });\n    }\n    return lines;\n  };\n  if (block.kind === \"code\") {\n    if (revealed < block.open) return null;\n    const since = local - getAgentRunArrival(chunks, block.open - 1);\n    let height = expo((since + 1) / CODE_OPEN) * (CODE_HEAD + 2 * CODE_PAD);\n    const lines: AgentRunLineState[] = [];\n    for (const [row, line] of block.lines.entries()) {\n      if (revealed <= line.raw) break;\n      const grown = local - getAgentRunArrival(chunks, line.raw);\n      height += expo((grown + 1) / LINE_GROW) * CODE_LINE;\n      lines.push({\n        key: `${key}-line-${row}`,\n        y: line.y,\n        height: line.height,\n        spans: spansOf(line.pieces, chunks, local, revealed),\n      });\n    }\n    const langShown = clamp(revealed - block.langRaw, 0, block.lang.length);\n    return {\n      kind: \"code\",\n      key,\n      y: block.y,\n      height: round(height),\n      opacity: round(easeOut((since + 1) / 4)),\n      lang: block.lang.slice(0, langShown),\n      lines,\n    };\n  }\n  if (revealed <= block.raw) return null;\n  if (block.kind === \"heading\") {\n    const complete =\n      revealed > block.end ||\n      (block.end >= text.length && revealed >= block.end);\n    const completeAt = getAgentRunArrival(\n      chunks,\n      Math.min(block.end, text.length - 1),\n    );\n    const morph = complete ? round(expo((local - completeAt) / MORPH)) : 0;\n    const markShown = clamp(revealed - block.raw, 0, block.mark.length);\n    return {\n      kind: \"heading\",\n      key,\n      y: block.y,\n      level: block.level,\n      styled: complete,\n      morph,\n      mark: block.mark.slice(0, markShown),\n      markWidth: block.markWidth,\n      lines: lineStates(block.raw),\n    };\n  }\n  if (block.kind === \"item\") {\n    const since = local - getAgentRunArrival(chunks, block.raw);\n    return {\n      kind: \"item\",\n      key,\n      y: block.y,\n      dot: round(backOut((since + 1) / POP, DOT_BACK)),\n      dotOpacity: round(easeOut((since + 1) / 3)),\n      lines: lineStates(block.raw),\n    };\n  }\n  return { kind: \"paragraph\", key, y: block.y, lines: lineStates(block.raw) };\n}\n\nexport interface AgentRunPlanItemState {\n  key: string;\n  index: number;\n  y: number;\n  numberOpacity: number;\n  lines: AgentRunLineState[];\n}\n\nfunction planState(\n  plan: AgentRunPlanSection,\n  chunks: readonly AgentRunChunk[],\n  local: number,\n) {\n  const revealed = getAgentRunReveal(chunks, local);\n  const items: AgentRunPlanItemState[] = [];\n  let rule = 0;\n  for (const item of plan.items) {\n    if (revealed <= item.raw) break;\n    const lines: AgentRunLineState[] = [];\n    for (const [row, line] of item.lines.entries()) {\n      if (revealed <= (row === 0 ? item.raw : line.raw)) break;\n      const since = local - getAgentRunArrival(chunks, line.raw);\n      const grown = line.height * expo((since + 1) / LINE_GROW);\n      rule = Math.max(rule, item.y + line.y + grown);\n      lines.push({\n        key: `plan-${item.index}-line-${row}`,\n        y: line.y,\n        height: line.height,\n        spans: spansOf(line.pieces, chunks, local, revealed),\n      });\n    }\n    const numbered = local - getAgentRunArrival(chunks, item.raw);\n    items.push({\n      key: `plan-${item.index}`,\n      index: item.index,\n      y: item.y,\n      numberOpacity: fadeOf(numbered),\n      lines,\n    });\n  }\n  return { y: plan.y, rule: round(rule), items };\n}\n\nfunction chipPops(count: number, since: number, span: number) {\n  const spread = Math.max(0, CHIP_SPAN * span - CHIP_LEAD);\n  const step = count > 1 ? spread / (count - 1) : 0;\n  return Array.from({ length: count }, (_, index) => {\n    const age = since - CHIP_LEAD - index * step;\n    return {\n      visible: age >= 0,\n      opacity: round(easeOut((age + 1) / 4)),\n      scale: round(0.8 + 0.2 * backOut((age + 1) / POP, CHIP_BACK)),\n    };\n  });\n}\n\nfunction toolsState(\n  tools: AgentRunToolsSection,\n  timeline: AgentRunTimeline,\n  time: number,\n) {\n  let height = 2;\n  const rows = tools.rows\n    .filter((row) => time >= timeline.tools[row.index].start)\n    .map((row) => {\n      const span = timeline.tools[row.index];\n      const since = time - span.start;\n      const open = expo(since / ROW_IN);\n      height += row.height * open;\n      const pops = chipPops(\n        row.chips.length + (row.more > 0 ? 1 : 0),\n        since,\n        span.span,\n      );\n      const more = pops[row.chips.length];\n      return {\n        key: `tool-${row.index}`,\n        index: row.index,\n        y: row.y,\n        height: round(row.height * open),\n        divider: row.divider,\n        icon: row.icon,\n        name: row.name,\n        detail: row.detail,\n        duration: row.duration,\n        content: round(easeOut((since - 1) / ROW_FADE)),\n        spinner: getAgentRunSpinner(time, span.start, span.end),\n        label: round(easeOut((time - span.end - LABEL_DELAY) / LABEL_FADE)),\n        chips: row.chips.map((chip, index) => ({ ...chip, ...pops[index] })),\n        more: row.more > 0 && more ? { count: row.more, ...more } : null,\n        sources: row.chips.length + row.more > 0,\n      };\n    });\n  return { y: tools.y, height: round(height), rows };\n}\n\nexport function getAgentRunState(\n  frame: number,\n  model: AgentRunModel = getAgentRunModel(),\n  playback: AgentRunPlayback = {},\n) {\n  const time = getAgentRunTime(frame, playback);\n  const { run, stage, timeline, layout } = model;\n  const elapsed = getAgentRunElapsed(time, timeline);\n  const planLocal = time - timeline.planStart;\n  const answerLocal = time - timeline.answerStart;\n  const answerRevealed = getAgentRunReveal(timeline.answerChunks, answerLocal);\n  const running = time - RUN_START;\n  const swap =\n    time >= timeline.thinkEnd ? expo((time - timeline.thinkEnd) / SWAP) : 0;\n  const thinking = \"Thinking\";\n  const blocks: AgentRunBlockState[] = [];\n  for (const block of layout.blocks) {\n    const next = blockState(\n      block,\n      run.answer,\n      timeline.answerChunks,\n      answerLocal,\n      answerRevealed,\n    );\n    if (next) blocks.push(next);\n  }\n  return {\n    time,\n    elapsed,\n    scroll: round(getAgentRunScroll(time, model.scroll)),\n    panel: {\n      width: stage.panelWidth,\n      height: stage.panelHeight,\n      viewport: stage.viewport,\n      unit: stage.unit,\n      opacity: round(easeOut((time + 1) / PANEL_IN)),\n      lift: round(PANEL_RISE * (1 - expo(time / PANEL_IN))),\n    },\n    header: {\n      title: run.title ?? \"\",\n      label: formatAgentRunSeconds(elapsed),\n      opacity: round(easeOut((running + 1) / STATUS_IN)),\n      spinner: getAgentRunSpinner(time, RUN_START, timeline.runEnd),\n      done: time >= timeline.runEnd,\n    },\n    bubble: {\n      y: layout.bubble.y,\n      height: layout.bubble.height,\n      lines: layout.bubble.lines,\n      opacity: round(easeOut((time - BUBBLE_AT + 1) / BUBBLE_IN)),\n      lift: round(BUBBLE_RISE * (1 - expo((time - BUBBLE_AT) / BUBBLE_IN))),\n    },\n    status: {\n      y: layout.status.y,\n      visible: running >= 0,\n      opacity: round(easeOut((running + 1) / STATUS_IN)),\n      thinking,\n      shimmer: getAgentRunShimmer(time, thinking.length),\n      thought: `Thought for ${Math.max(1, Math.round(timeline.thinkSeconds))}s`,\n      swap: round(swap),\n      lift: round(SWAP_RISE * (1 - swap)),\n    },\n    plan: layout.plan\n      ? planState(layout.plan, timeline.planChunks, planLocal)\n      : null,\n    tools: layout.tools ? toolsState(layout.tools, timeline, time) : null,\n    blocks,\n    revealed: {\n      plan: getAgentRunReveal(timeline.planChunks, planLocal),\n      answer: answerRevealed,\n    },\n  };\n}\n\nexport type AgentRunState = ReturnType<typeof getAgentRunState>;\n\ninterface AgentRunSurface {\n  panel: string;\n  border: string;\n  divider: string;\n  fg: string;\n  muted: string;\n  subtle: string;\n  surface: string;\n  surfaceBorder: string;\n  code: string;\n  track: string;\n  shadow: string;\n}\n\nexport interface AgentRunPalette extends AgentRunSurface {\n  accent: string;\n  accentTint: string;\n  accentText: string;\n}\n\nexport const AGENT_RUN_THEMES: Record<AgentRunTheme, AgentRunSurface> = {\n  light: {\n    panel: \"#ffffff\",\n    border: \"#e4e4e0\",\n    divider: \"#ededea\",\n    fg: \"#1c1c1a\",\n    muted: \"#63635e\",\n    subtle: \"#9a9a94\",\n    surface: \"#f3f3f1\",\n    surfaceBorder: \"#e7e7e3\",\n    code: \"#f8f8f6\",\n    track: \"#e3e3df\",\n    shadow: \"0 1px 2px rgba(28, 28, 26, 0.06)\",\n  },\n  dark: {\n    panel: \"#161616\",\n    border: \"#2b2b2a\",\n    divider: \"#232322\",\n    fg: \"#ececea\",\n    muted: \"#a2a29d\",\n    subtle: \"#6d6d68\",\n    surface: \"#212120\",\n    surfaceBorder: \"#2e2e2d\",\n    code: \"#1c1c1b\",\n    track: \"#343433\",\n    shadow: \"0 1px 2px rgba(0, 0, 0, 0.5)\",\n  },\n};\n\nexport function getAgentRunPalette(\n  theme = agentRunDefaults.theme,\n  accentColor = agentRunDefaults.accentColor,\n): AgentRunPalette {\n  const dark = theme === \"dark\";\n  const tint = dark ? 22 : 12;\n  return {\n    ...AGENT_RUN_THEMES[dark ? \"dark\" : \"light\"],\n    accent: accentColor,\n    accentTint: `color-mix(in srgb, ${accentColor} ${tint}%, transparent)`,\n    accentText: dark\n      ? `color-mix(in srgb, ${accentColor} 62%, #ffffff)`\n      : accentColor,\n  };\n}\n\ntype IconNode =\n  | { tag: \"path\"; d: string }\n  | { tag: \"circle\"; cx: number; cy: number; r: number }\n  | { tag: \"ellipse\"; cx: number; cy: number; rx: number; ry: number }\n  | {\n      tag: \"rect\";\n      x: number;\n      y: number;\n      width: number;\n      height: number;\n      rx: number;\n    };\n\ntype IconName = AgentRunIcon | \"copy\";\n\nconst ICONS: Record<IconName, readonly IconNode[]> = {\n  chart: [\n    { tag: \"path\", d: \"M3 3v16a2 2 0 0 0 2 2h16\" },\n    { tag: \"path\", d: \"M18 17V9\" },\n    { tag: \"path\", d: \"M13 17V5\" },\n    { tag: \"path\", d: \"M8 17v-3\" },\n  ],\n  database: [\n    { tag: \"ellipse\", cx: 12, cy: 5, rx: 9, ry: 3 },\n    { tag: \"path\", d: \"M3 5V19A9 3 0 0 0 21 19V5\" },\n    { tag: \"path\", d: \"M3 12A9 3 0 0 0 21 12\" },\n  ],\n  search: [\n    { tag: \"path\", d: \"m21 21-4.34-4.34\" },\n    { tag: \"circle\", cx: 11, cy: 11, r: 8 },\n  ],\n  book: [\n    { tag: \"path\", d: \"M12 7v14\" },\n    {\n      tag: \"path\",\n      d: \"M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z\",\n    },\n  ],\n  globe: [\n    { tag: \"circle\", cx: 12, cy: 12, r: 10 },\n    { tag: \"path\", d: \"M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20\" },\n    { tag: \"path\", d: \"M2 12h20\" },\n  ],\n  terminal: [\n    { tag: \"path\", d: \"M12 19h8\" },\n    { tag: \"path\", d: \"m4 17 6-6-6-6\" },\n  ],\n  file: [\n    {\n      tag: \"path\",\n      d: \"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z\",\n    },\n    { tag: \"path\", d: \"M14 2v5a1 1 0 0 0 1 1h5\" },\n    { tag: \"path\", d: \"M10 9H8\" },\n    { tag: \"path\", d: \"M16 13H8\" },\n    { tag: \"path\", d: \"M16 17H8\" },\n  ],\n  copy: [\n    { tag: \"rect\", x: 8, y: 8, width: 14, height: 14, rx: 2 },\n    {\n      tag: \"path\",\n      d: \"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\",\n    },\n  ],\n};\n\nfunction Icon({\n  name,\n  size,\n  color,\n}: {\n  name: IconName;\n  size: number;\n  color: string;\n}) {\n  return (\n    <svg\n      width={size}\n      height={size}\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke={color}\n      strokeWidth={2}\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      style={{ display: \"block\", flexShrink: 0 }}\n    >\n      {ICONS[name].map((node, index) => {\n        if (node.tag === \"path\") return <path key={index} d={node.d} />;\n        if (node.tag === \"circle\") {\n          return <circle key={index} cx={node.cx} cy={node.cy} r={node.r} />;\n        }\n        if (node.tag === \"ellipse\") {\n          return (\n            <ellipse\n              key={index}\n              cx={node.cx}\n              cy={node.cy}\n              rx={node.rx}\n              ry={node.ry}\n            />\n          );\n        }\n        return (\n          <rect\n            key={index}\n            x={node.x}\n            y={node.y}\n            width={node.width}\n            height={node.height}\n            rx={node.rx}\n          />\n        );\n      })}\n    </svg>\n  );\n}\n\nfunction Spinner({\n  state,\n  size,\n  palette,\n}: {\n  state: AgentRunSpinnerState;\n  size: number;\n  palette: AgentRunPalette;\n}) {\n  const active = 1 - state.done;\n  return (\n    <svg\n      width={size}\n      height={size}\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      strokeWidth={2}\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      style={{ display: \"block\", flexShrink: 0 }}\n    >\n      <circle cx={12} cy={12} r={10} stroke={palette.track} opacity={active} />\n      <circle\n        cx={12}\n        cy={12}\n        r={10}\n        stroke={palette.accent}\n        opacity={active}\n        strokeDasharray={`${round(state.arc * CIRCLE)} ${round(CIRCLE)}`}\n        transform={`rotate(${state.angle} 12 12)`}\n      />\n      <circle\n        cx={12}\n        cy={12}\n        r={10}\n        stroke={palette.muted}\n        opacity={state.done}\n      />\n      <path\n        d=\"m9 12 2 2 4-4\"\n        stroke={palette.muted}\n        strokeDasharray={round(CHECK_LENGTH)}\n        strokeDashoffset={round(CHECK_LENGTH * (1 - state.check))}\n        opacity={state.check > 0 ? 1 : 0}\n      />\n    </svg>\n  );\n}\n\nfunction spanStyle(\n  span: AgentRunSpan,\n  palette: AgentRunPalette,\n): CSSProperties {\n  if (span.kind === \"bold\") return { fontWeight: 600 };\n  if (span.kind === \"code\") {\n    return {\n      fontFamily: MONO,\n      fontSize: \"0.875em\",\n      padding: \"1px 5px\",\n      borderRadius: 5,\n      background: palette.surface,\n      border: `1px solid ${palette.surfaceBorder}`,\n    };\n  }\n  if (span.kind === \"cite\") {\n    return {\n      display: \"inline-flex\",\n      alignItems: \"center\",\n      justifyContent: \"center\",\n      minWidth: 18,\n      height: 18,\n      padding: \"0 5px\",\n      marginLeft: 2,\n      borderRadius: 9,\n      boxSizing: \"border-box\",\n      background: palette.accentTint,\n      color: palette.accentText,\n      fontSize: 11.5,\n      fontWeight: 600,\n      lineHeight: \"18px\",\n      fontVariantNumeric: \"tabular-nums\",\n      verticalAlign: \"middle\",\n      opacity: span.opacity,\n      transform: `scale(${span.scale})`,\n    };\n  }\n  if (span.kind === \"punct\" || span.kind === \"comment\") {\n    return { color: palette.subtle };\n  }\n  if (span.kind === \"string\") return { color: palette.muted };\n  return {};\n}\n\nfunction renderSpans(spans: readonly AgentRunSpan[], palette: AgentRunPalette) {\n  return spans.map((span, index) => (\n    <span key={index} style={spanStyle(span, palette)}>\n      {span.fragments.map((fragment, part) => (\n        <span\n          key={part}\n          style={\n            fragment.opacity < 1 ? { opacity: fragment.opacity } : undefined\n          }\n        >\n          {fragment.text}\n        </span>\n      ))}\n    </span>\n  ));\n}\n\nfunction AnswerBlock({\n  block,\n  palette,\n}: {\n  block: AgentRunBlockState;\n  palette: AgentRunPalette;\n}) {\n  if (block.kind === \"code\") {\n    return (\n      <div\n        style={{\n          position: \"absolute\",\n          top: block.y,\n          left: 0,\n          width: COLUMN,\n          height: block.height,\n          opacity: block.opacity,\n          overflow: \"hidden\",\n          boxSizing: \"border-box\",\n          border: `1px solid ${palette.surfaceBorder}`,\n          borderRadius: 10,\n          background: palette.code,\n        }}\n      >\n        <div\n          style={{\n            height: CODE_HEAD,\n            boxSizing: \"border-box\",\n            display: \"flex\",\n            alignItems: \"center\",\n            justifyContent: \"space-between\",\n            padding: \"0 12px 0 16px\",\n            borderBottom: `1px solid ${palette.divider}`,\n            fontFamily: MONO,\n            fontSize: 12.5,\n            color: palette.muted,\n          }}\n        >\n          <span>{block.lang}</span>\n          <Icon name=\"copy\" size={14} color={palette.subtle} />\n        </div>\n        {block.lines.map((line) => (\n          <div\n            key={line.key}\n            style={{\n              position: \"absolute\",\n              top: CODE_HEAD + CODE_PAD - 1 + line.y,\n              left: 16,\n              height: line.height,\n              lineHeight: `${line.height}px`,\n              fontFamily: MONO,\n              fontSize: CODE_SIZE,\n              whiteSpace: \"pre\",\n              color: palette.fg,\n            }}\n          >\n            {renderSpans(line.spans, palette)}\n          </div>\n        ))}\n      </div>\n    );\n  }\n  if (block.kind === \"heading\") {\n    const spec = HEADINGS[block.level];\n    const from = BODY_SIZE / spec.size;\n    const scale = block.styled ? from + (1 - from) * block.morph : 1;\n    return (\n      <div>\n        {block.lines.map((line, index) => (\n          <div\n            key={line.key}\n            style={{\n              position: \"absolute\",\n              top: block.y + line.y,\n              left: 0,\n              height: line.height,\n              display: \"flex\",\n              alignItems: \"center\",\n              whiteSpace: \"pre\",\n            }}\n          >\n            {index === 0 && block.mark.length > 0 && block.morph < 1 ? (\n              <span\n                style={{\n                  maxWidth: block.markWidth * (1 - block.morph),\n                  overflow: \"hidden\",\n                  flexShrink: 0,\n                  opacity: 1 - block.morph,\n                  fontSize: BODY_SIZE,\n                  color: palette.subtle,\n                }}\n              >\n                {block.mark}\n              </span>\n            ) : null}\n            <span\n              style={{\n                fontSize: block.styled ? spec.size : BODY_SIZE,\n                fontWeight: Math.round(400 + 200 * block.morph),\n                lineHeight: 1.25,\n                transform: `scale(${round(scale)})`,\n                transformOrigin: \"0 50%\",\n              }}\n            >\n              {renderSpans(line.spans, palette)}\n            </span>\n          </div>\n        ))}\n      </div>\n    );\n  }\n  const indent = block.kind === \"item\" ? ITEM_INDENT : 0;\n  return (\n    <div>\n      {block.kind === \"item\" ? (\n        <div\n          style={{\n            position: \"absolute\",\n            top: block.y + BODY_LINE / 2 - 3,\n            left: 5,\n            width: 6,\n            height: 6,\n            borderRadius: 3,\n            background: palette.fg,\n            opacity: block.dotOpacity,\n            transform: `scale(${block.dot})`,\n          }}\n        />\n      ) : null}\n      {block.lines.map((line) => (\n        <div\n          key={line.key}\n          style={{\n            position: \"absolute\",\n            top: block.y + line.y,\n            left: indent,\n            height: line.height,\n            lineHeight: `${line.height}px`,\n            whiteSpace: \"pre\",\n          }}\n        >\n          {renderSpans(line.spans, palette)}\n        </div>\n      ))}\n    </div>\n  );\n}\n\nfunction Header({\n  header,\n  palette,\n}: {\n  header: AgentRunState[\"header\"];\n  palette: AgentRunPalette;\n}) {\n  return (\n    <div\n      style={{\n        height: HEADER_H,\n        boxSizing: \"border-box\",\n        display: \"flex\",\n        alignItems: \"center\",\n        justifyContent: \"space-between\",\n        gap: 16,\n        padding: `0 ${PAD_X}px`,\n        borderBottom: `1px solid ${palette.divider}`,\n      }}\n    >\n      <span\n        style={{\n          minWidth: 0,\n          fontSize: 14,\n          fontWeight: 500,\n          whiteSpace: \"nowrap\",\n          overflow: \"hidden\",\n          textOverflow: \"ellipsis\",\n        }}\n      >\n        {header.title}\n      </span>\n      <div\n        style={{\n          display: \"flex\",\n          alignItems: \"center\",\n          gap: 7,\n          flexShrink: 0,\n          opacity: header.opacity,\n        }}\n      >\n        <Spinner state={header.spinner} size={14} palette={palette} />\n        <span\n          style={{\n            minWidth: 38,\n            fontSize: 13,\n            color: palette.muted,\n            fontVariantNumeric: \"tabular-nums\",\n            textAlign: \"right\",\n          }}\n        >\n          {header.label}\n        </span>\n      </div>\n    </div>\n  );\n}\n\nfunction Bubble({\n  bubble,\n  palette,\n}: {\n  bubble: AgentRunState[\"bubble\"];\n  palette: AgentRunPalette;\n}) {\n  return (\n    <div\n      style={{\n        position: \"absolute\",\n        top: bubble.y,\n        right: 0,\n        padding: `${BUBBLE_PAD_Y}px ${BUBBLE_PAD_X}px`,\n        borderRadius: 18,\n        background: palette.surface,\n        fontSize: BUBBLE_SIZE,\n        lineHeight: `${BUBBLE_LINE}px`,\n        whiteSpace: \"pre\",\n        opacity: bubble.opacity,\n        transform: `translateY(${bubble.lift}px)`,\n      }}\n    >\n      {bubble.lines.map((line, index) => (\n        <div key={index}>{line}</div>\n      ))}\n    </div>\n  );\n}\n\nfunction Status({\n  status,\n  palette,\n}: {\n  status: AgentRunState[\"status\"];\n  palette: AgentRunPalette;\n}) {\n  if (!status.visible) return null;\n  return (\n    <div\n      style={{\n        position: \"absolute\",\n        top: status.y,\n        left: 0,\n        width: COLUMN,\n        height: STATUS_H,\n        fontSize: 15,\n        fontWeight: 500,\n        lineHeight: `${STATUS_H}px`,\n        whiteSpace: \"pre\",\n        opacity: status.opacity,\n      }}\n    >\n      {status.swap < 1 ? (\n        <span\n          style={{\n            position: \"absolute\",\n            top: 0,\n            left: 0,\n            opacity: 1 - status.swap,\n          }}\n        >\n          {status.shimmer.map((level, index) => (\n            <span key={index} style={{ opacity: level }}>\n              {status.thinking[index]}\n            </span>\n          ))}\n        </span>\n      ) : null}\n      {status.swap > 0 ? (\n        <span\n          style={{\n            position: \"absolute\",\n            top: 0,\n            left: 0,\n            color: palette.muted,\n            opacity: status.swap,\n            transform: `translateY(${status.lift}px)`,\n          }}\n        >\n          {status.thought}\n        </span>\n      ) : null}\n    </div>\n  );\n}\n\nfunction Plan({\n  plan,\n  palette,\n}: {\n  plan: NonNullable<AgentRunState[\"plan\"]>;\n  palette: AgentRunPalette;\n}) {\n  return (\n    <div\n      style={{\n        position: \"absolute\",\n        top: plan.y,\n        left: 0,\n        width: COLUMN,\n        fontSize: PLAN_SIZE,\n        color: palette.muted,\n      }}\n    >\n      <div\n        style={{\n          position: \"absolute\",\n          top: 0,\n          left: 0,\n          width: 2,\n          height: plan.rule,\n          borderRadius: 1,\n          background: palette.track,\n        }}\n      />\n      {plan.items.map((item) => (\n        <div key={item.key}>\n          <span\n            style={{\n              position: \"absolute\",\n              top: item.y,\n              left: 14,\n              lineHeight: `${PLAN_LINE}px`,\n              color: palette.subtle,\n              fontVariantNumeric: \"tabular-nums\",\n              opacity: item.numberOpacity,\n            }}\n          >\n            {`${item.index + 1}.`}\n          </span>\n          {item.lines.map((line) => (\n            <div\n              key={line.key}\n              style={{\n                position: \"absolute\",\n                top: item.y + line.y,\n                left: PLAN_INDENT,\n                height: line.height,\n                lineHeight: `${line.height}px`,\n                whiteSpace: \"pre\",\n              }}\n            >\n              {renderSpans(line.spans, palette)}\n            </div>\n          ))}\n        </div>\n      ))}\n    </div>\n  );\n}\n\nfunction chipStyle(\n  palette: AgentRunPalette,\n  pop: { opacity: number; scale: number },\n  padding: string,\n): CSSProperties {\n  return {\n    display: \"inline-flex\",\n    alignItems: \"center\",\n    gap: 5,\n    height: 24,\n    boxSizing: \"border-box\",\n    padding,\n    borderRadius: 7,\n    border: `1px solid ${palette.surfaceBorder}`,\n    background: palette.surface,\n    fontSize: CHIP_SIZE,\n    color: palette.muted,\n    fontVariantNumeric: \"tabular-nums\",\n    whiteSpace: \"nowrap\",\n    opacity: pop.opacity,\n    transform: `scale(${pop.scale})`,\n  };\n}\n\ntype AgentRunRowState = NonNullable<AgentRunState[\"tools\"]>[\"rows\"][number];\n\nfunction ToolRow({\n  row,\n  palette,\n}: {\n  row: AgentRunRowState;\n  palette: AgentRunPalette;\n}) {\n  const chips = row.chips.filter((chip) => chip.visible);\n  return (\n    <div\n      style={{\n        position: \"absolute\",\n        top: row.y,\n        left: 0,\n        right: 0,\n        height: row.height,\n        overflow: \"hidden\",\n        boxSizing: \"border-box\",\n        borderTop: row.divider ? `1px solid ${palette.divider}` : undefined,\n      }}\n    >\n      <div\n        style={{\n          height: ROW_H,\n          display: \"flex\",\n          alignItems: \"center\",\n          gap: 10,\n          padding: \"0 14px\",\n          opacity: row.content,\n        }}\n      >\n        <Icon name={row.icon} size={16} color={palette.muted} />\n        <span\n          style={{\n            flexShrink: 0,\n            fontSize: 15,\n            fontWeight: 500,\n            whiteSpace: \"nowrap\",\n          }}\n        >\n          {row.name}\n        </span>\n        <span\n          style={{\n            flex: 1,\n            minWidth: 0,\n            fontSize: 15,\n            color: palette.muted,\n            whiteSpace: \"nowrap\",\n            overflow: \"hidden\",\n            textOverflow: \"ellipsis\",\n          }}\n        >\n          {row.detail}\n        </span>\n        <span\n          style={{\n            fontSize: 13,\n            color: palette.muted,\n            fontVariantNumeric: \"tabular-nums\",\n            opacity: row.label,\n          }}\n        >\n          {row.duration}\n        </span>\n        <Spinner state={row.spinner} size={16} palette={palette} />\n      </div>\n      {row.sources ? (\n        <div\n          style={{\n            display: \"flex\",\n            gap: CHIP_GAP,\n            height: CHIPS_H,\n            padding: \"0 14px 0 40px\",\n            opacity: row.content,\n          }}\n        >\n          {chips.map((chip, index) => (\n            <span\n              key={`${index}-${chip.label}`}\n              style={chipStyle(palette, chip, \"0 9px 0 7px\")}\n            >\n              <Icon name={chip.icon} size={12} color={palette.subtle} />\n              {chip.label}\n            </span>\n          ))}\n          {row.more?.visible ? (\n            <span style={chipStyle(palette, row.more, \"0 9px\")}>\n              {`+${row.more.count}`}\n            </span>\n          ) : null}\n        </div>\n      ) : null}\n    </div>\n  );\n}\n\nfunction Tools({\n  tools,\n  palette,\n}: {\n  tools: NonNullable<AgentRunState[\"tools\"]>;\n  palette: AgentRunPalette;\n}) {\n  if (tools.rows.length === 0) return null;\n  return (\n    <div\n      style={{\n        position: \"absolute\",\n        top: tools.y,\n        left: 0,\n        width: COLUMN,\n        height: tools.height,\n        boxSizing: \"border-box\",\n        overflow: \"hidden\",\n        border: `1px solid ${palette.surfaceBorder}`,\n        borderRadius: 12,\n      }}\n    >\n      {tools.rows.map((row) => (\n        <ToolRow key={row.key} row={row} palette={palette} />\n      ))}\n    </div>\n  );\n}\n\nexport function AgentRun({\n  run = agentRunDefaults.run,\n  theme = agentRunDefaults.theme,\n  accentColor = agentRunDefaults.accentColor,\n  compression = agentRunDefaults.compression,\n  seed = agentRunDefaults.seed,\n  speed = agentRunDefaults.speed,\n  className,\n}: AgentRunProps) {\n  const frame = useCurrentFrame();\n  const { width, height, fps } = useVideoConfig();\n  const model = useMemo(\n    () => getAgentRunModel(run, { seed, compression, width, height }),\n    [run, seed, compression, width, height],\n  );\n  const state = getAgentRunState(frame, model, { fps, speed });\n  const palette = getAgentRunPalette(theme, accentColor);\n  const { panel } = state;\n\n  return (\n    <div className={className} style={{ position: \"absolute\", inset: 0 }}>\n      <div\n        style={{\n          position: \"absolute\",\n          left: \"50%\",\n          top: \"50%\",\n          width: panel.width,\n          height: panel.height,\n          translate: \"-50% -50%\",\n          scale: `${panel.unit}`,\n        }}\n      >\n        <div\n          style={{\n            position: \"relative\",\n            width: \"100%\",\n            height: \"100%\",\n            boxSizing: \"border-box\",\n            overflow: \"hidden\",\n            opacity: panel.opacity,\n            transform: `translateY(${panel.lift}px)`,\n            background: palette.panel,\n            border: `1px solid ${palette.border}`,\n            borderRadius: 18,\n            boxShadow: palette.shadow,\n            color: palette.fg,\n            fontFamily: SANS,\n            fontSize: BODY_SIZE,\n            WebkitFontSmoothing: \"antialiased\",\n          }}\n        >\n          <Header header={state.header} palette={palette} />\n          <div\n            style={{\n              position: \"relative\",\n              height: panel.viewport,\n              overflow: \"hidden\",\n            }}\n          >\n            <div\n              style={{\n                position: \"absolute\",\n                top: 0,\n                left: PAD_X,\n                width: COLUMN,\n                transform: `translateY(${-state.scroll}px)`,\n              }}\n            >\n              <Bubble bubble={state.bubble} palette={palette} />\n              <Status status={state.status} palette={palette} />\n              {state.plan ? <Plan plan={state.plan} palette={palette} /> : null}\n              {state.tools ? (\n                <Tools tools={state.tools} palette={palette} />\n              ) : null}\n              {state.blocks.map((block) => (\n                <AnswerBlock key={block.key} block={block} palette={palette} />\n              ))}\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/agent-run.tsx"
    }
  ],
  "type": "registry:component"
}
