← All proposals

Thinking State

A small inline status: the agent icon (Zappy by default), a label ("Thinking..."), and the elapsed time in the code font ("17.1s").

Net-new componentOpen in Storybook

proposals/ThinkingState/HANDOFF.md83 lines · View on GitHub

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 StatusIndicator type. StatusIndicator has no agent icon, animated label or elapsed time.
  • The consumer owns the timer. time is a formatted string, so formatting and ticking stay out of the component. showTimer turns it on or off.
  • The shimmer is a text-clipped gradient (secondary to default foreground, swept with a local thinking-state-shimmer keyframe) on the label text only. The PDS pds-ai-active-shimmer overlay was dropped because it showed as a lit box in dark mode. A grey pds-pulse chip 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.
  • state replaces a boolean: active, completed and stopped. A boolean could not tell finished from cut off. stopped is still and muted so it reads as not finished.
  • Sizes xs, s and m are named after the type tokens. The icon is the same height as the text, and the time is one step smaller (except at xs, 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 Icon component (through the local wrapper, for sparklesSolid).
  • Net-new: the whole component. See custom.md (class pds-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.
  • stopped is 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. completed shows the same text, still. A finished run is worded with the label and the timer, "Thought for" with time "4s" (decided 2026-10-21); Chain of Thought also collapses into something expandable.
  • Stopped icon. Does stopped need 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/ in pds-toolkit-react, beside StatusIndicator (the status category). Create it with npm run generate:component, then copy the files over.
  • Swap the local Icon wrapper for the package Icon once sparklesSolid exists there, and the local mergeClasses for 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).