← All proposals

Chat with an agent

A recipe, not a component: one interactive story that composes the existing proposals into a chat with an agent, from the empty state to a conversation.

Extends a PDS componentOpen in Storybook

proposals/ChatWithAgent/HANDOFF.md121 lines · View on GitHub

Handoff: Chat with an agent (recipe)

What and why

The pieces of an AI chat (Chat Panel, Chat Input, Chat Message, Chain of Thought, Suggestion Card, Code Diff, Feedback Bar and more) are proposed one by one. This recipe shows them working together, from the empty state to a conversation, so the flow can be judged and the parts can be put together the same way. It is a recipe, not a component: a story and a Usage page that compose existing components. It has no API, class, token or CSS of its own that needs porting. There is nothing to port beyond the components it uses (below). The engineer copies the composition and keeps the story as documentation.

  • Figma / Jira: none. Source: the designer's chat reference screenshots and direction (2026-10-19). The wording and the scripted replies are made-up samples.
  • Context: context.md

Exploration and decisions

  • A recipe, moved out of Chat Panel. It started as the With Sample Content story of Chat Panel. It is the whole chat experience, not the panel, so it lives here next to Event log with agent.
  • The sample is scripted. Three exchanges repeat (a dependency check ending in a Suggestion Card with a Code Diff, a commit, a multidev), plus a fourth reply to Refine. Steps tick every 0.9 seconds and the thanks text fades back to the thumbs after 4.5 seconds. All timing and state live in the story, because the screen owns them, not a component.
  • Refine uses the Chat Input's aboveField slot. Context the next message applies to: a bar above the input reading "Refining: 2 changes (composer.json, pantheon.yml)" with a dismiss button. A branch bar that follows the agent's work was considered and not built (passive; the header already shows context).
  • Full screen and history removed. The expand button, full-screen layout and conversation list were dropped (designer: not needed for now). The chat is the drawer only. If full screen returns, use the PDS FullScreenOverlay.
  • Scrolling is the Chat Panel's. The story's own scroll-to-end code was replaced by hasScrollToLatest and hasContentFade, so the view follows the end only while the person is at the end.
  • Helper text and the Review dot are off. SHOW_HELPER_TEXT is false, and Review has no dot.

What it composes and how

There is no API. The composition is one Chat Panel docked as a drawer (size='m' default).

Chat Panel (ChatPanel)

  • title: an IdentityBlock (avatar label "Agent", "Zappy", "Pantheon AI").
  • header with headerInset='none': a summary row (StatusIndicator "3 awaiting review" with an arrow icon), context rows (two TimelineItem size='xs': "Reading: entire site" and "Editing:" with a Tag and a subtle "Add page" Button), and a SegmentedButton for Compose / Chat / Review. Only Chat is built; the Chat Panel does not know about the switch.
  • footer: absent on the empty state; the compact Chat Input once there is a conversation.
  • onFilesDrop + dropLabel, hasScrollToLatest + scrollToLatestLabel, hasContentFade.

Empty state: VerticalEmptyState (messages icon, heading, description), with the default-size Chat Input in the same centered group (a flex column, gap xl).

Chat Input (ChatInput, the same SampleComposer in both places)

  • variant default on the empty state, compact in the footer; it takes focus when it moves to the footer.
  • actionsStart: an icon-only MenuButton ("Add photos & files"). actionsEnd: a bare Select as the model picker, an IconButton for voice, and Send, an IconButton with variant='primary' (isWorking while the agent works).
  • fieldHeader: ChatAttachment chips for dropped files. onFilesDrop and dropLabel are given to the input too.
  • aboveField: the Refine context bar (below).
  • Enter sends, Shift+Enter adds a line (textareaProps.onKeyDown).

The agent's turn (ChatMessageList of ChatMessages; the user's message sender='user')

  • A ChainOfThought with expandWhenActive and isActive while working, labelled "Working..." then "Thought for N seconds" (collapsed). Its steps are TimelineItems with status active for the current one and done for earlier ones, some with secondaryText. The Thinking State is inside the Chain of Thought.
  • The reply sits under it. A reply that proposes changes ends with a SuggestionCard (state pending / accepted / dismissed) whose children are the Code diff recipe (CodeDiffSample). Accept, Refine and Dismiss are Buttons in actionsStart; after Accept the FeedbackBar is in actionsEnd; after Dismiss, an Undo Button gets focus.
  • A plain reply gets a FeedbackBar underneath. The thanks text (statusLabel) replaces the thumbs for a few seconds, then the thumbs return with the choice pressed.

