Handoff: ChainOfThought
What and why
A user needs to see what an agent did, and open it up when they want the detail: its reasoning, a web search, tool calls, or plain progress. PDS has nothing for this. This proposal is a collapsible record: a summary line (a Thinking State with a chevron, such as "Thought for 4 seconds") over a list of steps. Each step is a TimelineItem: an optional status icon, a label, quiet secondary text, optional expandable detail, and a slot for any content under it (a log block, file tags, a table). The list itself is a Timeline without the rail.
Chain of Thought builds on two other proposals. It renders the summary with ThinkingState, its body is a Timeline and its rows are TimelineItem (../Timeline/HANDOFF.md, written 2026-10-16; AgentStep was renamed to TimelineItem). Timeline and TimelineItem must port with it; the row's API is in that handoff.
- Figma / Jira: none. Sources were designer screenshots: a tool-call log, a reasoning-and-tools trace, and an agent investigating a failed build.
- Context:
context.md
Exploration and decisions
variant:steps(default),reasoning,searchandcoding. It sets the body:stepsis rows with status icons;reasoningis running text;searchis a quiet query row with result rows (a bold title and the domain as secondary text) and a quiet "+N more" row;codingis rows with a bold verb, code-font secondary text and+/-diff counts. The rows areTimelineItem, so the body is the consumer's content.- Built on native
<details>, notExpansionPanel.ExpansionPanelis a bordered card with a space-between chevron. The references have a borderless, left-aligned summary with the chevron beside the label and a rule down the left of the steps. - The summary is rendered by Chain of Thought from
label,isActive,time,showTimerandiconName, so the size hierarchy is enforced. The cost is that there is no custom summary. expandWhenActiveopens the list whileisActiveis true and closes it when the agent finishes. The first time the user opens or closes it, the automatic behavior stops for that instance.size:sis a 14px summary over 12px rows;mis 16px over 14px. Rows read the size from the list (Timeline'ssize, or apds-copy-*class on the reasoning list).- The body is a
Timeline. Chain of Thought rendersTimeline(no rail) for the list variants, so list semantics belong to the list and a step also works alone inline. It keeps its own rule down the left of the steps. Steps are children, not a data array. - Content under a step is a slot (
childrenon TimelineItem), always visible and lined up with the label.detailstays for content that starts collapsed. In the example, a finished step keeps its output indetail(collapsed) and the active step shows its content open. - File tags under a step are PDS
Tag(tagColor='slate',size='xs'), not Badge: they are objects, not status, and Tag can link and truncate.
API
ChainOfThought
| Prop | Type | Default | Purpose |
|---|---|---|---|
children |
ReactNode |
none | The body. TimelineItem components for the list variants (each is wrapped in a list item), or paragraphs for reasoning. |
className |
string |
none | Additional class names. |
expandWhenActive |
boolean |
false |
Open while isActive is true and close when it turns false, until the user toggles it. |
iconName |
IconProps['iconName'] |
'zappy' |
Icon before the summary label. |
isActive |
boolean |
false |
Agent still working: shimmers the label and pulses the icon. |
isOpen |
boolean |
false |
Initial open state. |
label |
string |
required | The always-visible line that toggles the list. |
showTimer |
boolean |
false |
Show the elapsed time. Only renders with time. |
size |
's' | 'm' |
's' |
Size of the whole block. |
time |
string |
none | Elapsed time, formatted by the consumer. |
variant |
'coding' | 'reasoning' | 'search' | 'steps' |
'steps' |
What the list shows. |
The row props (TimelineItem) are in ../Timeline/HANDOFF.md.
Variants and states: steps, reasoning, search and coding; sizes s and m; active, completed and expanded or collapsed; steps with detail, content, secondary text and diff counts; all four step statuses.
Tokens used
Chain of Thought:
--pds-animation-transition-rotation--pds-border-radius-default--pds-border-width-default--pds-border-width-outline--pds-color-border-default--pds-color-foreground-default--pds-color-foreground-default-secondary--pds-color-interactive-focus--pds-spacing-2xs--pds-spacing-s--pds-spacing-xs--pds-typography-ff-default--pds-typography-fw-medium--pds-typography-fw-regular--pds-typography-lh-m--pds-typography-size-s--pds-typography-size-xs
Timeline and TimelineItem add the status and diff colors and the code font; see ../Timeline/HANDOFF.md. Custom variables here: --chain-of-thought-indent, --timeline-gap (set on the list).
Impact
From impact.md: nothing reaches existing PDS components; everything is scoped to pds-chain-of-thought. It depends on ThinkingState, Timeline, TimelineItem, the local Icon and Spinner (the active step uses a monochrome spinner). The example stories use the package's CodeBlock, Tag and FlexContainer.
Reused vs net-new
- Reused:
ThinkingState(summary),TimelineandTimelineItem(body),Icon,Spinner, and in the storiesCodeBlock,TagandFlexContainer. - Net-new: Chain of Thought (and, through its body, Timeline and TimelineItem; see their handoff). See
custom.md.
Accessibility
- The summary is a
summaryinsidedetails, so Enter and Space toggle it natively, and it takes the visible focus ring (--pds-color-interactive-focus). The state is mirrored in React so the chevron rotates. - The list is a
Timeline(anol), so a screen reader announces the number of steps. - Each step's status is announced as visually-hidden text (
labelStrings, translatable); the active spinner has its own label. - Shimmer and pulse on the summary stop under
prefers-reduced-motion. - The a11y panel was clean on the Event log with agent story, which includes a Chain of Thought, in light. It has not been run on the Chain of Thought stories themselves, nor in dark. Run it in both before the port.
Decisions and open questions
- Reasoning text. Plain paragraphs now; italic, links and markdown are not handled.
- Motion. Steps appear instantly when added. Does a new step animate in?
- Long lists. No max-height or scroll; decide when steps can run to dozens.
- Result favicons. Search results use a
globeicon; the reference shows each site's favicon. Aleadingslot on TimelineItem would take an image. - "+7 more" is a plain quiet row. Should it be a link or button that expands the rest?
- Custom icon on a done step replaces the check, so completion shows only by color and hidden text. Keep a small check?
- Content when a step finishes. Should content under a step collapse when it is done? The consumer decides now.
- Not built: a larger, bolder step style and a Chain of Thought header with a metadata line ("Investigation started..."), both from the investigation screenshot.
- Stopped runs. Should a stopped run pass through to the summary (
stoppedstate)? - The dark-mode shimmer question from ThinkingState is settled (text-clipped gradient, confirmed 2026-10-19); it applies here unchanged.
Port notes
- New components. Where they live is the engineer's call, but a reasonable home is
src/components/for Chain of Thought with Timeline beside it. Usenpm run generate:componentfor each, then copy the files over. - Port ThinkingState first, then Timeline and TimelineItem, then Chain of Thought. Swap the local
Iconwrapper andmergeClassesfor the real ones, and port the Spinner proposal for the step spinner (ready:../Spinner/HANDOFF.md). The order is indocs/PORT-ORDER.md.
Preview
npm run storybook, then Component Proposals/Chain of Thought (rows: Component Proposals/Timeline/Timeline Item).
Figma: https://www.figma.com/design/AMGWJbraJoi2PKWnxo29Ah/PDS-Proposals?node-id=374-45, page Chain of Thought (6 stories, light only; captures of Storybook with library icons swapped in, sent 2026-10-20).