Chain of Thought
A visible record of an AI agent's work: steps joined by a rail, each with an icon, a label, an optional query, a result count and its sources, plus a live Working line.
Press Replay to watch a run. A step you append fades in and its rail draws itself; its results land (the count ticks up and the sources fan out); the next step starts; and finishing sweeps the rail down to a check. It only displays: you stream the steps in as your agent works.
Basic
A ChainOfThought holds ChainOfThoughtSteps. With status="done" and the final elapsed it ends with "Worked for 11.2s".
- Done: Planning the search3 angles
- Done: Searched X for
design system pricing7 posts - Done: Searched Reddit for
r/reactjs worth paying for12 threads - Done: Reading the strongest three threads
Step states
Done steps read muted and the active one reads in full, so the eye lands on what is happening now. A pending step's icon is grey until it starts. Screen readers get each step's status.
- Done: Searched X for
design system pricing7 posts - In progress: Searched Reddit for
r/reactjs worth paying for12 threads - Pending: Searching Hacker News for
design system
Logos, emoji and icons
Make each step recognisable. Use the real logo of a site the agent searched, in its brand colour (sourceLogo('reddit')); an emoji for steps with no site (💡 planning, 📄 reading); or any icon. A logo whose brand colour is black (X, GitHub, Notion, Medium, Wikipedia) is drawn in the text colour, so it stays visible in dark mode.
- Done: Searched Google for
design tokens - Done: Planning the search
- Done: Reading the docs
<SourceLogo name="reddit" />Sources
A SourceStack of overlapping site chips shows where a step's results came from. Add onSourcesClick to make the row a button with a chevron.
- Done: Searched Google for
design system pricing 202610 results
Working indicator
The line under the steps: a sparkle and a shimmering "Working" with the elapsed time, or a check and "Worked for". It is also exported on its own. Pass startedAt and the time counts up by itself.
Controlled
Closed by default here. Pass open and onOpenChange to own the state.
Open: false
Streaming from an agent
Append a step when a tool call starts, give it its count when the results land, mark only the last step active, and give the container status="working" with startedAt. When the run ends, switch to status="done" and the final elapsed.
// the agent's own tool calls map straight onto steps
for await (const event of agent.stream(prompt)) {
if (event.type === 'tool_start')
setSteps((s) => [...s, { id: event.id, icon: iconFor(event.tool), label: labelFor(event), query: event.input }]);
if (event.type === 'tool_result')
setSteps((s) => s.map((x) => (x.id === event.id ? { ...x, count: `${event.results.length} results` } : x)));
}
setDone(true);Source logos
17 sites. Logos are trademarks of their owners and appear only to name where a result came from.
Motion
| Moment | What happens |
|---|---|
| A step arrives | Its row opens while the previous rail flows down into it and turns the corner; the icon lands, the label is written in, the query types itself, the count ticks up and the source chips fan out. |
| The active step | A spark runs down the rail into it (in the logo's brand colour) and its icon beats as it lands. |
| Pending to active | A pending step's icon is grey; it fades into colour when the step starts. |
| Working | The label shimmers while the sparkle beside it, in the brand colour, turns and twinkles on the same beat. |
| Finished | One sweep runs down the rail lighting each icon, then the check is drawn. |
| Open and close | The chevron turns with a spring and the steps rise in turn. |
| Reduced motion | Everything is instant: no spark, shimmer, sweep or tick-up. |
Only what arrives after the first render animates, so a finished run you reload does not replay.
Accessibility
- The header is a button with
aria-expandedandaria-controls; steps are a list, and each one announces its status ("Done:", "In progress:", "Pending:"). - The active step has
aria-current="step". The Working line is a live region; its ticking time is kept out of it. - Text uses the muted foreground, which meets 4.5:1 on the surface in light and dark; only decorative icons are lighter.
- Logos and emoji are decorative next to a text label; give a
SourceStacksource alabelso it is read out. - Reduced motion makes every animation instant.
ChainOfThought props
| Prop | Type | Default | Description |
|---|---|---|---|
| title | ReactNode | The header text, e.g. "Ran 3 searches" or "Thinking". | |
| icon | IconComponent | search icon | The header icon, drawn at 16px. An icon, an emoji or a logo. |
| open / defaultOpen | boolean | true | Controlled or initial open state. The header toggles it. |
| onOpenChange | (open) => void | Called with the new state when the header is clicked. | |
| status | "working" | "done" | Adds the working line under the steps: "working" (sparkle, shimmer) or "done" ("Worked for 11.2s"). Omit it for no line. | |
| elapsed | number | Elapsed seconds for the working line. | |
| startedAt | number | Epoch ms when the work started; the working line then counts up on its own. | |
| children | ReactNode | ChainOfThoughtStep elements. |
ChainOfThoughtStep props
| Prop | Type | Default | Description |
|---|---|---|---|
| status | "pending" | "active" | "done" | done | pending (not started, grey), active (in progress, read in full) or done (muted). |
| icon | IconComponent | search icon | The step's icon, drawn at 16px: a Sakani icon, an emoji or sourceLogo('reddit'). |
| label | ReactNode | What happened, e.g. "Searched X for". | |
| query | string | The query or argument, shown in monospace after the label. | |
| count | ReactNode | A result count on the right, e.g. "7 posts". A leading number ticks up when it arrives. | |
| accent | string | Colour of the spark that runs into the step while it is active. Defaults to the icon's brand colour when it is a sourceLogo. | |
| sources | ReactNode | A row under the step, usually a <SourceStack>. | |
| sourcesLabel | string | Sources | Text before the sources. |
| onSourcesClick | () => void | Makes the sources row a button with a chevron. |
SourceStack props
| Prop | Type | Default | Description |
|---|---|---|---|
| sources | { label, icon? }[] | The sources, in order. Three to five read best. `label` is read out and shown as a tooltip; `icon` defaults to a globe. |
WorkingIndicator props
| Prop | Type | Default | Description |
|---|---|---|---|
| status | "working" | "done" | working | "working" shows the sparkle and a shimmering label; "done" shows a check. |
| label | string | Replaces the default label ("Working" / "Worked for"). | |
| elapsed | number | Elapsed seconds. Takes precedence over startedAt. | |
| startedAt | number | Epoch ms when the work started; while "working" the time ticks on its own. |
SourceLogo props
| Prop | Type | Default | Description |
|---|---|---|---|
| name | SourceLogoName | Which site (see the list above). | |
| size | number | string | 16 | Rendered size: px, or any CSS length. |
| title | string | Accessible name. Without it the logo is decorative, which is right next to a text label. |
Why it works this way
Why
- It is built for an agent that streams: append a step when a tool call starts, give it a count when the results land, and the rail, counts and source chips animate in by themselves. It only displays, so you keep the run's state.
- Each step can carry the real logo of the site it searched, in its brand colour, or an emoji, so a long run is scannable at a glance. The Working line and the finishing check are part of it.
Don't
- Don't use Stepper for an agent run. A Stepper is a progress bar for a known sequence; a Chain of Thought grows as the agent works.
- Don't use ActivityFeed for it. That is a log of past events.
- Don't mark several steps active. Only the step in progress reads in full.
- Don't use a generic magnifier on every step. Give each step its site's logo or an emoji.