Show the run as a vertical list of its planned steps: pending ones muted, the active one with a live timer, finished ones checked with one line of evidence, failed ones with the reason and a retry. A step that needs a person shows as waiting and holds the approval inline, and a filling connector shows overall progress.
Why a timeline and not a chat#
Agents that run for minutes (support triage, refunds, data cleanup, coding tasks) do work in steps, and people check on them the way they check a delivery: what is done, what is happening, what is next, is anything stuck. A chat thread answers that badly: steps scroll away, and the state of the whole run is never in one place. A timeline keeps the run on one screen.
The states#
Every step needs five, and each must be distinct without colour alone:
| State | Shows |
|---|---|
| Pending | Title, muted. "Up next" for screen readers. |
| Active | Title in full colour, a live timer, a subtle indicator. |
| Done | A check, the duration, one line of evidence. |
| Error | The reason in a sentence, Retry or Skip. |
| Waiting | What it waits for, with the approval or input inline. |
Cancelled is worth adding if people can stop runs.
Evidence, not status#
"Step 2 completed" tells nobody anything. Write each finished step's detail from the step's own result: counts, names, IDs. "Matched 7 accounts, merged 2 duplicate refunds" is checkable; people trust runs they can check. Generate these lines in the step handler, where the numbers are, not with another model call.
Keeping it steady#
Runs update while people read. Keep step heights stable as statuses change, let the active step's detail appear without pushing finished steps around, and keep the page from jumping when a long run adds steps. If a run has more than about thirty steps, collapse finished groups ("12 steps done") and virtualize.
The waiting step#
When the run needs a person, the step itself turns to waiting and shows the approval card inside it. The person sees everything that led to the request directly above it, which is the context they need to decide. After the decision, the card folds into a receipt and the next step starts.
With PixilUI#
RunTimeline takes the steps in order; change a step's status and the timeline animates the change. Children render under a step, which is where the approval goes.
import { ApprovalCard } from "@/components/pixilui/approval-card";
import { RunTimeline, type RunTimelineStep } from "@/components/pixilui/run-timeline";
export function RefundRun({ steps, approve, reject }: { steps: RunTimelineStep[]; approve: () => Promise<void>; reject: (note: string) => Promise<void> }) {
const withApproval = steps.map((step) =>
step.id === "approve" && step.status === "waiting"
? {
...step,
children: (
<ApprovalCard
title="Send 7 apology emails and apply $105.00 in credits"
risk="medium"
items={[{ label: "Credits", value: "$105.00", numeric: true }]}
labels={{ approve: "Approve and send" }}
onApprove={approve}
onReject={reject}
/>
),
}
: step,
);
return <RunTimeline aria-label="Refund run 7f2e1c" steps={withApproval} />;
}
// steps: [{ id: "pull", title: "Pull stuck refunds from Stripe", status: "done", detail: "9 refunds pending for more than 72 hours", startedAt, endedAt }, ...]Install it with npx shadcn@latest add @pixilui/run-timeline. It brings the approval card with it.
When not to use this#
- Runs under a few seconds. A timeline that appears and completes instantly is noise. Show a thinking state.
- Unplanned, open-ended loops. If the agent does not know its steps in advance, list actions as they happen with tool call rows instead of pretending there is a plan.
- Many runs at once. To watch fifty runs, you need a runs list or inbox with status filters; a timeline is for one run.
- Developer tracing. Latency and cost per span belong in a trace waterfall.