Run Timeline
SaveA run's status, a per-step progress strip, and its steps in order on a connected rail, with start times, durations, and errors.
- Category
- Agent
- Version
- 0.2.0
- Frameworks
- Next.js, Vite + React
- Styling
- Tailwind CSS v4
Preview
Update the rate-limit docs
1 of 3 steps done
search_docs, Done
Found 3 pages
· 800 msedit_files, Running
Update the limits table
open_pull_request, Queued
Installation
npx @seamlessui/cli add run-timelinePre-release@seamlessui/cli is not on npm yet. Install from a local build.
Review the plan first. A dry run lists every file and package without changing anything:
npx @seamlessui/cli add run-timeline --dry-runUsage
import { RunTimeline } from "@/components/seamless/agent/run-timeline";
export default function RunTimelineExample() {
return (
<RunTimeline
className="w-full max-w-md"
run={{
title: "Update the rate-limit docs",
status: "running",
steps: [
{ 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" },
{ id: "2", name: "edit_files", kind: "tool", status: "running", summary: "Update the limits table", startedAt: "2026-11-14T10:00:01.600Z" },
{ id: "3", name: "open_pull_request", kind: "tool", status: "queued" },
],
}}
/>
);
}States
Every state, rendered from local fixture data.
Research run
No steps yet
Steps appear here as the agent works.
Research run
1 of 3 steps done
search_docs, Done
Found 3 pages
· 800 msfetch_page, Running
Read /docs/api/rate-limits
open_pull_request, Queued
Research run
2 of 3 steps done
search_docs, Done
Found 3 pages
· 800 msfetch_page, Done
Read /docs/api/rate-limits
· 1.3 sopen_pull_request, Queued
Research run
3 of 3 steps done · 3.4 s
search_docs, Done
Found 3 pages
· 800 msfetch_page, Done
Read /docs/api/rate-limits
· 1.3 sopen_pull_request, Done
Research run
1 of 3 steps done · 2.3 s
fetch_page failed: 504 Gateway Timeout.
search_docs, Done
Found 3 pages
· 800 msfetch_page, Failed
504 Gateway Timeout
· 1.3 sopen_pull_request, Skipped
Research run
2 of 3 steps done · 2.5 s
search_docs, Done
Found 3 pages
· 800 msfetch_page, Done
Read /docs/api/rate-limits
· 1.3 sopen_pull_request, Skipped
API
| Prop | Type | Default | Description |
|---|---|---|---|
| run | Pick<AgentRun, "title" | "status" | "steps" | "error" | "createdAt" | "finishedAt"> | — | 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.
Files
Installed into src/components/seamless, or components/seamless when the project has no src directory.
src/components/seamless/agent/run-timeline.tsxrun-timeline- Also installs badge@0.2.0registry dependency
- Also installs agent-run@0.1.1registry dependency
- Also installs step-list@0.2.0registry dependency
- npm packages: noneReact and Tailwind CSS v4 required
Source
The exact source the CLI installs, from /r/items/react/run-timeline/0.2.0.json.
agent/run-timeline.tsxShowHide
import type { ComponentProps } from "react";
import { Badge, type BadgeTone } from "../ui/badge";
import type { AgentRun, RunStatus, StepStatus } from "./agent-run";
import { StatusGlyph, type StatusGlyphKind, StepList } from "./step-list";
const runStatus: Record<RunStatus, { label: string; tone: BadgeTone; glyph: StatusGlyphKind }> = {
queued: { label: "Queued", tone: "neutral", glyph: "queued" },
running: { label: "Running", tone: "info", glyph: "running" },
"waiting-for-approval": { label: "Needs approval", tone: "warning", glyph: "waiting" },
succeeded: { label: "Succeeded", tone: "success", glyph: "success" },
failed: { label: "Failed", tone: "danger", glyph: "failed" },
cancelled: { label: "Cancelled", tone: "neutral", glyph: "skipped" },
};
const segment: Record<StepStatus, string> = {
queued: "bg-zinc-950/10 dark:bg-white/12",
running: "bg-sky-500",
succeeded: "bg-emerald-500",
failed: "bg-red-500",
skipped: "bg-zinc-950/[0.05] dark:bg-white/[0.06]",
};
export interface RunTimelineProps extends Omit<ComponentProps<"section">, "children"> {
run: Pick<AgentRun, "title" | "status" | "steps" | "error" | "createdAt" | "finishedAt">;
}
/** The run's status and its steps in order on a connected rail, with start times and durations. */
export function RunTimeline({ run, className, ...props }: RunTimelineProps) {
const meta = runStatus[run.status];
const done = run.steps.filter((step) => step.status === "succeeded").length;
const total = run.createdAt && run.finishedAt ? formatDuration(run.createdAt, run.finishedAt) : "";
return (
<section
aria-label={`${run.title} timeline`}
data-status={run.status}
className={[
"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",
"dark:bg-black dark:text-white dark:shadow-[inset_0_1px_0_rgb(255_255_255/0.06)]",
run.status === "waiting-for-approval"
? "border-amber-500/35"
: run.status === "failed"
? "border-red-500/35"
: "border-zinc-950/10 dark:border-white/10",
className,
]
.filter(Boolean)
.join(" ")}
{...props}
>
<header className="flex items-start gap-3 px-4 pt-4">
<StatusGlyph key={meta.glyph} kind={meta.glyph} size={20} />
<div className="min-w-0 flex-1">
<h3 className="truncate text-sm font-semibold">{run.title}</h3>
<p className="mt-0.5 text-xs text-zinc-500 tabular-nums">
{run.steps.length ? `${done} of ${run.steps.length} steps done` : "No steps yet"}
{total ? ` · ${total}` : ""}
</p>
</div>
<span role="status" className="shrink-0">
<Badge tone={meta.tone}>{meta.label}</Badge>
</span>
</header>
{run.steps.length ? (
<div aria-hidden="true" className="mx-4 mt-3 flex gap-1">
{run.steps.map((step) => (
<span
key={step.id}
className={`h-1 flex-1 rounded-full transition-colors duration-200 motion-reduce:transition-none ${segment[step.status]}`}
/>
))}
</div>
) : null}
{run.status === "failed" && run.error ? (
<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">
{run.error}
</p>
) : null}
<div className="mt-3 border-t border-zinc-950/[0.07] px-4 py-4 dark:border-white/[0.08]">
{run.steps.length ? (
<StepList
steps={run.steps.map((step) => ({
id: step.id,
status: step.status,
label: step.kind === "tool" ? <span className="font-mono text-[13px]">{step.name}</span> : step.name,
detail: step.status === "failed" && step.error ? step.error : step.summary,
meta: step.startedAt ? (
<>
<time dateTime={step.startedAt}>{formatTime(step.startedAt)}</time>
{step.finishedAt ? ` · ${formatDuration(step.startedAt, step.finishedAt)}` : ""}
</>
) : undefined,
}))}
/>
) : (
<p className="text-sm text-zinc-500">Steps appear here as the agent works.</p>
)}
</div>
</section>
);
}
function formatTime(iso: string): string {
const date = new Date(iso);
return Number.isNaN(date.getTime()) ? "" : date.toISOString().slice(11, 19);
}
function formatDuration(start: string, end: string): string {
const ms = new Date(end).getTime() - new Date(start).getTime();
if (!Number.isFinite(ms) || ms < 0) return "";
return ms < 1000 ? `${ms} ms` : `${(ms / 1000).toFixed(1)} s`;
}