{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "agent-steps",
  "title": "Agent Steps",
  "description": "A live agent run whose plan is still being written — steps stream in without moving what is already on screen, failures retry in place, and a replan cancels the rest of the plan instead of failing it.",
  "dependencies": [
    "lucide-react",
    "zod"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/registry/ui/agent-steps.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport {\n  Ban,\n  Check,\n  ChevronRight,\n  CircleAlert,\n  Hourglass,\n  ListChecks,\n  LoaderCircle,\n  Minus,\n  RotateCw,\n  X,\n} from \"lucide-react\"\nimport { cn } from \"@/lib/utils\"\nimport type { AgentStep, AgentStepStatus, AgentStepsStatus } from \"./agent-steps.contract\"\n\nconst STATUS_TEXT: Record<AgentStepStatus, string> = {\n  cancelled: \"Cancelled\",\n  done: \"Done\",\n  failed: \"Failed\",\n  pending: \"Queued\",\n  running: \"Running\",\n  skipped: \"Skipped\",\n}\n\n/**\n * Every state carries a shape *and* a word, never a hue alone: an empty ring is\n * queued, a spinner is running, a check is done, a cross failed, a dash was\n * skipped, and a dashed ring with a slash was cancelled by a replan. The last\n * two look calm on purpose — neither of them is an error.\n */\nconst NODE_CLASS: Record<AgentStepStatus, string> = {\n  cancelled: \"border-dashed border-muted-foreground/40 bg-transparent text-muted-foreground\",\n  done: \"border-primary bg-primary text-primary-foreground\",\n  // text-background, not text-destructive-foreground: that token does not exist\n  // in this theme, so it would be dropped and the glyph would inherit row colour.\n  failed: \"border-destructive bg-destructive text-background\",\n  pending: \"border-muted-foreground/30 bg-transparent text-muted-foreground\",\n  running: \"border-primary bg-transparent text-primary ring-2 ring-primary/20\",\n  skipped: \"border-border bg-muted text-muted-foreground\",\n}\n\nconst STATUS_TONE: Record<AgentStepStatus, string> = {\n  cancelled: \"text-muted-foreground\",\n  done: \"text-muted-foreground\",\n  failed: \"font-medium text-destructive\",\n  pending: \"text-muted-foreground\",\n  running: \"font-medium text-foreground\",\n  skipped: \"text-muted-foreground\",\n}\n\nconst TITLE_TONE: Record<AgentStepStatus, string> = {\n  // A struck-through title says \"this will never run\" without borrowing the\n  // colour of failure.\n  cancelled: \"text-muted-foreground line-through decoration-muted-foreground/60\",\n  done: \"text-foreground\",\n  failed: \"text-foreground\",\n  pending: \"text-muted-foreground\",\n  running: \"text-foreground\",\n  skipped: \"text-muted-foreground\",\n}\n\nconst TERMINAL: readonly AgentStepStatus[] = [\"done\", \"failed\", \"skipped\", \"cancelled\"]\n\nconst FOCUS_RING =\n  \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\"\n\n/**\n * Announcements are coalesced on this delay. A run that settles four steps in\n * one second speaks once, and a stream that appends thirty queued steps does\n * not speak at all — only a settled step, a stall or a settled run is news.\n */\nconst ANNOUNCE_DELAY = 700\n\nconst MIN_TICK = 100\n\nconst DEFAULT_STALL_MS = 30_000\n\nfunction isTerminal(status: AgentStepStatus) {\n  return TERMINAL.includes(status)\n}\n\nfunction toEpoch(value: string | number | Date | undefined): number | null {\n  if (value === undefined) return null\n  // Date.parse / getTime only — no `new Date()` during render.\n  const ms = typeof value === \"number\" ? value : typeof value === \"string\" ? Date.parse(value) : value.getTime()\n  return Number.isFinite(ms) ? ms : null\n}\n\n/**\n * \"exact\" keeps a tenth of a second under 10s (a settled step reading 1.2s looks\n * measured, 1s looks guessed); \"coarse\" is whole seconds, used for anything\n * still running so the digits move exactly once per tick.\n */\nfunction formatDuration(ms: number, mode: \"coarse\" | \"exact\" = \"exact\") {\n  const safe = Math.max(0, ms)\n  if (mode === \"exact\" && safe < 10_000) return `${(safe / 1000).toFixed(1)}s`\n  const totalSeconds = Math.floor(safe / 1000)\n  if (totalSeconds < 60) return `${totalSeconds}s`\n  const minutes = Math.floor(totalSeconds / 60)\n  if (minutes < 60) return `${minutes}m ${String(totalSeconds % 60).padStart(2, \"0\")}s`\n  return `${Math.floor(minutes / 60)}h ${String(minutes % 60).padStart(2, \"0\")}m`\n}\n\nfunction countLines(text: string) {\n  let lines = 1\n  for (let i = 0; i < text.length; i += 1) if (text.charCodeAt(i) === 10) lines += 1\n  return lines\n}\n\nfunction getServerClock(): null {\n  return null\n}\n\n/**\n * The wall clock as an external store, floored to the tick so the snapshot is\n * cacheable (React re-reads it on every render; a raw `Date.now()` differs on\n * consecutive calls and trips the \"getSnapshot should be cached\" loop guard).\n *\n * Elapsed time is always recomputed as `now - startedAt`, never accumulated by\n * a counter, so a throttled or frozen timer costs you *update frequency*, not\n * correctness: a tab that slept for forty seconds reads 40s on its next tick,\n * and the visibilitychange subscription makes that read happen the instant the\n * page comes back instead of up to one tick later.\n */\nfunction useWallClock(active: boolean, interval: number): number | null {\n  const subscribe = React.useCallback(\n    (onStoreChange: () => void) => {\n      if (!active) return () => {}\n      const id = window.setInterval(onStoreChange, interval)\n      const onVisibility = () => {\n        if (document.visibilityState === \"visible\") onStoreChange()\n      }\n      document.addEventListener(\"visibilitychange\", onVisibility)\n      return () => {\n        window.clearInterval(id)\n        document.removeEventListener(\"visibilitychange\", onVisibility)\n      }\n    },\n    [active, interval],\n  )\n\n  const getSnapshot = React.useCallback(() => {\n    if (!active) return null\n    return Math.floor(Date.now() / interval) * interval\n  }, [active, interval])\n\n  return React.useSyncExternalStore(subscribe, getSnapshot, getServerClock)\n}\n\nfunction StepGlyph({ status }: { status: AgentStepStatus }) {\n  if (status === \"running\") {\n    return <LoaderCircle aria-hidden=\"true\" className=\"size-3.5 animate-spin motion-reduce:animate-none\" />\n  }\n  if (status === \"done\") return <Check aria-hidden=\"true\" className=\"size-3.5\" />\n  if (status === \"failed\") return <X aria-hidden=\"true\" className=\"size-3.5\" />\n  if (status === \"skipped\") return <Minus aria-hidden=\"true\" className=\"size-3.5\" />\n  if (status === \"cancelled\") return <Ban aria-hidden=\"true\" className=\"size-3\" />\n  // pending is the empty ring itself.\n  return null\n}\n\n/**\n * One sentence, or nothing. Appending queued steps is deliberately silent — a\n * plan that grows while you are reading it must not interrupt the reader.\n */\nfunction announcementFor(steps: AgentStep[], stalledTitle: string | null): string {\n  if (steps.length === 0) return \"\"\n  const total = steps.length\n  let done = 0\n  let failed = 0\n  let settled = 0\n  let last: AgentStep | null = null\n  for (const step of steps) {\n    if (step.status === \"done\") done += 1\n    if (step.status === \"failed\") failed += 1\n    if (isTerminal(step.status)) {\n      settled += 1\n      last = step\n    }\n  }\n\n  if (settled === total) {\n    return failed > 0\n      ? `Run finished with ${failed} failed step${failed === 1 ? \"\" : \"s\"}. ${done} of ${total} steps completed.`\n      : `Run finished. ${done} of ${total} steps completed.`\n  }\n  if (stalledTitle) return `Still running: ${stalledTitle}. No result yet.`\n  if (!last) return \"\"\n  return `${last.title}: ${STATUS_TEXT[last.status].toLowerCase()}. ${settled} of ${total} steps complete.`\n}\n\nexport interface AgentStepsProps extends React.HTMLAttributes<HTMLDivElement> {\n  items: AgentStep[]\n  status: AgentStepsStatus\n  /**\n   * Pin \"now\" (ISO string, epoch ms or Date) instead of reading the wall clock:\n   * deterministic screenshots, server rendering, and time-travel demos.\n   */\n  now?: string | number | Date\n  /** Cadence of the running step's live readout, ms. Clamped to >= 100; 0 (or non-finite) freezes it and no interval is ever created. */\n  tickMs?: number\n  /** A running step older than this gets a \"still running\" note and one announcement. 0 disables it. */\n  stallAfterMs?: number\n  /** Renders the per-step Retry control on failed steps. Omit it and no retry affordance exists. */\n  onRetryStep?: (id: string) => void\n  /** Reloads the whole run — the button in the `error` branch. Omit it and the branch has no button. */\n  onReload?: () => void\n  /** Names the list for assistive tech and titles the header. */\n  label?: string\n  /** Rows drawn in the loading branch. Clamped to 1–12. */\n  skeletonRows?: number\n  emptyState?: React.ReactNode\n}\n\n/**\n * A live view of an agent working through a plan it is still writing.\n *\n * Steps arrive while the run is in flight, so the list only ever grows\n * downwards — nothing above a new row moves. Steps can fail and be retried, be\n * skipped, or be *cancelled* when the agent throws away the rest of its plan\n * and writes a new one; the finished work above the revision stays exactly\n * where it was, and nothing the component renders about one step depends on\n * another step's status.\n */\nexport const AgentSteps = React.forwardRef<HTMLDivElement, AgentStepsProps>(function AgentSteps(\n  {\n    items,\n    status,\n    now: nowProp,\n    tickMs = 1000,\n    stallAfterMs = DEFAULT_STALL_MS,\n    onRetryStep,\n    onReload,\n    label = \"Agent steps\",\n    skeletonRows = 3,\n    emptyState,\n    className,\n    ...props\n  },\n  ref,\n) {\n  const uid = React.useId()\n  const [openOutputs, setOpenOutputs] = React.useState<Record<string, boolean>>({})\n  const [announcement, setAnnouncement] = React.useState(\"\")\n  const announcedRef = React.useRef<string | null>(null)\n\n  // Duplicate ids collide on React keys and on the aria-controls wiring — one\n  // disclosure would toggle its twin. First occurrence wins.\n  const rows = React.useMemo(() => {\n    const seen = new Set<string>()\n    const kept: AgentStep[] = []\n    for (const step of items) {\n      if (seen.has(step.id)) continue\n      seen.add(step.id)\n      kept.push(step)\n    }\n    return kept.map((step, index) => {\n      const previous = index === 0 ? null : kept[index - 1]\n      return {\n        lineCount: step.output ? countLines(step.output) : 0,\n        // The first step of a *later* planning pass opens the band; the very\n        // first step never does, however high its revision number is.\n        revisionBreak: previous !== null && (step.planRevision ?? 0) > (previous.planRevision ?? 0),\n        step,\n      }\n    })\n  }, [items])\n\n  const totals = React.useMemo(() => {\n    let settled = 0\n    let first: number | null = null\n    let last: number | null = null\n    for (const { step } of rows) {\n      if (isTerminal(step.status)) settled += 1\n      const start = toEpoch(step.startedAt)\n      const end = toEpoch(step.endedAt)\n      if (start !== null && (first === null || start < first)) first = start\n      if (end !== null && (last === null || end > last)) last = end\n    }\n    return { first, last, settled, total: rows.length }\n  }, [rows])\n\n  const tick = Number.isFinite(tickMs) && tickMs > 0 ? Math.max(MIN_TICK, tickMs) : 0\n  const stallAfter = Number.isFinite(stallAfterMs) && stallAfterMs > 0 ? stallAfterMs : 0\n  const pinnedNow = toEpoch(nowProp)\n  const needsClock =\n    pinnedNow === null &&\n    status === \"ready\" &&\n    rows.some(({ step }) => step.status === \"running\" && toEpoch(step.startedAt) !== null)\n  const clockNow = useWallClock(needsClock && tick > 0, tick || MIN_TICK)\n  const nowMs = pinnedNow ?? clockNow\n\n  const meta = rows.map(row => {\n    const start = toEpoch(row.step.startedAt)\n    const end = toEpoch(row.step.endedAt)\n    const live = row.step.status === \"running\" && start !== null && nowMs !== null ? nowMs - start : null\n    let duration: string | null = null\n    if (start !== null && end !== null) duration = formatDuration(end - start)\n    else if (live !== null) duration = formatDuration(live, \"coarse\")\n    return {\n      ...row,\n      duration,\n      // Long silence is a visible state, not something the reader has to time\n      // with their own stopwatch.\n      stalled: stallAfter > 0 && live !== null && live >= stallAfter,\n      stalledFor: live,\n    }\n  })\n\n  const stalledTitle = meta.find(row => row.stalled)?.step.title ?? null\n\n  const liveMessage =\n    status === \"loading\"\n      ? \"Planning the run.\"\n      : status === \"error\"\n        ? \"The run could not be loaded.\"\n        : status === \"empty\"\n          ? \"\"\n          : announcementFor(\n              rows.map(row => row.step),\n              stalledTitle,\n            )\n\n  React.useEffect(() => {\n    // The first paint is the baseline, not news — landing on a finished run\n    // should not shout its outcome at you.\n    if (announcedRef.current === null) {\n      announcedRef.current = liveMessage\n      return\n    }\n    if (announcedRef.current === liveMessage) return\n    const timer = window.setTimeout(() => {\n      announcedRef.current = liveMessage\n      setAnnouncement(liveMessage)\n    }, ANNOUNCE_DELAY)\n    return () => window.clearTimeout(timer)\n  }, [liveMessage])\n\n  const toggleOutput = (id: string) => setOpenOutputs(prev => ({ ...prev, [id]: !prev[id] }))\n\n  const skeletonCount = Math.max(1, Math.min(12, Math.floor(Number.isFinite(skeletonRows) ? skeletonRows : 3)))\n  const runSettled = totals.total > 0 && totals.settled === totals.total\n  const runElapsed =\n    totals.first !== null && totals.last !== null && totals.last >= totals.first\n      ? formatDuration(totals.last - totals.first)\n      : null\n\n  return (\n    <div className={cn(\"w-full rounded-xl border bg-card text-card-foreground\", className)} ref={ref} {...props}>\n      {status === \"loading\" && (\n        <div aria-hidden=\"true\" className=\"flex flex-col gap-3 p-3\">\n          {Array.from({ length: skeletonCount }, (_, index) => (\n            <div className=\"flex items-start gap-3\" key={index}>\n              <div className=\"size-6 shrink-0 animate-pulse rounded-full bg-muted motion-reduce:animate-none\" />\n              <div className=\"flex min-w-0 flex-1 flex-col gap-2 pt-1\">\n                <div\n                  className=\"h-3 animate-pulse rounded bg-muted motion-reduce:animate-none\"\n                  style={{ width: `${44 + ((index * 17) % 30)}%` }}\n                />\n                <div\n                  className=\"h-2.5 animate-pulse rounded bg-muted motion-reduce:animate-none\"\n                  style={{ width: `${18 + ((index * 11) % 16)}%` }}\n                />\n              </div>\n            </div>\n          ))}\n        </div>\n      )}\n\n      {status === \"empty\" &&\n        (emptyState ?? (\n          <div className=\"flex flex-col items-center gap-2 px-6 py-10 text-center\">\n            <ListChecks aria-hidden=\"true\" className=\"size-7 text-muted-foreground/60\" />\n            <p className=\"text-sm font-medium\">No steps yet</p>\n            <p className=\"text-xs text-muted-foreground\">\n              The agent hasn&apos;t planned anything for this turn — steps appear here as it works.\n            </p>\n          </div>\n        ))}\n\n      {status === \"error\" && (\n        <div className=\"flex flex-col items-center gap-3 px-6 py-10 text-center\">\n          <CircleAlert aria-hidden=\"true\" className=\"size-7 text-destructive\" />\n          <div className=\"flex flex-col gap-1\">\n            <p className=\"text-sm font-medium\">Couldn&apos;t load this run</p>\n            <p className=\"text-xs text-muted-foreground\">The step log didn&apos;t come back.</p>\n          </div>\n          {onReload && (\n            <button\n              className={cn(\n                \"cursor-pointer rounded-md border px-3 py-1.5 text-xs font-medium transition-colors hover:bg-muted\",\n                FOCUS_RING,\n              )}\n              onClick={onReload}\n              type=\"button\"\n            >\n              Try again\n            </button>\n          )}\n        </div>\n      )}\n\n      {status === \"ready\" && (\n        <>\n          {/* Fixed height + truncate + tabular-nums: the header is the only thing\n              above the list, so it must never grow a second line when the counts\n              change — that would push every row down mid-read. */}\n          <div className=\"flex h-10 items-center gap-3 border-b px-3\">\n            <span className=\"min-w-0 flex-1 truncate text-sm font-medium\">{label}</span>\n            <span className=\"shrink-0 text-xs tabular-nums text-muted-foreground\">\n              {totals.total === 0\n                ? \"No steps\"\n                : `${totals.settled}/${totals.total} settled${runSettled && runElapsed ? ` · ${runElapsed}` : \"\"}`}\n            </span>\n          </div>\n\n          {meta.length === 0 ? (\n            <p className=\"px-3 py-6 text-center text-sm text-muted-foreground\">No steps in this run yet.</p>\n          ) : (\n            <ol aria-label={label} className=\"flex w-full flex-col p-3\" role=\"list\">\n              {meta.map((row, index) => {\n                const step = row.step\n                const outputId = `${uid}-output-${index}`\n                const triggerId = `${uid}-trigger-${index}`\n                const open = Boolean(step.output) && Boolean(openOutputs[step.id])\n                const retried = step.retryCount ?? 0\n                const isLast = index === meta.length - 1\n                return (\n                  <li\n                    aria-current={step.status === \"running\" ? \"step\" : undefined}\n                    className=\"relative flex gap-3 pb-4 last:pb-0\"\n                    data-stalled={row.stalled ? \"true\" : undefined}\n                    data-status={step.status}\n                    key={step.id}\n                    role=\"listitem\"\n                  >\n                    {!isLast && (\n                      <span\n                        aria-hidden=\"true\"\n                        className={cn(\n                          \"absolute bottom-0 left-3 top-6 w-px -translate-x-1/2\",\n                          step.status === \"done\"\n                            ? \"bg-primary\"\n                            : step.status === \"failed\"\n                              ? \"bg-destructive\"\n                              : \"bg-border\",\n                        )}\n                      />\n                    )}\n\n                    <span\n                      aria-hidden=\"true\"\n                      className={cn(\n                        \"relative z-10 flex size-6 shrink-0 items-center justify-center rounded-full border transition-colors\",\n                        NODE_CLASS[step.status],\n                      )}\n                    >\n                      <StepGlyph status={step.status} />\n                    </span>\n\n                    <div className=\"min-w-0 flex-1\">\n                      {row.revisionBreak && (\n                        // The band sits INSIDE the step it opens, so the list is\n                        // still exactly one listitem per step.\n                        <p className=\"-mt-0.5 mb-2 flex items-center gap-2 text-xs font-medium text-muted-foreground\">\n                          <span className=\"shrink-0\">Plan revised</span>\n                          <span aria-hidden=\"true\" className=\"h-px flex-1 bg-border\" />\n                        </p>\n                      )}\n\n                      <div className=\"flex flex-wrap items-baseline justify-between gap-x-3 gap-y-0.5\">\n                        <span className=\"flex min-w-0 flex-wrap items-baseline gap-x-2 gap-y-1\">\n                          <span className={cn(\"min-w-0 text-sm font-medium wrap-anywhere\", TITLE_TONE[step.status])}>\n                            {step.title}\n                          </span>\n                          {retried > 0 && (\n                            <span className=\"shrink-0 rounded-full border px-1.5 py-px text-[0.65rem] font-medium tabular-nums text-muted-foreground\">\n                              {retried} {retried === 1 ? \"retry\" : \"retries\"}\n                            </span>\n                          )}\n                        </span>\n                        <span className=\"flex shrink-0 items-center gap-1.5 text-xs\">\n                          <span className={STATUS_TONE[step.status]}>{STATUS_TEXT[step.status]}</span>\n                          {row.duration && (\n                            <>\n                              <span aria-hidden=\"true\" className=\"text-muted-foreground\">\n                                ·\n                              </span>\n                              <span className=\"tabular-nums text-muted-foreground\">{row.duration}</span>\n                            </>\n                          )}\n                        </span>\n                      </div>\n\n                      {step.detail && (\n                        <p className=\"mt-0.5 text-xs text-muted-foreground wrap-anywhere\">{step.detail}</p>\n                      )}\n\n                      {step.summary && (\n                        <p className=\"mt-1 text-xs leading-relaxed text-muted-foreground wrap-anywhere\">\n                          {step.summary}\n                        </p>\n                      )}\n\n                      {step.status === \"cancelled\" && step.cancelledReason && (\n                        <p className=\"mt-1 text-xs text-muted-foreground wrap-anywhere\">{step.cancelledReason}</p>\n                      )}\n\n                      {step.status === \"failed\" && step.error && (\n                        <div className=\"mt-2 max-h-40 overflow-auto rounded-lg border border-destructive/40 bg-destructive/5 p-2.5\">\n                          <pre className=\"whitespace-pre-wrap font-mono text-xs leading-relaxed text-foreground wrap-anywhere\">\n                            {step.error}\n                          </pre>\n                        </div>\n                      )}\n\n                      {step.output && (\n                        <>\n                          <button\n                            aria-controls={outputId}\n                            aria-expanded={open}\n                            className={cn(\n                              \"mt-1.5 inline-flex cursor-pointer items-center gap-1 rounded-md text-xs font-medium text-muted-foreground\",\n                              \"transition-colors hover:text-foreground\",\n                              FOCUS_RING,\n                            )}\n                            id={triggerId}\n                            onClick={() => toggleOutput(step.id)}\n                            type=\"button\"\n                          >\n                            <ChevronRight\n                              aria-hidden=\"true\"\n                              className={cn(\n                                \"size-3.5 transition-transform duration-200 motion-reduce:transition-none\",\n                                open && \"rotate-90\",\n                              )}\n                            />\n                            {open ? \"Hide output\" : \"Show output\"}\n                            <span className=\"tabular-nums\">\n                              · {row.lineCount} {row.lineCount === 1 ? \"line\" : \"lines\"}\n                            </span>\n                          </button>\n                          {/* A tall output scrolls inside its own labelled,\n                              focusable region — never clipped, never ellipsised,\n                              and never allowed to push the run off screen. */}\n                          <div\n                            aria-labelledby={triggerId}\n                            className=\"mt-1.5 max-h-80 overflow-auto rounded-lg border bg-muted/40 p-2.5\"\n                            data-slot=\"step-output\"\n                            hidden={!open}\n                            id={outputId}\n                            role=\"group\"\n                            tabIndex={open ? 0 : -1}\n                          >\n                            <pre className=\"whitespace-pre-wrap font-mono text-xs leading-relaxed text-foreground wrap-anywhere\">\n                              {step.output}\n                            </pre>\n                          </div>\n                        </>\n                      )}\n\n                      {step.status === \"failed\" && onRetryStep && (\n                        <button\n                          aria-label={`Retry step: ${step.title}`}\n                          className={cn(\n                            \"mt-2 inline-flex cursor-pointer items-center gap-1.5 rounded-md border px-2 py-1 text-xs font-medium\",\n                            \"transition-colors hover:bg-muted\",\n                            FOCUS_RING,\n                          )}\n                          onClick={() => onRetryStep(step.id)}\n                          type=\"button\"\n                        >\n                          <RotateCw aria-hidden=\"true\" className=\"size-3.5\" />\n                          Retry\n                        </button>\n                      )}\n\n                      {row.stalled && row.stalledFor !== null && (\n                        // Last in the row, so it can only ever push the steps\n                        // *below* it — never the output the reader is looking at.\n                        <p className=\"mt-2 flex items-start gap-1.5 text-xs text-muted-foreground\">\n                          <Hourglass aria-hidden=\"true\" className=\"mt-px size-3.5 shrink-0\" />\n                          <span className=\"wrap-anywhere\">\n                            Still running after{\" \"}\n                            <span className=\"tabular-nums\">{formatDuration(row.stalledFor, \"coarse\")}</span> — no\n                            result yet.\n                          </span>\n                        </p>\n                      )}\n                    </div>\n                  </li>\n                )\n              })}\n            </ol>\n          )}\n        </>\n      )}\n\n      <span aria-live=\"polite\" className=\"sr-only\" role=\"status\">\n        {announcement}\n      </span>\n    </div>\n  )\n})\n\nAgentSteps.displayName = \"AgentSteps\"\n\nexport default AgentSteps\n",
      "type": "registry:ui"
    },
    {
      "path": "src/registry/ui/agent-steps.contract.ts",
      "content": "import { z } from \"zod\"\n\n/**\n * What one step of an agent run reports.\n *\n * `cancelled` and `failed` are deliberately separate terminal states, and the\n * distinction is the whole point of this contract: a *failed* step tried and\n * broke (it can be retried), a *cancelled* step was thrown away when the agent\n * revised its plan and will never run. Collapsing them into one \"not ok\" state\n * makes a replan read as a crash. `skipped` is a third thing again: the agent\n * looked at the step and decided it was unnecessary.\n */\nexport const agentStepStatusSchema = z.enum([\"pending\", \"running\", \"done\", \"failed\", \"skipped\", \"cancelled\"])\n\nexport const agentStepSchema = z.object({\n  /** Stable identity. Drives React keys, the output disclosure and `onRetryStep`. Duplicates are dropped. */\n  id: z.string().min(1),\n  title: z.string().min(1),\n  /** One line under the title — the tool being called, the file being touched. */\n  detail: z.string().optional(),\n  status: agentStepStatusSchema,\n  /**\n   * Absolute instants, ISO 8601 with an offset. Durations are always derived\n   * from these against an injected `now`; the component never starts a\n   * stopwatch of its own, so a step that began before the panel mounted (or\n   * while the tab was suspended) still reports the true elapsed time.\n   */\n  startedAt: z.iso.datetime({ offset: true }).optional(),\n  endedAt: z.iso.datetime({ offset: true }).optional(),\n  /** One-line result, shown inline the moment the step settles. */\n  summary: z.string().optional(),\n  /** The full produced text. Collapsed behind a disclosure, never truncated once open. */\n  output: z.string().optional(),\n  /** Why a failed step failed. Rendered without a disclosure — a failure you have to click to read is a failure you miss. */\n  error: z.string().optional(),\n  /** Re-runs after a failure. 0 / undefined = first attempt; anything higher renders as a visible badge. */\n  retryCount: z.number().int().nonnegative().optional(),\n  /** Why a cancelled step will never run, e.g. \"superseded by the revised plan\". */\n  cancelledReason: z.string().optional(),\n  /**\n   * Which planning pass produced this step (0 = the original plan). A step whose\n   * revision is higher than its predecessor's opens a \"Plan revised\" band, which\n   * is what makes a mid-run change of plan legible instead of looking like a\n   * batch of steps that mysteriously died.\n   */\n  planRevision: z.number().int().nonnegative().optional(),\n})\n\n/** The panel's own render state — \"is there a run to show at all\", independent of any step's status. */\nexport const agentStepsStatusSchema = z.enum([\"loading\", \"empty\", \"error\", \"ready\"])\n\nexport const agentStepsSchema = z.object({\n  status: agentStepsStatusSchema,\n  items: z.array(agentStepSchema),\n})\n\nexport type AgentStepStatus = z.infer<typeof agentStepStatusSchema>\nexport type AgentStep = z.infer<typeof agentStepSchema>\nexport type AgentStepsStatus = z.infer<typeof agentStepsStatusSchema>\nexport type AgentStepsData = z.infer<typeof agentStepsSchema>\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}