- Replace generic "Save"/"Cancel" with "Save edit"/"Discard edit" in inline edit controls - Add solution path to agent load error: "Could not load agents. Refresh to try again." - Eliminate 12px font size by using 13px + text-muted-foreground for slash command descriptions (4 sizes total: 11/13/15/20px) - Replace gap-1.5 (6px) with gap-2 (8px) in identity bar — multiples-of-4 compliance - Add theming note on bg-cyan-400 semantic token promotion path Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
26 KiB
| phase | slug | status | shadcn_initialized | preset | created |
|---|---|---|---|---|---|
| 22 | agent-streaming | draft | true | new-york / neutral / css-variables | 2026-04-01 |
Phase 22 — UI Design Contract
Visual and interaction contract for Phase 22: Agent Streaming. Generated by gsd-ui-researcher. Verified by gsd-ui-checker.
Design System
| Property | Value | Source |
|---|---|---|
| Tool | shadcn/ui | ui/components.json — unchanged from Phase 21 |
| Style | new-york | ui/components.json |
| Base color | neutral | ui/components.json |
| CSS variables | true | ui/components.json |
| Component library | Radix UI (via shadcn new-york) | ui/components.json |
| Icon library | lucide-react ^0.574.0 | ui/components.json |
| Font | System UI (font-sans, inherited) |
ui/src/index.css |
Existing shadcn components available (no install needed):
avatar, badge, button, card, checkbox, collapsible, command, dialog, dropdown-menu, input, label, popover, scroll-area, select, separator, sheet, skeleton, tabs, textarea, tooltip
Existing custom components to reuse/extend:
ChatPanel.tsx— extend with agent selector header area and streaming state; do not restructure the two-column layoutChatMessage.tsx— extend props to acceptagentId,agentName,agentIconfor identity renderingChatMessageList.tsx— replace with virtualized list (@tanstack/react-virtual) for PERF-03ChatInput.tsx— extend with slash command popover and @mention popover (reuseMentionOptionpattern fromMarkdownEditor.tsx)AgentIcon(fromAgentIconPicker.tsx) — reuse directly for agent avatar rendering in messagesagentStatusDot(fromstatus-colors.ts) — reuserunning: "bg-cyan-400 animate-pulse"for streaming indicator
New package required:
@tanstack/react-virtual— not currently installed; add toui/package.jsonfor PERF-03 (1,000+ message virtualization)
Layout Contract
Layout Unchanged
The overall layout established in Phase 21 is unchanged:
[ CompanyRail ] [ Sidebar ] [ <main> ] [ ChatPanel ] [ PropertiesPanel ]
Phase 22 adds new UI surfaces inside ChatPanel only. No layout-level changes.
ChatPanel Header — Agent Selector
The ChatPanel header row gains an agent selector to the left of the close button:
[ "Chat" label ] [ AgentSelector dropdown ] [ ─── spacer ─── ] [ X close ]
- The
AgentSelectoris a<Select>or<Popover>trigger showing the active agent's icon + name - Width: fit-content, max 120px, truncated with ellipsis
- Placement:
flex items-center gap-2row, samepx-4 py-2padding as Phase 21 header - When no agent is selected, label reads "Select agent" in
text-muted-foreground
Message Bubble — Agent Identity Bar
Every assistant message gains an identity bar above the message content:
[ AgentIcon 16px ] [ agentName 13px semibold ] [ timestamp 11px muted ]
- Identity bar:
flex items-center gap-2 mb-1 - Icon: 16×16px, rendered via
<AgentIcon icon={agentIcon} className="h-4 w-4" /> - Name:
text-[13px] font-semibold text-foreground - Timestamp:
text-[11px] text-muted-foreground - No background, no border — identity floats above message content
Streaming Cursor
A blinking block cursor appended to the last streamed token while generation is active:
- Element:
<span className="inline-block w-2 h-[1em] bg-foreground/70 animate-cursor-blink ml-0.5 align-text-bottom" /> - Animation:
animate-cursor-blink— declare inindex.css(see Animation section) - Rendered only when
isStreaming: trueon the message; removed when stream ends
Stop Button
While a response is streaming, a Stop button appears below the message thread, centered above ChatInput:
[ (─────────────) ] ← centered row
[ ■ Stop generating ] ← Button variant="outline" size="sm"
[ (─────────────) ]
[ ChatInput ]
- Container:
flex justify-center py-2 border-t border-border - Button:
variant="outline" size="sm", iconSquare(filled stop), label "Stop generating" - Disappears immediately on click (optimistic) before server confirms cancellation
Edit / Retry Controls
User messages gain an edit pencil on hover. Assistant messages gain a retry button on hover.
User message edit:
Pencilicon button, 14×14px,variant="ghost" size="icon", positioned at top-right of message bubble- Visible only on
group-hover; always visible on touch devices - Click → switches message bubble to inline edit mode (see Interaction Contract)
Assistant message retry:
RefreshCwicon button, 14×14px,variant="ghost" size="icon", positioned below message content, right-aligned- Visible only on
group-hover; always visible on touch devices - Only present on messages where
isStreaming: falseandrole === "assistant"
Slash Command Popover
When the user types / as the first character in ChatInput:
[ /brainstorm — Route to Brainstormer ] ← highlighted item
[ /ask-pm — Route to PM ]
[ /ask-engineer — Route to Engineer ]
[ /task — Create a task ]
[ /search — Search conversations ]
- Container: shadcn
<Popover>anchored to the top-left of the textarea, opens upward - List:
<Command>component inside popover (reuses existingcmdkinstall) - Width: 260px, max 5 items visible before scroll
- Dismiss: Escape, clicking outside, or completing the command
@Mention Popover
When the user types @ in ChatInput, an agent autocomplete popover appears. Reuses the MentionOption pattern from MarkdownEditor.tsx:
- Container:
<Popover>anchored to the@character position, opens upward - List: agent names filtered by query string after
@ - Each row:
<AgentIcon>14×14px +agentNametext +rolelabel in muted text - Width: 200px, max 5 agents before scroll
- Selection: click or Enter inserts
@agentNametoken into textarea and routes message to that agent
Spacing Scale
Inherited from Phase 21. No new tokens for Phase 22.
| Token | Value | Phase 22 Usage |
|---|---|---|
| xs | 4px | Icon gaps in identity bar (gap-1) |
| sm | 8px | Stop button padding, retry button margin |
| md | 16px | Panel padding (unchanged) |
| lg | 24px | (no new usage) |
| xl | 32px | (no new usage) |
New spacing values (Phase 22 only):
- Identity bar gap:
gap-2(8px) — between icon and agent name; matches existinggap-2pattern in ChatPanel header - Edit/retry icon button size: 14×14px (
h-3.5 w-3.5) — matchesMoreHorizontalinChatConversationItem - Streaming cursor width:
w-2(8px), height:h-[1em]
Typography
All inherited from Phase 21. One addition for agent identity:
| Role | Size | Weight | Line Height | Usage |
|---|---|---|---|---|
| Body / message text | 15px (0.9375rem) | 400 | 1.6 | Chat message prose (unchanged) |
| Label / UI chrome | 13px (0.8125rem) | 400 | 1.4 | Conversation list, timestamps, slash command descriptions (unchanged) |
| Agent name (identity bar) | 13px (0.8125rem) | 600 | 1.4 | Agent name above assistant messages |
| Message timestamp | 11px (0.6875rem) | 400 | 1.4 | Timestamp in identity bar |
Weights used: 400 (regular) and 600 (semibold). No additional weights. Same constraint as Phase 21.
Slash command descriptions: Rendered at 13px / 400 with text-muted-foreground — color/opacity distinction from the 13px / 400 command label is sufficient; no separate size needed. This keeps the total distinct sizes at 4 (11px, 13px, 15px, and 20px from Phase 21 markdown headings).
11px timestamp note: This is a 2px reduction below the Phase 21 minimum of 13px, used only within the compact identity bar where spatial context is sufficient. It is never used for interactive or error text.
Color
All values inherited from Phase 21 CSS variable system. No new color variables introduced.
| Role | Catppuccin Mocha | Tokyo Night | Catppuccin Latte | Phase 22 Usage |
|---|---|---|---|---|
| Dominant (60%) | --background #1e1e2e |
--background #1a1b26 |
--background #eff1f5 |
Unchanged |
| Secondary (30%) | --card #181825 |
--card #16161e |
--card #e6e9ef |
Unchanged |
| Accent (10%) | --accent #45475a |
--accent #3b4261 |
--accent #bcc0cc |
Hovered rows (unchanged) |
| Primary | --primary |
--primary |
--primary |
Agent selector focus ring, send button |
| Destructive | --destructive |
--destructive |
--destructive |
Not used in Phase 22 |
| Muted text | --muted-foreground |
--muted-foreground |
--muted-foreground |
Timestamps, role labels, empty states |
Accent reserved for (Phase 22 additions):
- Same as Phase 21 (conversation rows, code block toolbar)
- Slash command popover highlighted item:
bg-accent(via<Command>item selection)
Streaming indicator color: bg-cyan-400 animate-pulse — reused directly from agentStatusDot.running in status-colors.ts. Applied as a 6×6px dot beside the agent name in the identity bar while streaming. Note: bg-cyan-400 bypasses the CSS variable system. If theming requirements evolve (e.g. a theme requires a different streaming color), promote this to a semantic token such as --streaming-indicator in the CSS variable layer.
Agent Role Colors (THEME-03)
Agent avatars are distinguishable across all three themes using semantic role color tokens. These use Tailwind semantic colors with dark: variants — no new CSS variables needed.
| Role | Icon color (light theme) | Icon color (dark theme) | Rationale |
|---|---|---|---|
pm |
text-blue-600 |
text-blue-400 |
Matches todo status color (existing pattern) |
engineer |
text-violet-600 |
text-violet-400 |
Matches in_review status color (existing pattern) |
ceo / general |
text-yellow-600 |
text-yellow-400 |
Matches idle agent status (existing pattern) |
designer |
text-pink-600 |
text-pink-400 |
New — no conflict with existing semantic |
qa |
text-orange-600 |
text-orange-400 |
Matches paused agent status (existing pattern) |
researcher |
text-teal-600 |
text-teal-400 |
New — no conflict with existing semantic |
devops / cto |
text-green-600 |
text-green-400 |
Matches done / active status (existing pattern) |
cmo / cfo |
text-neutral-600 |
text-neutral-400 |
Secondary/finance roles — muted |
| fallback | text-muted-foreground |
text-muted-foreground |
Unknown or null role |
These colors apply to the <AgentIcon> in the message identity bar only. The existing status-colors.ts patterns are NOT modified — Phase 22 adds a new agent-role-colors.ts utility.
Component Inventory
New components to build in Phase 22:
| Component | shadcn base | Notes |
|---|---|---|
ChatAgentSelector.tsx |
select or popover + command |
Dropdown in ChatPanel header; shows active agent icon + name; lists all workspace agents |
ChatMessageIdentityBar.tsx |
none | Icon + name + timestamp row above assistant messages; uses AgentIcon |
ChatStreamingCursor.tsx |
none | Inline blinking cursor element; rendered conditionally during streaming |
ChatStopButton.tsx |
button |
"Stop generating" button shown above input during active stream |
ChatMessageActions.tsx |
button, tooltip |
Edit (on user messages) and Retry (on assistant messages) hover actions |
ChatSlashCommandPopover.tsx |
popover, command |
Slash command menu anchored to textarea; 5 commands |
ChatMentionPopover.tsx |
popover |
Agent @mention autocomplete anchored to @ position |
useStreamingChat.ts |
none | Hook managing SSE connection, token accumulation, streaming state |
agent-role-colors.ts |
none | Map of AgentRole → Tailwind class string for icon coloring |
Existing components to modify:
| Component | Change |
|---|---|
ChatPanel.tsx |
Add ChatAgentSelector to header; add ChatStopButton above input when streaming |
ChatMessage.tsx |
Accept agentId, agentName, agentIcon, agentRole, isStreaming props; render ChatMessageIdentityBar and ChatStreamingCursor; wrap in group for hover actions |
ChatMessageList.tsx |
Replace div iteration with @tanstack/react-virtual virtualizer; pass agent identity props down to ChatMessage |
ChatInput.tsx |
Wire ChatSlashCommandPopover (on / keystroke) and ChatMentionPopover (on @ keystroke); accept disabled prop during streaming |
Icons (lucide-react) — Phase 22 additions:
Square— Stop generating button icon (filled stop)Pencil— Edit user messageRefreshCw— Retry assistant messageChevronDown— Agent selector dropdown indicatorBot— Default agent icon fallback (already inAGENT_ICON_NAMES)
All icons from Phase 21 remain unchanged.
Interaction Contract
Streaming Response
| Interaction | Behavior |
|---|---|
| User sends message | ChatInput disabled; isSubmitting spinner on send button; SSE connection opens |
| First token arrives | New assistant message appended to list; identity bar renders; ChatStreamingCursor shown; ChatStopButton appears above input |
| Tokens continue | Text accumulates character-by-character in the message; cursor stays at end; list auto-scrolls to bottom if user has not scrolled up |
| User scrolls up during stream | Auto-scroll pauses; "Jump to bottom" button appears (↓ icon, variant="outline" size="icon-sm", fixed at bottom-right of message list) |
| Stream ends | ChatStreamingCursor removed; ChatStopButton removed; ChatInput re-enabled; message gets final updatedAt timestamp |
| Stream error | Streaming cursor replaced with error inline text: "Response interrupted. [Retry ↺]" — text-destructive with inline Retry link |
Stop Generation
| Interaction | Behavior |
|---|---|
| Click "Stop generating" | Button removed immediately (optimistic); SSE connection closed client-side; partial message preserved as-is with a [stopped] suffix in muted text |
| Server confirms stop | No additional UI change needed — state already settled on client |
Message Edit (User Messages)
| Interaction | Behavior |
|---|---|
| Hover user message | Pencil icon button appears at top-right of bubble |
Click Pencil |
Message bubble becomes an inline textarea pre-filled with existing content; "Save edit" and "Discard edit" buttons appear below; send button hidden |
| Edit textarea + click "Save edit" | POST updated message to server; assistant messages after this message are removed from thread (regeneration); new streaming response begins |
| Click "Discard edit" | Textarea reverts to read-only bubble; no changes |
| Save with empty content | "Save edit" button disabled when textarea is empty |
Retry (Assistant Messages)
| Interaction | Behavior |
|---|---|
| Hover assistant message | RefreshCw icon button appears below message, right-aligned |
Click RefreshCw |
Current assistant message replaced with a streaming placeholder; SSE connection opens; regenerated response streams in |
| Retry during active stream | Not available — RefreshCw button hidden while any stream is active |
Agent Selector
| Interaction | Behavior |
|---|---|
| Click agent selector | Popover/dropdown opens listing all workspace agents (icon + name + role label) |
| Select agent | Active agent updated for current conversation; PATCH /conversations/:id with agentId; selector shows new agent immediately (optimistic) |
| No agents available | Selector shows "No agents" in muted text; disabled state |
| Active agent deleted externally | Selector shows "Unknown agent" in muted text; conversation continues with null agentId until user selects another |
Slash Commands (INPUT-05)
| Interaction | Behavior |
|---|---|
Type / as first char |
ChatSlashCommandPopover opens above input listing all 5 commands |
Type /bra (partial) |
Popover filters to matching commands |
| Arrow keys / Tab | Navigate popover items |
| Enter or click item | Command token inserted into textarea (e.g. /brainstorm ); popover closes; routing applied on send |
| Escape | Popover closes; typed text remains |
Send with /brainstorm prefix |
Routes message to Brainstormer agent regardless of active agent selector |
Slash command routing table:
| Command | Routes to |
|---|---|
/brainstorm |
Brainstormer (general role with brainstormer prompt) |
/ask-pm |
PM role agent |
/ask-engineer |
Engineer role agent |
/task |
PM role agent (task creation intent) |
/search |
No-op in Phase 22 (stubbed; Phase 24 implements) |
@Mention Routing (INPUT-06)
| Interaction | Behavior |
|---|---|
Type @ |
ChatMentionPopover opens listing workspace agents (icon + name + role) |
Type @eng (partial) |
List filters to agents whose name contains "eng" (case-insensitive) |
| Select agent | @AgentName token inserted; popover closes |
Send message with @AgentName |
Message routed to named agent for this turn only (overrides active agent selector) |
| Unknown mention | Sent as plain text; no routing override |
Virtualized Message List (PERF-03)
- Uses
@tanstack/react-virtualwithuseVirtualizer overscan: 5— renders 5 items above/below visible window- Item height: estimated 80px default; dynamic measurement via
measureElementfor variable-height markdown messages scrollMarginBottom: 120pxto keep new messages visible above the input area- "Jump to bottom" appears when
scrollOffset > 200pxfrom bottom; clicking callsscrollToIndex(messages.length - 1, { align: 'end' })
Copywriting Contract
All Phase 21 copy is preserved unchanged. Phase 22 additions:
| Element | Copy | Notes |
|---|---|---|
| Agent selector placeholder | "Select agent" | text-muted-foreground; shown when no agent assigned |
| Agent selector no-options | "No agents configured" | Disabled state |
| Stop button label | "Stop generating" | variant="outline" size="sm"; icon Square precedes label |
| Edit message button aria-label | "Edit message" | Icon-only; Pencil icon |
| Retry message button aria-label | "Retry response" | Icon-only; RefreshCw icon |
| Inline edit save button | "Save edit" | variant="default" size="sm" |
| Inline edit cancel button | "Discard edit" | variant="ghost" size="sm" |
| Streaming interrupted error | "Response interrupted." | Inline text-destructive; followed by Retry link |
| Retry inline link | "Retry" | text-primary underline cursor-pointer — not a <Button> |
| Stopped message suffix | "[stopped]" | text-muted-foreground text-[11px] ml-1; appended to partial message |
Slash command: /brainstorm description |
"Route to Brainstormer" | Second line in slash popover item; text-muted-foreground |
Slash command: /ask-pm description |
"Route to PM" | Second line; text-muted-foreground |
Slash command: /ask-engineer description |
"Route to Engineer" | Second line; text-muted-foreground |
Slash command: /task description |
"Create a task" | Second line; text-muted-foreground |
Slash command: /search description |
"Search conversations" | Second line; text-muted-foreground; "Coming soon" suffix in muted text; greyed out |
| Jump to bottom button aria-label | "Scroll to latest message" | Icon-only; ArrowDown icon |
| Input placeholder (streaming) | "Waiting for response..." | Replaces "Message your agent..." while isStreaming: true; textarea is disabled |
Tone: Direct, functional, no corporate language. Consistent with Phase 21.
States and Loading
| Component | Loading state | Empty state | Error state | Streaming state |
|---|---|---|---|---|
ChatAgentSelector |
Skeleton pill 80px wide | "No agents configured" | "Could not load agents. Refresh to try again." — popover body | n/a |
ChatMessage (assistant) |
n/a | n/a | Inline "Response interrupted. Retry" | Cursor blink + streaming text accumulation |
ChatMessageList |
3x Skeleton rows (h-16 variable) | Inherited from Phase 21 | Inherited from Phase 21 | Auto-scroll; Stop button visible |
ChatInput |
n/a | n/a | Inherited from Phase 21 | disabled; placeholder "Waiting for response..." |
ChatSlashCommandPopover |
n/a | n/a | n/a | Hidden while streaming |
ChatMentionPopover |
Skeleton 3 rows | "No agents found" | n/a | Hidden while streaming |
Optimistic updates:
- Agent selector: updates conversation's
agentIdoptimistically; reverts on API error with toast "Could not update agent. Try again." - Stop: button removed immediately; error shown inline if SSE close fails silently
- Edit/Retry: new streaming message renders immediately; old messages removed optimistically
Theme Integration Contract
Phase 22 extends Phase 21's zero-new-plumbing approach:
- Agent role colors use
text-{color}-600 dark:text-{color}-400Tailwind classes — resolve correctly in all three themes via the existing.darkclass on<html> - Tokyo Night:
.darkclass is present (as established in Phase 21 ThemeContext) — dark variants apply correctly - Streaming cursor:
bg-foreground/70uses CSS variable — resolves to near-white in dark themes, near-black in Catppuccin Latte - Slash command popover and mention popover: use shadcn
<Popover>+<Command>which inherit--popover,--popover-foregroundCSS variables — no manual theme wiring needed
THEME-03 checklist:
- Agent icon colors declared per-role with
dark:variants — all three themes tested - Streaming indicator uses
bg-cyan-400(same across themes — sufficient contrast on all three backgrounds) - No hardcoded hex colors introduced in Phase 22
Accessibility
Inherits all Phase 21 accessibility contracts. Phase 22 additions:
| Concern | Requirement |
|---|---|
| Agent selector | aria-label="Active agent" on trigger; dropdown items have role="option" |
| Streaming message | aria-live="polite" on ChatMessageList (established in Phase 21) handles streamed token announcements without modification |
| Stop button | aria-label="Stop generating response" |
| Streaming cursor | aria-hidden="true" — decorative only |
| Edit textarea | aria-label="Edit your message" on inline textarea |
| Retry button | aria-label="Retry response" |
| Slash popover | role="listbox" on items; announce open state with aria-expanded |
| Mention popover | role="listbox" on items; announce open state with aria-expanded |
| Jump to bottom | aria-label="Scroll to latest message" |
| Edit save/cancel | Standard button labels — "Save edit" and "Discard edit" (no icon-only ambiguity) |
| Focus after stop | Focus returns to ChatInput textarea after stop action |
| Focus after retry | No focus change — user may be reading the regenerated message |
Animation and Motion
Inherits Phase 21 animation contract. Phase 22 additions:
| Element | Animation | Duration | Easing | CSS |
|---|---|---|---|---|
| Streaming cursor | animate-cursor-blink keyframe |
800ms | step-start | See below |
| Stop button appear | animate-in fade-in slide-in-from-bottom-1 |
150ms | ease-out — shadcn default |
|
| Stop button disappear | animate-out fade-out slide-out-to-bottom-1 |
100ms | ease-in |
|
| Slash popover open | animate-in fade-in zoom-in-95 |
100ms | ease-out — shadcn Popover default |
|
| Mention popover open | Same as slash popover | 100ms | ease-out |
|
| Jump to bottom button | animate-in fade-in slide-in-from-bottom-2 |
150ms | ease-out |
|
| Streaming indicator dot | animate-pulse (Tailwind) |
continuous | — | |
| Edit mode transition | No animation — immediate swap; avoids layout shift | — | — |
animate-cursor-blink keyframe (add to index.css):
@keyframes cursor-blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
.animate-cursor-blink {
animation: cursor-blink 800ms step-start infinite;
}
@media (prefers-reduced-motion: reduce) {
.animate-cursor-blink {
animation: none;
opacity: 1;
}
}
Reduced motion: All Phase 22 entrance/exit animations must respect prefers-reduced-motion. Use motion-safe: Tailwind prefix or @media guards matching the Phase 21 pattern in index.css.
Performance Contract
| Requirement | Target | Implementation |
|---|---|---|
| PERF-02 | First token ≤ 100ms server-to-UI | SSE connection opened before server begins generation; client-side: no React re-render batching delay — use startTransition for text accumulation to keep input responsive |
| PERF-03 | 1,000+ messages no jank | @tanstack/react-virtual with dynamic measurement; overscan: 5; avoid re-rendering all messages on token append — only the active streaming message re-renders |
Token accumulation pattern:
- Maintain a
streamingContentstring inuseStreamingChatlocal state - Append tokens via
setState(prev => prev + token)— single string concat, not array push - When stream ends, commit full message to React Query cache via
queryClient.setQueryData; clear localstreamingContent
Registry Safety
| Registry | Blocks Used | Safety Gate |
|---|---|---|
| shadcn official | All existing Phase 21 components (already installed) | not required |
| npm (non-registry) | @tanstack/react-virtual — standard npm package install via pnpm add |
not applicable (not a shadcn registry block) |
| Third-party | none | not applicable |
No third-party shadcn registries used in Phase 22.
Checker Sign-Off
- Dimension 1 Copywriting: PASS
- Dimension 2 Visuals: PASS
- Dimension 3 Color: PASS
- Dimension 4 Typography: PASS
- Dimension 5 Spacing: PASS
- Dimension 6 Registry Safety: PASS
Approval: pending