{
  "schemaVersion": 1,
  "target": "react",
  "name": "run-timeline",
  "version": "0.2.0",
  "category": "agent",
  "title": "Run Timeline",
  "description": "A run's status, a per-step progress strip, and its steps in order on a connected rail, with start times, durations, and errors.",
  "tags": [
    "agent",
    "run",
    "timeline",
    "steps",
    "status"
  ],
  "requires": {
    "projectTypes": [
      "next",
      "vite-react"
    ],
    "styling": "tailwind-v4"
  },
  "npmDependencies": {},
  "registryDependencies": [
    {
      "name": "badge",
      "version": "0.2.0"
    },
    {
      "name": "agent-run",
      "version": "0.1.1"
    },
    {
      "name": "step-list",
      "version": "0.2.0"
    }
  ],
  "docsPath": "/components/run-timeline",
  "props": [
    {
      "name": "run",
      "type": "Pick<AgentRun, \"title\" | \"status\" | \"steps\" | \"error\" | \"createdAt\" | \"finishedAt\">",
      "description": "The run to show. Times are shown in UTC so server and client render the same text."
    }
  ],
  "accessibility": [
    "The run status label is a live region, so status changes are announced.",
    "Each step includes its status as screen-reader text and as a visible label; the progress strip is decorative.",
    "Failures are shown as text, not only red.",
    "With reduced motion the spinner holds still and status changes are instant."
  ],
  "states": [
    "queued",
    "running",
    "waiting-for-approval",
    "succeeded",
    "failed",
    "cancelled"
  ],
  "files": [
    {
      "path": "agent/run-timeline.tsx",
      "content": "import type { ComponentProps } from \"react\";\nimport { Badge, type BadgeTone } from \"../ui/badge\";\nimport type { AgentRun, RunStatus, StepStatus } from \"./agent-run\";\nimport { StatusGlyph, type StatusGlyphKind, StepList } from \"./step-list\";\n\nconst runStatus: Record<RunStatus, { label: string; tone: BadgeTone; glyph: StatusGlyphKind }> = {\n  queued: { label: \"Queued\", tone: \"neutral\", glyph: \"queued\" },\n  running: { label: \"Running\", tone: \"info\", glyph: \"running\" },\n  \"waiting-for-approval\": { label: \"Needs approval\", tone: \"warning\", glyph: \"waiting\" },\n  succeeded: { label: \"Succeeded\", tone: \"success\", glyph: \"success\" },\n  failed: { label: \"Failed\", tone: \"danger\", glyph: \"failed\" },\n  cancelled: { label: \"Cancelled\", tone: \"neutral\", glyph: \"skipped\" },\n};\n\nconst segment: Record<StepStatus, string> = {\n  queued: \"bg-zinc-950/10 dark:bg-white/12\",\n  running: \"bg-sky-500\",\n  succeeded: \"bg-emerald-500\",\n  failed: \"bg-red-500\",\n  skipped: \"bg-zinc-950/[0.05] dark:bg-white/[0.06]\",\n};\n\nexport interface RunTimelineProps extends Omit<ComponentProps<\"section\">, \"children\"> {\n  run: Pick<AgentRun, \"title\" | \"status\" | \"steps\" | \"error\" | \"createdAt\" | \"finishedAt\">;\n}\n\n/** The run's status and its steps in order on a connected rail, with start times and durations. */\nexport function RunTimeline({ run, className, ...props }: RunTimelineProps) {\n  const meta = runStatus[run.status];\n  const done = run.steps.filter((step) => step.status === \"succeeded\").length;\n  const total = run.createdAt && run.finishedAt ? formatDuration(run.createdAt, run.finishedAt) : \"\";\n\n  return (\n    <section\n      aria-label={`${run.title} timeline`}\n      data-status={run.status}\n      className={[\n        \"rounded-xl border bg-white text-zinc-950 shadow-[0_1px_2px_rgb(0_0_0/0.04)] transition-[border-color] duration-200 motion-reduce:transition-none\",\n        \"dark:bg-black dark:text-white dark:shadow-[inset_0_1px_0_rgb(255_255_255/0.06)]\",\n        run.status === \"waiting-for-approval\"\n          ? \"border-amber-500/35\"\n          : run.status === \"failed\"\n            ? \"border-red-500/35\"\n            : \"border-zinc-950/10 dark:border-white/10\",\n        className,\n      ]\n        .filter(Boolean)\n        .join(\" \")}\n      {...props}\n    >\n      <header className=\"flex items-start gap-3 px-4 pt-4\">\n        <StatusGlyph key={meta.glyph} kind={meta.glyph} size={20} />\n        <div className=\"min-w-0 flex-1\">\n          <h3 className=\"truncate text-sm font-semibold\">{run.title}</h3>\n          <p className=\"mt-0.5 text-xs text-zinc-500 tabular-nums\">\n            {run.steps.length ? `${done} of ${run.steps.length} steps done` : \"No steps yet\"}\n            {total ? ` · ${total}` : \"\"}\n          </p>\n        </div>\n        <span role=\"status\" className=\"shrink-0\">\n          <Badge tone={meta.tone}>{meta.label}</Badge>\n        </span>\n      </header>\n\n      {run.steps.length ? (\n        <div aria-hidden=\"true\" className=\"mx-4 mt-3 flex gap-1\">\n          {run.steps.map((step) => (\n            <span\n              key={step.id}\n              className={`h-1 flex-1 rounded-full transition-colors duration-200 motion-reduce:transition-none ${segment[step.status]}`}\n            />\n          ))}\n        </div>\n      ) : null}\n\n      {run.status === \"failed\" && run.error ? (\n        <p className=\"mx-4 mt-3 rounded-lg border border-red-500/35 bg-red-500/[0.08] px-3 py-2 text-sm text-red-700 dark:text-red-300\">\n          {run.error}\n        </p>\n      ) : null}\n\n      <div className=\"mt-3 border-t border-zinc-950/[0.07] px-4 py-4 dark:border-white/[0.08]\">\n        {run.steps.length ? (\n          <StepList\n            steps={run.steps.map((step) => ({\n              id: step.id,\n              status: step.status,\n              label: step.kind === \"tool\" ? <span className=\"font-mono text-[13px]\">{step.name}</span> : step.name,\n              detail: step.status === \"failed\" && step.error ? step.error : step.summary,\n              meta: step.startedAt ? (\n                <>\n                  <time dateTime={step.startedAt}>{formatTime(step.startedAt)}</time>\n                  {step.finishedAt ? ` · ${formatDuration(step.startedAt, step.finishedAt)}` : \"\"}\n                </>\n              ) : undefined,\n            }))}\n          />\n        ) : (\n          <p className=\"text-sm text-zinc-500\">Steps appear here as the agent works.</p>\n        )}\n      </div>\n    </section>\n  );\n}\n\nfunction formatTime(iso: string): string {\n  const date = new Date(iso);\n  return Number.isNaN(date.getTime()) ? \"\" : date.toISOString().slice(11, 19);\n}\n\nfunction formatDuration(start: string, end: string): string {\n  const ms = new Date(end).getTime() - new Date(start).getTime();\n  if (!Number.isFinite(ms) || ms < 0) return \"\";\n  return ms < 1000 ? `${ms} ms` : `${(ms / 1000).toFixed(1)} s`;\n}\n"
    }
  ],
  "example": {
    "content": "import { RunTimeline } from \"@/components/seamless/agent/run-timeline\";\n\nexport default function RunTimelineExample() {\n  return (\n    <RunTimeline\n      className=\"w-full max-w-md\"\n      run={{\n        title: \"Update the rate-limit docs\",\n        status: \"running\",\n        steps: [\n          { id: \"1\", name: \"search_docs\", kind: \"tool\", status: \"succeeded\", summary: \"Found 3 pages\", startedAt: \"2026-11-14T10:00:00.600Z\", finishedAt: \"2026-11-14T10:00:01.400Z\" },\n          { id: \"2\", name: \"edit_files\", kind: \"tool\", status: \"running\", summary: \"Update the limits table\", startedAt: \"2026-11-14T10:00:01.600Z\" },\n          { id: \"3\", name: \"open_pull_request\", kind: \"tool\", status: \"queued\" },\n        ],\n      }}\n    />\n  );\n}\n"
  }
}
