Handoff: ThinkingState
What and why
While an agent works, the user needs to see that something is happening, what it is doing and how long it has been going. PDS has StatusIndicator (a dot or spinner with a label) and Spinner, but nothing that shows an agent's activity with an icon, a label and elapsed time. This is a net-new pattern: a small inline status with the agent icon (Zappy by default), a label ("Thinking...") and the elapsed time in the code font ("17.1s"). While active, the label shimmers and the icon pulses; when finished or stopped, the same text sits still.
- Figma / Jira: Figma page below under Preview; Jira none. Source was a designer screenshot.
- Context:
context.md
Exploration and decisions
- A new pattern, not a
StatusIndicatortype.StatusIndicatorhas no agent icon, animated label or elapsed time. - The consumer owns the timer.
timeis a formatted string, so formatting and ticking stay out of the component.showTimerturns it on or off. - The shimmer is a text-clipped gradient (secondary to default foreground, swept with a local
thinking-state-shimmerkeyframe) on the label text only. The PDSpds-ai-active-shimmeroverlay was dropped because it showed as a lit box in dark mode. A greypds-pulsechip was tried first and rejected, because it draws a skeleton behind a label with known content. - The icon pulses between 75% and 100% of its size, never larger, on the same duration as the shimmer.
statereplaces a boolean:active,completedandstopped. A boolean could not tell finished from cut off.stoppedis still and muted so it reads as not finished.- Sizes
xs,sandmare named after the type tokens. The icon is the same height as the text, and the time is one step smaller (except atxs, where the next step is too small to read).
API
| Prop | Type | Default | Purpose |
|---|---|---|---|
className |
string |
none | Additional class names. |
iconName |
IconProps['iconName'] |
'zappy' |
Icon before the label (sparklesSolid for other agents). |
label |
string |
required | What the agent is doing. Announced to screen readers. |
showTimer |
boolean |
false |
Show the elapsed time. Only renders when time is set. |
size |
'xs' | 's' | 'm' |
's' |
Text size; the icon, time and gap follow. |
state |
'active' | 'completed' | 'stopped' |
'active' |
active shimmers and pulses; completed is still; stopped is still and muted. |
time |
string |
none | Elapsed time, formatted by the consumer. Hidden from screen readers. |
Variants and states: three sizes; active, completed and stopped; with and without the timer; a live ticking timer (the LiveTimer story, demo only).
Tokens used
--pds-animation-duration-ai-shimmer--pds-color-foreground-default--pds-color-foreground-default-secondary--pds-spacing-2xs--pds-spacing-3xs--pds-spacing-xs--pds-typography-ff-code--pds-typography-ff-default--pds-typography-lh-m--pds-typography-size-m--pds-typography-size-s--pds-typography-size-xs
Impact
impact.md finds nothing that reaches existing PDS components: everything is scoped to pds-thinking-state. It does not list the proposals that build on this one, though: ChainOfThought renders it as its summary line, so changes to its sizes or states reach Chain of Thought.
Reused vs net-new
- Reused: PDS design tokens and the
Iconcomponent (through the local wrapper, forsparklesSolid). - Net-new: the whole component. See
custom.md(classpds-thinking-state, three--thinking-state-*variables, no raw values).
Accessibility
- The container is
role="status", so the label is announced when it appears. - The time is
aria-hidden, so a ticking value is not read out. Tabular numerals stop it jittering. - The shimmer and the icon pulse stop under
prefers-reduced-motion. stoppedis shown by muted color and the consumer's wording together ("Stopped after 17.1s"), so color is not the only cue.- The a11y panel has not been run on these stories. Run it in light and dark before the port.
Decisions and open questions
- Dark mode shimmer: settled. The PDS overlay showed as a lit box in dark, so the label uses a text-clipped gradient from the foreground tokens. The designer confirmed it reads right in dark mode (2026-10-19).
- Finished wording.
completedshows the same text, still. A finished run is worded with the label and the timer, "Thought for" withtime"4s" (decided 2026-10-21); Chain of Thought also collapses into something expandable. - Stopped icon. Does
stoppedneed a different icon (a stop mark) as well as the muted color? - Placement. Not decided: above the chat input, or in the message list.
Port notes
- Goes in
src/components/inpds-toolkit-react, besideStatusIndicator(the status category). Create it withnpm run generate:component, then copy the files over. - Swap the local
Iconwrapper for the packageIcononcesparklesSolidexists there, and the localmergeClassesfor the real one. - Port it before ChainOfThought, which depends on it.
Preview
npm run storybook, then Proposals/Thinking State.
Figma: https://www.figma.com/design/AMGWJbraJoi2PKWnxo29Ah/PDS-Proposals?node-id=318-43, page Thinking State (captured from Storybook, flat layers, light and dark).