Agent run
AIProA live timeline of an agent doing work: the goal, run status and elapsed time, ordered steps with tool calls, approvals and a final result.
npx prodular-ui add agent-runPart of Prodular UI Pro. Preview and read the docs freely, install with a license. See pricing
Live run
Steps run one after another, tool calls appear as they happen and the run stops for approval before it sends anything. Approve or reject, pause, stop or restart.
Chase overdue invoices for October
- Pull overdue invoices. Running0s
- Match against bank deposits. Queued
- Draft reminder emails. Queued
- Get approval to send. Queued
- Send 12 payment reminders. Queued
- Log activity in the CRM. Queued
"use client"
import * as React from "react"import { PauseIcon, PlayIcon, RotateCcwIcon, SquareIcon } from "lucide-react"
import { AiMark } from "@/components/ui/ai-mark"import { AgentRun, AgentRunActions, AgentRunElapsed, AgentRunHeader, AgentRunMeta, AgentRunResult, AgentRunStatus, AgentRunStep, AgentRunStepContent, AgentRunStepDescription, AgentRunSteps, AgentRunTitle, AgentRunToolCall, AgentRunToolCalls, type AgentRunStatusValue, type AgentStepStatus,} from "@/components/ui/agent-run"import { Approval, ApprovalActions, ApprovalHeader, ApprovalItem, ApprovalItems, ApprovalResolution, ApprovalRisk, ApprovalSection, ApprovalTitle,} from "@/components/ui/approval"import { Button } from "@/components/ui/button"
type Tool = { name: string; args: string; ms: number }type PlanStep = { title: string done: string ms: number tools?: Tool[] approval?: boolean}
const plan: PlanStep[] = [ { title: "Pull overdue invoices", done: "Found 14 invoices more than 30 days past due, $38,920 in total.", ms: 2200, tools: [{ name: "ledger.invoices.search", args: "status open, overdue over 30 days", ms: 1500 }], }, { title: "Match against bank deposits", done: "2 invoices were already paid but not reconciled. Left them out.", ms: 2600, tools: [ { name: "bank.transactions.list", args: "Sep 1 to Oct 20", ms: 1200 }, { name: "reconcile.match", args: "14 candidates", ms: 1000 }, ], }, { title: "Draft reminder emails", done: "Wrote 12 reminders, second notice tone, with the invoice PDF attached.", ms: 2400, }, { title: "Get approval to send", done: "", ms: 0, approval: true }, { title: "Send 12 payment reminders", done: "All 12 delivered. 3 opened in the first minute.", ms: 2000, tools: [{ name: "mail.send_batch", args: "12 messages", ms: 1600 }], }, { title: "Log activity in the CRM", done: "Added a collections note to each account.", ms: 1400, },]
const recipients = [ { name: "Northwind Studio", invoice: "INV-2041, 42 days", amount: "$4,180.00" }, { name: "Halcyon Freight", invoice: "INV-2017, 38 days", amount: "$7,950.00" }, { name: "Brightwater Dental", invoice: "INV-1996, 61 days", amount: "$2,310.00" },]
function clock() { return new Date().toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" })}
export default function AgentRunLive() { const [runStart, setRunStart] = React.useState(() => Date.now()) const [runEnd, setRunEnd] = React.useState<number | undefined>() const [index, setIndex] = React.useState(0) const [stepStart, setStepStart] = React.useState(() => Date.now()) const [durations, setDurations] = React.useState<number[]>([]) const [paused, setPaused] = React.useState(false) const [stopped, setStopped] = React.useState(false) const [decision, setDecision] = React.useState<{ status: "approved" | "rejected"; at: string } | null>(null) const [now, setNow] = React.useState(() => Date.now())
const finished = index >= plan.length const step = plan[index] const waiting = !finished && step.approval && !decision
React.useEffect(() => { if (finished || paused || stopped || step.approval) return const tick = setInterval(() => setNow(Date.now()), 150) const done = setTimeout(() => { setDurations((d) => [...d, Date.now() - stepStart]) setIndex((i) => i + 1) setStepStart(Date.now()) }, step.ms) return () => { clearInterval(tick) clearTimeout(done) } }, [finished, paused, stopped, step, stepStart])
React.useEffect(() => { if (finished && !runEnd) setRunEnd(Date.now()) }, [finished, runEnd])
const restart = () => { setRunStart(Date.now()) setRunEnd(undefined) setIndex(0) setStepStart(Date.now()) setDurations([]) setPaused(false) setStopped(false) setDecision(null) }
const decide = (status: "approved" | "rejected") => { setDecision({ status, at: clock() }) setDurations((d) => [...d, Date.now() - stepStart]) if (status === "rejected") { setStopped(true) setRunEnd(Date.now()) } else { setIndex((i) => i + 1) setStepStart(Date.now()) } }
const runStatus: AgentRunStatusValue = stopped ? "stopped" : finished ? "done" : waiting ? "waiting" : paused ? "paused" : "running"
const statusOf = (i: number): AgentStepStatus => { if (i < index) return "done" if (i > index) return stopped ? "skipped" : "queued" if (stopped) return plan[i].approval ? "failed" : "skipped" if (plan[i].approval) return "waiting" return paused ? "queued" : "running" }
return ( <div className="flex w-full items-start justify-center"> <AgentRun status={runStatus} className="w-full max-w-xl"> <AgentRunHeader> <AgentRunTitle>Chase overdue invoices for October</AgentRunTitle> <AgentRunMeta> <AgentRunStatus /> <AgentRunElapsed startedAt={runStart} endedAt={runEnd} /> <span> Step {Math.min(index + 1, plan.length)} of {plan.length} </span> </AgentRunMeta> <AgentRunActions> {!finished && !stopped && ( <> <Button variant="ghost" size="icon-sm" aria-label={paused ? "Resume run" : "Pause run"} disabled={waiting} onClick={() => { setPaused((p) => !p) setStepStart(Date.now()) }} > {paused ? <PlayIcon /> : <PauseIcon />} </Button> <Button variant="ghost" size="icon-sm" aria-label="Stop run" onClick={() => { setStopped(true) setRunEnd(Date.now()) }} > <SquareIcon className="size-3.5" /> </Button> </> )} <Button variant="outline" size="sm" onClick={restart}> <RotateCcwIcon data-icon="inline-start" /> Restart </Button> </AgentRunActions> </AgentRunHeader>
<AgentRunSteps> {plan.map((p, i) => { const status = statusOf(i) const elapsed = now - stepStart let offset = 0 const tools = (p.tools ?? []).flatMap((tool) => { const start = offset offset += tool.ms if (status === "done") return [{ ...tool, status: "done" as const }] if (status !== "running" || elapsed < start) return [] return [{ ...tool, status: elapsed < offset ? ("running" as const) : ("done" as const) }] }) return ( <AgentRunStep key={p.title} status={status} title={p.title} duration={durations[i]} startedAt={status === "running" ? stepStart : undefined} meta={ stopped && i === index && !p.approval ? "Stopped" : p.approval && decision?.status === "rejected" ? "Rejected" : p.approval && decision?.status === "approved" ? "Approved" : undefined } > {status === "done" && p.done && ( <AgentRunStepDescription>{p.done}</AgentRunStepDescription> )} {tools.length > 0 && ( <AgentRunToolCalls> {tools.map((tool) => ( <AgentRunToolCall key={tool.name} name={tool.name} status={tool.status} duration={tool.ms} > {tool.args} </AgentRunToolCall> ))} </AgentRunToolCalls> )} {p.approval && (status === "waiting" || decision) && ( <AgentRunStepContent> <Approval status={decision?.status ?? "pending"} risk="medium" > <ApprovalHeader> <ApprovalTitle className="text-sm">Send 12 payment reminders</ApprovalTitle> <ApprovalRisk /> </ApprovalHeader> {!decision && ( <ApprovalSection title="Recipients" action="$31,460.00 outstanding"> <ApprovalItems total={12}> {recipients.map((r) => ( <ApprovalItem key={r.name} title={r.name} description={r.invoice} value={r.amount} /> ))} </ApprovalItems> </ApprovalSection> )} {decision ? ( <ApprovalResolution by="Maya Chen" at={decision.at} reason={ decision.status === "rejected" ? "Hold until the November statement run." : undefined } /> ) : ( <ApprovalActions> <Button size="sm" onClick={() => decide("approved")}> Approve and send </Button> <Button size="sm" variant="outline"> Edit emails </Button> <Button size="sm" variant="ghost" onClick={() => decide("rejected")}> Reject </Button> </ApprovalActions> )} </Approval> </AgentRunStepContent> )} </AgentRunStep> ) })} </AgentRunSteps>
{finished && ( <AgentRunResult> <AiMark variant="inline" label="Result" /> <p className="text-sm leading-relaxed text-pretty"> 12 reminders sent for $31,460.00 outstanding. 2 invoices were reconciled and closed. I will check for replies and payments tomorrow at 09:00. </p> <div className="flex gap-2"> <Button size="sm" variant="outline"> View sent emails </Button> <Button size="sm" variant="ghost"> Open collections report </Button> </div> </AgentRunResult> )} </AgentRun> </div> )}Step states
Every step carries a status: done with its duration, failed with a retry, skipped, running with a live timer, waiting for approval and queued. Click a tool call with details to expand it.
Resolve ticket #48213: refund for a double charge
- Read the conversation. Done1.2s
Customer was charged twice for the Team plan on October 14.
- Find both charges. Done3.9s
- Check refund policy. Failed2.1s
The policy page timed out. Retried twice.
- Escalate to billing team. SkippedSkipped
Not needed, the duplicate is clear.
- Prepare refund of $480.00. Running0s
- Approve refund to Pinecrest Labs. Waiting for approvalNeeds approval
- Reply to the customer. Queued
"use client"
import * as React from "react"import { RotateCcwIcon, SquareIcon } from "lucide-react"
import { AgentRun, AgentRunActions, AgentRunElapsed, AgentRunHeader, AgentRunMeta, AgentRunStatus, AgentRunStep, AgentRunStepActions, AgentRunStepDescription, AgentRunSteps, AgentRunTitle, AgentRunToolCall, AgentRunToolCalls,} from "@/components/ui/agent-run"import { Button } from "@/components/ui/button"
export default function AgentRunStates() { const [startedAt] = React.useState(() => Date.now() - 84_000) const [stepStartedAt] = React.useState(() => Date.now() - 6_000)
return ( <div className="flex w-full items-start justify-center"> <AgentRun status="running" className="w-full max-w-xl"> <AgentRunHeader> <AgentRunTitle>Resolve ticket #48213: refund for a double charge</AgentRunTitle> <AgentRunMeta> <AgentRunStatus /> <AgentRunElapsed startedAt={startedAt} /> <span>Support agent</span> </AgentRunMeta> <AgentRunActions> <Button variant="ghost" size="icon-sm" aria-label="Stop run"> <SquareIcon className="size-3.5" /> </Button> </AgentRunActions> </AgentRunHeader> <AgentRunSteps> <AgentRunStep status="done" title="Read the conversation" duration={1240}> <AgentRunStepDescription> Customer was charged twice for the Team plan on October 14. </AgentRunStepDescription> </AgentRunStep> <AgentRunStep status="done" title="Find both charges" duration={3870}> <AgentRunToolCalls> <AgentRunToolCall name="billing.charges.list" duration={2140} details={`{ "customer": "cus_8fK2", "since": "2026-10-01" }\n\n2 results: ch_3Qa1 $480.00, ch_3Qa4 $480.00`} > customer Pinecrest Labs </AgentRunToolCall> <AgentRunToolCall name="billing.invoices.get" duration={910}> INV-7720 </AgentRunToolCall> </AgentRunToolCalls> </AgentRunStep> <AgentRunStep status="failed" title="Check refund policy" duration={2100}> <AgentRunStepDescription> The policy page timed out. Retried twice. </AgentRunStepDescription> <AgentRunStepActions> <Button size="xs" variant="outline"> <RotateCcwIcon data-icon="inline-start" /> Retry step </Button> </AgentRunStepActions> </AgentRunStep> <AgentRunStep status="skipped" title="Escalate to billing team"> <AgentRunStepDescription>Not needed, the duplicate is clear.</AgentRunStepDescription> </AgentRunStep> <AgentRunStep status="running" title="Prepare refund of $480.00" startedAt={stepStartedAt}> <AgentRunToolCalls> <AgentRunToolCall name="billing.refunds.preview" status="running"> ch_3Qa4 </AgentRunToolCall> </AgentRunToolCalls> </AgentRunStep> <AgentRunStep status="waiting" title="Approve refund to Pinecrest Labs" /> <AgentRunStep status="queued" title="Reply to the customer" /> </AgentRunSteps> </AgentRun> </div> )}Compact, in a table row
The status alone fits a table cell. Open it to see the compact run in a popover.
| Work order | Site | Technician | Agent |
|---|---|---|---|
| WO-3318 | Harbor Point, unit 4 | Ana Lucero | |
| WO-3319 | Elm Street Clinic | Marcus Hale | |
| WO-3320 | Riverside Depot | Jin Park |
"use client"
import * as React from "react"
import { AgentRun, AgentRunElapsed, AgentRunFooter, AgentRunHeader, AgentRunMeta, AgentRunStatus, AgentRunStep, AgentRunSteps, AgentRunTitle,} from "@/components/ui/agent-run"import { Button } from "@/components/ui/button"import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
const rows = [ { id: "WO-3318", site: "Harbor Point, unit 4", tech: "Ana Lucero", status: "running" as const }, { id: "WO-3319", site: "Elm Street Clinic", tech: "Marcus Hale", status: "waiting" as const }, { id: "WO-3320", site: "Riverside Depot", tech: "Jin Park", status: "done" as const },]
function RunDetails({ status, startedAt }: { status: "running" | "waiting" | "done"; startedAt: number }) { return ( <AgentRun status={status} size="compact" variant="plain"> <AgentRunHeader> <AgentRunTitle>Reschedule after parts delay</AgentRunTitle> <AgentRunMeta> <AgentRunStatus /> <AgentRunElapsed startedAt={startedAt} endedAt={status === "done" ? startedAt + 48_000 : undefined} /> </AgentRunMeta> </AgentRunHeader> <AgentRunSteps> <AgentRunStep status="done" title="Check part ETA" duration={1800} /> <AgentRunStep status="done" title="Find open slots this week" duration={2600} /> <AgentRunStep status={status === "done" ? "done" : status} title={status === "waiting" ? "Confirm Thursday 10:00 with customer" : "Offer Thursday 10:00"} startedAt={status === "running" ? startedAt + 40_000 : undefined} duration={status === "done" ? 3100 : undefined} /> <AgentRunStep status={status === "done" ? "done" : "queued"} title="Update the dispatch board" duration={900} /> </AgentRunSteps> <AgentRunFooter>Dispatch agent · Started by Priya Raman</AgentRunFooter> </AgentRun> )}
export default function AgentRunCompact() { const [startedAt] = React.useState(() => Date.now() - 52_000) return ( <div className="mx-auto w-full max-w-xl self-start overflow-hidden rounded-xl border bg-card shadow-xs"> <table className="w-full text-sm"> <thead> <tr className="border-b text-left text-[13px] text-muted-foreground"> <th className="px-4 py-2 font-normal">Work order</th> <th className="px-4 py-2 font-normal">Site</th> <th className="px-4 py-2 font-normal">Technician</th> <th className="px-4 py-2 text-right font-normal">Agent</th> </tr> </thead> <tbody className="divide-y"> {rows.map((row) => ( <tr key={row.id}> <td className="px-4 py-2.5 font-mono text-xs">{row.id}</td> <td className="px-4 py-2.5">{row.site}</td> <td className="px-4 py-2.5 text-muted-foreground">{row.tech}</td> <td className="px-4 py-1.5 text-right"> <Popover> <PopoverTrigger render={<Button variant="ghost" size="sm" className="-me-2 text-[13px] font-normal" />} > <AgentRunStatus status={row.status}> {row.status === "running" ? "Rescheduling" : row.status === "waiting" ? "Needs approval" : "Rescheduled"} </AgentRunStatus> </PopoverTrigger> <PopoverContent align="end" className="w-80 gap-0 py-0"> <RunDetails status={row.status} startedAt={startedAt} /> </PopoverContent> </Popover> </td> </tr> ))} </tbody> </table> </div> )}Anatomy
<AgentRun status="running"> <AgentRunHeader> <AgentRunTitle>Chase overdue invoices for October</AgentRunTitle> <AgentRunMeta> <AgentRunStatus /> <AgentRunElapsed startedAt={startedAt} /> </AgentRunMeta> <AgentRunActions>{/* Pause, Stop */}</AgentRunActions> </AgentRunHeader> <AgentRunSteps> <AgentRunStep status="done" title="Pull overdue invoices" duration={1400}> <AgentRunStepDescription>Found 14 invoices.</AgentRunStepDescription> <AgentRunToolCalls> <AgentRunToolCall name="ledger.invoices.search" duration={820} /> </AgentRunToolCalls> </AgentRunStep> <AgentRunStep status="running" title="Draft reminder emails" startedAt={stepStart} /> <AgentRunStep status="queued" title="Send reminders" /> </AgentRunSteps> <AgentRunResult>{/* summary and next actions */}</AgentRunResult></AgentRun>Guidance
Use it when
- Use it whenever an agent works for more than a couple of seconds or takes more than one action, so people can see what it is doing and stop it.
- Use the default size in a side panel or a page, and size="compact" with variant="plain" in popovers, table rows and narrow columns.
- Name steps by what the agent does, verb first: Pull overdue invoices, Draft reminder emails. Put what it found in AgentRunStepDescription once the step is done.
- Nest the tools a step called with AgentRunToolCalls, and pass details for raw input and output that people can expand when they need to audit it.
- Put an Approval inside the waiting step with AgentRunStepContent, so the decision sits where the run stopped.
Avoid
- Showing only a spinner and a final result. People trust agents they can watch and interrupt.
- Long technical step names or raw function names as titles. Tool names belong in AgentRunToolCall.
- More than one running step at the top level. Run parallel work as tool calls or nested steps under one running step.
- Putting the AgentRun card inside another card. Use variant="plain" when the surrounding surface already is a container.
Rules for AI agents
Published in llms-full.txt, the registry and the MCP server, so coding agents follow them when they build with this component.
- Give every AgentRunStep an explicit status on every render. Never infer it from position or leave a finished step as running.
- Keep exactly one top level step running at a time while the run is running. Set the run status to waiting while any step is waiting, and to done, failed or stopped when it ends.
- Pass startedAt to running steps and to AgentRunElapsed in the header so elapsed time is always visible. Pass duration when a step finishes and endedAt when the run ends.
- Any step that sends something outside the product, moves money, deletes data or cannot be undone must be preceded by a waiting step that renders an Approval. Do not perform the action until the person approves.
- Always render a Stop control in AgentRunActions while the run is running or waiting, and stop the underlying work when it is pressed. Mark remaining steps skipped.
- A failed step shows the reason in AgentRunStepDescription in plain language and offers Retry step in AgentRunStepActions. Never fail silently.
- Use AgentRunToolCall for every tool the agent calls inside a step, with the real tool name. Do not invent tool calls for decoration.
- End a finished run with AgentRunResult: what changed, with numbers, and the next action. Mark it with AiMark.
API
AgentRun
| Prop | Type | Default | Description |
|---|---|---|---|
| status | "queued" | "running" | "paused" | "waiting" | "done" | "failed" | "stopped" | "running" | Status of the whole run. Drives AgentRunStatus and aria-busy. |
| size | "default" | "compact" | "default" | compact tightens spacing and type for popovers and table rows. |
| variant | "card" | "plain" | "card" | plain drops the card chrome so it can sit in a popover, sheet or panel. |
AgentRunStatus
| Prop | Type | Default | Description |
|---|---|---|---|
| status | AgentRunStatus | Optional. Overrides the run status so the badge can be used on its own. |
AgentRunElapsed
| Prop | Type | Default | Description |
|---|---|---|---|
| startedAt | number | Epoch milliseconds. Ticks every second until endedAt is set. | |
| endedAt | number | Freezes the timer at the final duration. |
AgentRunStep
| Prop | Type | Default | Description |
|---|---|---|---|
| status | "queued" | "running" | "done" | "failed" | "skipped" | "waiting" | Required. Exposed as data-status on the step. | |
| title | ReactNode | Required. What the step does, verb first. | |
| startedAt | number | Running steps show a live elapsed time from here. | |
| duration | number | Milliseconds, shown on done and failed steps. | |
| meta | ReactNode | Replaces the text on the right of the title. |
AgentRunToolCall
| Prop | Type | Default | Description |
|---|---|---|---|
| name | string | Tool name in mono, for example ledger.invoices.search. | |
| status | "queued" | "running" | "done" | "failed" | "done" | Running tool calls shimmer and show a spinner. |
| details | ReactNode | Raw input and output. Makes the row expandable. |
Installation
The Prodular UI CLI adds the source to components/ui with everything it depends on. In a project that already uses shadcn/ui, the shadcn CLI installs it from the Prodular registry too. See Installation for the first setup.
npx prodular-ui add agent-run# or, with the registry in components.jsonnpx shadcn@latest add @prodular/agent-run