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 Contentstory of Chat Panel. It is the whole chat experience, not the panel, so it lives here next toEvent 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
aboveFieldslot. 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
hasScrollToLatestandhasContentFade, so the view follows the end only while the person is at the end. - Helper text and the Review dot are off.
SHOW_HELPER_TEXTisfalse, 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: anIdentityBlock(avatar label "Agent", "Zappy", "Pantheon AI").headerwithheaderInset='none': a summary row (StatusIndicator"3 awaiting review" with an arrow icon), context rows (twoTimelineItemsize='xs': "Reading: entire site" and "Editing:" with aTagand a subtle "Add page"Button), and aSegmentedButtonfor 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)
variantdefault on the empty state,compactin the footer; it takes focus when it moves to the footer.actionsStart: an icon-onlyMenuButton("Add photos & files").actionsEnd: a bareSelectas the model picker, anIconButtonfor voice, and Send, anIconButtonwithvariant='primary'(isWorkingwhile the agent works).fieldHeader:ChatAttachmentchips for dropped files.onFilesDropanddropLabelare 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
ChainOfThoughtwithexpandWhenActiveandisActivewhile working, labelled "Working..." then "Thought for N seconds" (collapsed). Its steps areTimelineItems withstatusactivefor the current one anddonefor earlier ones, some withsecondaryText. The Thinking State is inside the Chain of Thought. - The reply sits under it. A reply that proposes changes ends with a
SuggestionCard(statepending / accepted / dismissed) whose children are the Code diff recipe (CodeDiffSample). Accept, Refine and Dismiss areButtons inactionsStart; after Accept theFeedbackBaris inactionsEnd; after Dismiss, an UndoButtongets focus. - A plain reply gets a
FeedbackBarunderneath. 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 hiddenrole='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.mdand are tagged ready for dev; none is blocking): ChainOfThought (with ThinkingState inside it), ChatAttachment, ChatInput, ChatMessage (andChatMessageList), 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
FullScreenOverlayfor 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.mdhas 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.