nexus/.planning/phases/22-agent-streaming/22-UI-SPEC.md
Nexus Dev 3ab0955da9 docs(22): fix UI-SPEC checker blocks — copywriting, typography, spacing, color note
- 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>
2026-04-04 03:55:47 +00:00

26 KiB
Raw Blame History

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 layout
  • ChatMessage.tsx — extend props to accept agentId, agentName, agentIcon for identity rendering
  • ChatMessageList.tsx — replace with virtualized list (@tanstack/react-virtual) for PERF-03
  • ChatInput.tsx — extend with slash command popover and @mention popover (reuse MentionOption pattern from MarkdownEditor.tsx)
  • AgentIcon (from AgentIconPicker.tsx) — reuse directly for agent avatar rendering in messages
  • agentStatusDot (from status-colors.ts) — reuse running: "bg-cyan-400 animate-pulse" for streaming indicator

New package required:

  • @tanstack/react-virtual — not currently installed; add to ui/package.json for 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 AgentSelector is 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-2 row, same px-4 py-2 padding 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 in index.css (see Animation section)
  • Rendered only when isStreaming: true on 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", icon Square (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:

  • Pencil icon 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:

  • RefreshCw icon 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: false and role === "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 existing cmdk install)
  • 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 + agentName text + role label in muted text
  • Width: 200px, max 5 agents before scroll
  • Selection: click or Enter inserts @agentName token 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 existing gap-2 pattern in ChatPanel header
  • Edit/retry icon button size: 14×14px (h-3.5 w-3.5) — matches MoreHorizontal in ChatConversationItem
  • 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 message
  • RefreshCw — Retry assistant message
  • ChevronDown — Agent selector dropdown indicator
  • Bot — Default agent icon fallback (already in AGENT_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-virtual with useVirtualizer
  • overscan: 5 — renders 5 items above/below visible window
  • Item height: estimated 80px default; dynamic measurement via measureElement for variable-height markdown messages
  • scrollMarginBottom: 120px to keep new messages visible above the input area
  • "Jump to bottom" appears when scrollOffset > 200px from bottom; clicking calls scrollToIndex(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 agentId optimistically; 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}-400 Tailwind classes — resolve correctly in all three themes via the existing .dark class on <html>
  • Tokyo Night: .dark class is present (as established in Phase 21 ThemeContext) — dark variants apply correctly
  • Streaming cursor: bg-foreground/70 uses 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-foreground CSS 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 streamingContent string in useStreamingChat local 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 local streamingContent

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