WebberUI

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.

Loading preview…
npx shadcn@latest add https://webberui.com/r/ai-plan-steps.json

Playground

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.json

Or, 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

PropTypeDefaultDescription
stepsPlanStep[]Step data (with nested children); every state transition is driven by this data
titlestring"任務計畫" (Plan)Header title text; an empty string hides the title
toolIconsRecord<string, PlanToolIcon>Tool name → icon map, overriding built-in entries with the same key (use lowercase keys)
showProgressbooleantrueShow the completion ratio (x/y), thin progress bar and total elapsed time in the header
collapsiblebooleantrueWhether rows with a detail or sub-steps can be toggled; false keeps them always expanded with no chevron
defaultCollapsedbooleanfalseIn uncontrolled mode, whether collapsible rows start collapsed
expandedIdsstring[]Controlled list of expanded step ids; when provided the expansion state is fully managed outside
onExpandedChange(ids: string[]) => voidCallback when the expanded list changes; receives every expanded row id after the change
onStepClick(step: PlanStep) => voidCallback when any row is clicked (independent of toggling)
compactbooleanfalseCompact mode: smaller type, spacing and icons; the tool chip keeps only its icon
strikeDonebooleanfalseStrike through the titles of done steps
labelsPartial<AiPlanStepsLabels>Override UI strings (status text for screen readers, ratio wording, empty state, etc.)
classNamestringForwarded to the outermost container

PlanStep

FieldTypeDefaultDescription
idstringUnique id used for the React key, expansion state and aria-controls; stick to alphanumerics, - and _
titlestringStep title
status"pending" | "running" | "done" | "failed" | "skipped"Current status
toolstringTool name (e.g. read, bash); mapped to an icon via toolIcons and shown as a chip
detailstringExtra text shown when expanded (result summary, error message, etc.); rendered in red for failed steps
durationMsnumberElapsed time in milliseconds; when set it is shown at the end of the row and counted in the total
childrenPlanStep[]Sub-steps; rows with sub-steps or a detail get an expand chevron

AiPlanStepsLabels

FieldTypeDefaultDescription
pendingstring"待處理" (Pending)Screen-reader text for the pending status
runningstring"執行中" (Running)Screen-reader text for the running status, also used in the live region as "Running: …"
donestring"完成" (Done)Screen-reader text for the done status
failedstring"失敗" (Failed)Screen-reader text for the failed status
skippedstring"略過" (Skipped)Screen-reader text for the skipped status
progressstring"已完成" (completed)Wording after the completion ratio numbers
totalDurationstring"總耗時" (Total time)Accessible label for the total elapsed time
emptystring"尚無步驟" (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_fileread, web_searchsearch, longer keys win); anything unmatched gets a wrench icon. Pass your own map via toolIcons to 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 durationMs wins, 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; use expandedIds + onExpandedChange when you need to persist or sync it
  • Animation: status icons swap through AnimatePresence popLayout (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 section labelled by the title via aria-labelledby; lists use role="list" / role="listitem" (Safari drops list semantics for list-style: none, so the explicit roles keep them), and the running row carries aria-current="step"
  • Collapsible rows are native buttons (type="button") with aria-expanded and aria-controls pointing at the detail region; Tab / Enter / Space work from the keyboard and focus-visible shows a ring
  • Each title is followed by screen-reader-only status text ("(Running)" etc., translatable via labels); the header also has a role="status" aria-live="polite" summary that is announced whenever the ratio or the running steps change
  • The progress bar is a role="progressbar" with aria-valuenow set to the done count and aria-valuemax to 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

On this page