Screen decisions the recipe makes

  • The input moves from the empty state to the footer on the first send.
  • Chain of Thought is open while working and collapsed when done.
  • Refine puts the context bar in aboveField, moves focus to the input and announces "Refining <title>" in a visually hidden role='status' region. Sending clears the bar, adds a "(Refining: <title>)" line to the user's message, and the agent replies with a narrowed Suggestion Card. Dismissing the bar is a button labelled "Stop refining".
  • Which replies get thumbs, and the timing of the thanks text.

What the screen owns (not a component): the turns and the script, step timing, the model choice, the dropped files, the Refine state, the Suggestion Card's accepted/dismissed state, the feedback choice and thanks timing, and moving focus.

Story-only CSS (chat-with-agent-story.css, see custom.md): the frame (full height, drawer shadow), the summary and rows spacing, the header rule above the view switch, the empty-state group and its overrides, the reply spacing and the Refine bar. Overrides of other components, each an open question for that component (below): the Segmented Button stretched to full width, the Tag in the Badge's silver-muted colors, the "Add page" Button 24px high, and the empty state heading in Heading XL with a Copy XS secondary description.

Tokens used

Story-only CSS (the recipe has no component CSS), so these are for the story's own layout, not for porting:

  • --pds-border-width-default
  • --pds-color-badge-silver-muted-background
  • --pds-color-badge-silver-muted-foreground
  • --pds-color-border-default-secondary
  • --pds-color-foreground-default
  • --pds-color-foreground-default-secondary
  • --pds-color-surface-default-secondary
  • --pds-elevation-drawer-right
  • --pds-spacing-2xl
  • --pds-spacing-2xs
  • --pds-spacing-s
  • --pds-spacing-xl
  • --pds-spacing-xs
  • --pds-typography-size-xl

Custom CSS variables: none of its own. --button-height, --tag-bg and --tag-fg are other components' variables, overridden in the story. Net-new classes: none. Raw values kept: none. npm run check:tokens -- ChatWithAgent: 0 problems.

Impact

From impact.md (npm run check:impact -- --write ChatWithAgent):

  • Nothing reaches existing components. Its CSS is scoped to its own chat-with-agent-story__* classes.
  • Opt-in overrides only inside the story: Empty State heading and text, Segmented Button width, Icon margin, Button height, Tag colors.
  • Does not redefine any --pds-* token.
  • No other proposal imports or uses it.
  • It depends on the components below, and changes to them change what the story shows.

Reused vs net-new

  • Reused, from the PDS package: FlexContainer, SegmentedButton, StatusIndicator, Tag, VerticalEmptyState.
  • Reused, from other proposals (all have a HANDOFF.md and are tagged ready for dev; none is blocking): ChainOfThought (with ThinkingState inside it), ChatAttachment, ChatInput, ChatMessage (and ChatMessageList), ChatPanel, the Code diff recipe, FeedbackBar, Icon, IconButton (variant='primary' is Send), IdentityBlock, Select (variant='bare'), SuggestionCard, Timeline (TimelineItem). Each handoff is ../<Name>/HANDOFF.md.
  • Net-new: none. See custom.md.

Accessibility

a11y panel not run; light and dark not checked by eye. What the code does:

  • Keyboard and focus come from the components. Enter sends, Shift+Enter adds a line.
  • On the first send the input moves to the footer and takes focus (autoFocus).
  • Refine moves focus to the input. The context bar is announced through a visually hidden role='status' region ("Refining <title>"), and its dismiss button has the label "Stop refining".
  • After Dismiss on a Suggestion Card, focus moves to Undo; after Undo, to Dismiss.
  • The Feedback Bar has a group label and thumb labels ("Rate this response", "Good response", "Bad response"); the thanks text is its statusLabel.
  • Dropped files have a drop overlay label, and each chip has a "Remove <name>" label.
  • The Chain of Thought is native details/summary. The Scroll to latest button has a label.

Decisions and open questions

  • Is the Compose / Chat / Review switch meant to change the content? Only Chat is built.
  • Which story-only overrides should become real options on their components: a full-width Segmented Button, a neutral Tag color, a smaller Button than 28px, and an empty-state heading size?
  • If full screen returns, use the PDS FullScreenOverlay for the overlay and a link or route for the embedded page.
  • Sample wording, scripts and timings are made up, and the story is not checked in dark.

Port notes

  • Nothing to generate and no folder in pds-toolkit-react. Port the components above first (../../docs/PORT-ORDER.md has the recommended order), then keep the story and Usage page as documentation of the composition.
  • The engineer copies the composition: the props and slots above, and the screen decisions. The scripts, timers and sample text are story-only.

Preview

npm run storybook, then Recipes/Chat with an agent (story Interactive; Usage page Usage). Figma: none.