AI Plan Steps (React)
A React component for agent plan tracking, driven purely by data — steps flow through pending → running → done / failed / skipped with tool-icon chips, nested sub-steps joined by a connector line, a breathing pulse on the active step, a completion ratio and total elapsed time; status changes swap icons via AnimatePresence and new steps slide in from below.
The todo tracker you see in agent products: hand it a steps array and each row renders by status — an empty circle for pending, a spinning ring (with the whole row breathing) for running, a check that springs in for done, an X for failed, a dashed circle for skipped. Next to the title sit a tool chip (tool names map to built-in lucide icons, overridable via toolIcons) and the elapsed time; rows with a detail or sub-steps expand, and sub-steps are indented and joined by a connector line on the left. The header shows the completion ratio x/y, a thin three-segment progress bar (done / failed / skipped) and the total elapsed time. The component runs no timers of its own — every state comes from your data, so whatever the agent reports is what gets drawn; status changes swap icons with AnimatePresence, and newly added steps slide in from below.
npx shadcn@latest add https://webberui.com/r/ai-plan-steps.jsonPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<AiPlanSteps />
Installation
npx shadcn@latest add https://webberui.com/r/ai-plan-steps.jsonOr, once registries are configured in components.json, install it as @webberui/ai-plan-steps.
Usage
import { AiPlanSteps, type PlanStep } from "@/components/ui/ai-plan-steps";
const steps: PlanStep[] = [
{
id: "audit",
title: "Audit the current login flow",
status: "done",
tool: "read",
durationMs: 1050,
children: [
{ id: "audit-read", title: "Read the auth/ directory", status: "done", tool: "read", durationMs: 640 },
{ id: "audit-grep", title: "Flag duplicated validation logic", status: "done", tool: "grep", durationMs: 410 },
],
},
{ id: "extract", title: "Extract a shared validateCredentials()", status: "running", tool: "edit" },
{ id: "test", title: "Run unit tests", status: "pending", tool: "test" },
];
<AiPlanSteps
steps={steps}
title="Plan"
onStepClick={(step) => console.log(step.id)}
/>Progress is driven by you updating the steps array (for example, one setState per agent report); the component only turns the difference into animation.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
steps | PlanStep[] | — | Step data (with nested children); every state transition is driven by this data |
title | string | "任務計畫" (Plan) | Header title text; an empty string hides the title |
toolIcons | Record<string, PlanToolIcon> | — | Tool name → icon map, overriding built-in entries with the same key (use lowercase keys) |
showProgress | boolean | true | Show the completion ratio (x/y), thin progress bar and total elapsed time in the header |
collapsible | boolean | true | Whether rows with a detail or sub-steps can be toggled; false keeps them always expanded with no chevron |
defaultCollapsed | boolean | false | In uncontrolled mode, whether collapsible rows start collapsed |
expandedIds | string[] | — | Controlled list of expanded step ids; when provided the expansion state is fully managed outside |
onExpandedChange | (ids: string[]) => void | — | Callback when the expanded list changes; receives every expanded row id after the change |
onStepClick | (step: PlanStep) => void | — | Callback when any row is clicked (independent of toggling) |
compact | boolean | false | Compact mode: smaller type, spacing and icons; the tool chip keeps only its icon |
strikeDone | boolean | false | Strike through the titles of done steps |
labels | Partial<AiPlanStepsLabels> | — | Override UI strings (status text for screen readers, ratio wording, empty state, etc.) |
className | string | — | Forwarded to the outermost container |
PlanStep
| Field | Type | Default | Description |
|---|---|---|---|
id | string | — | Unique id used for the React key, expansion state and aria-controls; stick to alphanumerics, - and _ |
title | string | — | Step title |
status | "pending" | "running" | "done" | "failed" | "skipped" | — | Current status |
tool | string | — | Tool name (e.g. read, bash); mapped to an icon via toolIcons and shown as a chip |
detail | string | — | Extra text shown when expanded (result summary, error message, etc.); rendered in red for failed steps |
durationMs | number | — | Elapsed time in milliseconds; when set it is shown at the end of the row and counted in the total |
children | PlanStep[] | — | Sub-steps; rows with sub-steps or a detail get an expand chevron |
AiPlanStepsLabels
| Field | Type | Default | Description |
|---|---|---|---|
pending | string | "待處理" (Pending) | Screen-reader text for the pending status |
running | string | "執行中" (Running) | Screen-reader text for the running status, also used in the live region as "Running: …" |
done | string | "完成" (Done) | Screen-reader text for the done status |
failed | string | "失敗" (Failed) | Screen-reader text for the failed status |
skipped | string | "略過" (Skipped) | Screen-reader text for the skipped status |
progress | string | "已完成" (completed) | Wording after the completion ratio numbers |
totalDuration | string | "總耗時" (Total time) | Accessible label for the total elapsed time |
empty | string | "尚無步驟" (No steps yet) | Text shown when there are no steps |
summarizePlanSteps(steps) (returns per-status counts over the flattened tree plus the total elapsed time), formatPlanDuration(ms) (820ms / 1.4s / 12s / 1m 05s), and the PlanStep, PlanStepStatus, PlanToolIcon, PlanSummary, AiPlanStepsLabels and AiPlanStepsProps types are also named exports.
How it works
- Built-in tool icon map:
read,write,edit,search,grep,glob,bash,shell,terminal,web,fetch,browse,db,database,sql,git,test,build,deploy,code,think,plan. Lookup first tries the whole lowercased name, then falls back to a "contains keyword" match (read_file→read,web_search→search, longer keys win); anything unmatched gets a wrench icon. Pass your own map viatoolIconsto override or extend it - Ratio and total time: counts are computed over the flattened tree (sub-steps at every level included); for total time, a parent with its own
durationMswins, otherwise its children are summed, so parent and children are never double-counted - Expansion state: uncontrolled mode only records rows the user has toggled and infers the rest from
defaultCollapsed, so steps added later follow the same default; useexpandedIds+onExpandedChangewhen you need to persist or sync it - Animation: status icons swap through
AnimatePresencepopLayout (the done check springs in), the running row breathes with a faint blue overlay, progress bar widths ease, new rows slide in from below with a layout position animation and removed rows fade out while the rest close the gap; the component itself has no setTimeout / setInterval
Accessibility
- The root is a
sectionlabelled by the title viaaria-labelledby; lists userole="list"/role="listitem"(Safari drops list semantics forlist-style: none, so the explicit roles keep them), and the running row carriesaria-current="step" - Collapsible rows are native
buttons (type="button") witharia-expandedandaria-controlspointing at the detail region; Tab / Enter / Space work from the keyboard andfocus-visibleshows a ring - Each title is followed by screen-reader-only status text ("(Running)" etc., translatable via
labels); the header also has arole="status"aria-live="polite"summary that is announced whenever the ratio or the running steps change - The progress bar is a
role="progressbar"witharia-valuenowset to the done count andaria-valuemaxto the total number of steps - When the user has "reduce motion" enabled at the system level, the spin, breathing, spring-in, slide-in and layout-shift animations are disabled and status changes keep only fades and color changes
AI Diff Suggestion Card
AI suggestions presented as an inline diff — deletions struck through in red, additions in green, accepted or rejected one by one.
AI Model Arena (React)
An arena-style side-by-side blind test React component: two anonymous responses stream in sync, you vote A / B / tie / both bad, then the cards flip to reveal the model names and provider color blocks, crown the winner, and roll the scoreboard numbers.