--- phase: 22 slug: agent-streaming status: draft shadcn_initialized: true preset: new-york / neutral / cssVariables created: 2026-04-01 revised: 2026-04-01 --- # Phase 22 — UI Design Contract > Visual and interaction contract for frontend phases. Generated by gsd-ui-researcher, verified by gsd-ui-checker. --- ## Design System | Property | Value | |----------|-------| | Tool | shadcn (new-york style) — carried forward from Phase 21 | | Preset | new-york, baseColor: neutral, cssVariables: true | | Component library | Radix UI (via shadcn) | | Icon library | lucide-react (^0.574.0) | | Font | System UI stack (inherited from existing CSS — no custom font declared) | Source: `ui/components.json`, `ui/src/index.css` (unchanged from Phase 21) **shadcn components already installed (usable without install):** `Avatar`, `Badge`, `Button`, `Card`, `Checkbox`, `Command`, `Dialog`, `DropdownMenu`, `Input`, `Popover`, `ScrollArea`, `Select`, `Separator`, `Sheet`, `Skeleton`, `Tabs`, `Textarea`, `Tooltip` **New shadcn components for this phase (install before use):** None required — `Select` and `DropdownMenu` cover the AgentSelector. `Avatar` covers the agent badge avatar slot. **New npm packages for this phase (not shadcn registry):** - `virtua` ^0.49.0 — virtualized list for ChatMessageList (PERF-03). Install: `pnpm --filter @paperclipai/ui add virtua` - Server: `openai` or `ai` SDK — LLM streaming. See RESEARCH.md for provider resolution. Not a UI registry concern. --- ## Focal Point The primary focal point of Phase 22 is the **streaming message in progress** — specifically the assistant message bubble that receives incoming tokens. While tokens arrive, the bubble is the only animated element on screen. The Stop button (rendered inside `ChatInput` in place of Send) is the secondary focal point during streaming. --- ## Spacing Scale Carried forward from Phase 21 without change. Source: 21-UI-SPEC.md. | Token | Value | Usage | |-------|-------|-------| | xs | 4px | Icon gaps (`gap-1`), inline icon + label spacing | | sm | 8px | Compact padding inside list items, badge padding, button icon padding | | sm+ | 12px | Conversation list item vertical padding (`py-3`) — named exception, follows `EntityRow` pattern | | md | 16px | Default element padding (`p-4`), chat panel header padding | | lg | 24px | Section padding on desktop (`p-6`) | | xl | 32px | Gap between major UI zones | | 2xl | 48px | Empty-state vertical padding (`py-12`) | | 3xl | 64px | Page-level section breaks (not applicable in panel context) | **Phase 22 additions:** | Token | Value | Usage | |-------|-------|-------| | agent-badge-gap | 8px (sm) | Gap between agent avatar and agent name in `ChatAgentBadge` | | agent-avatar | 20px | Width and height of the agent avatar circle in `ChatAgentBadge` | | streaming-dot | 8px (sm) | Width and height of the pulsing streaming cursor dot | Exceptions (unchanged from Phase 21): - `sm+` (12px): conversation list item vertical padding only. - Touch targets: `min-height: 44px` on coarse-pointer devices (global `index.css` rule). - Chat input bottom padding: `pb-[calc(env(safe-area-inset-bottom)+16px)]` on mobile. --- ## Typography Carried forward from Phase 21 without change. Source: 21-UI-SPEC.md. Two weights only: regular (400) and semibold (600). | Role | Size | Weight | Line Height | Tailwind Class | |------|------|--------|-------------|----------------| | Body | 14px | 400 (regular) | 1.5 | `text-sm` | | Label | 13px | 400 (regular) | 1.4 | `text-[13px]` | | Heading | 16px | 600 (semibold) | 1.25 | `text-base font-semibold` | | Meta / Timestamp | 12px | 400 (regular) | 1.4 | `text-xs text-muted-foreground` | **Phase 22 typography additions:** - Agent name in `ChatAgentBadge`: Label (13px / regular / `text-muted-foreground`). Not semibold — the name is contextual metadata, not a heading. - Slash command suggestion label in the command popover: Body (14px / regular). - Agent selector trigger label: Label (13px / regular), truncated with `truncate`. - Stop button label (text inside ChatInput during streaming): Body (14px / regular), no special weight. The Square icon carries the visual signal. Rules (unchanged): - Agent message content inside `ChatMarkdownMessage` uses `prose prose-sm` — do not override. - The chat input `Textarea` uses Body (14px / regular). - Timestamps use Meta (12px / regular / muted-foreground), visible on hover only. --- ## Color All colors reference CSS custom properties already declared for all three themes in `ui/src/index.css`. Never hard-code hex values. Source: 21-UI-SPEC.md + RESEARCH.md Pattern 6. | Role | CSS Variable | 60/30/10 | Usage | |------|-------------|----------|-------| | Dominant surface | `--background` | 60% | Chat panel background, message list background | | Secondary surface | `--card` / `--sidebar` | 30% | Conversation list sidebar, code block container | | Accent | `--primary` | 10% | Send button, active conversation border-left, filled pin icon | | Muted | `--muted` | — | Input background, empty state icon container, streaming cursor background | | Muted foreground | `--muted-foreground` | — | Placeholder text, timestamps, agent name in badge, secondary labels | | Destructive | `--destructive` | — | Delete conversation action only (carried from Phase 21) | | Border | `--border` | — | Dividers, chat panel border, input border | Accent (`--primary`) reserved for exactly these elements (Phase 22 carries Phase 21 list, no additions): 1. The "Send message" primary action button background 2. The active conversation in the sidebar (left-border indicator) 3. Filled pin icon on pinned conversations **Phase 22 agent colors — CSS custom properties (THEME-03):** Agent role colors use `--chart-1` through `--chart-5` CSS custom properties already declared for all three themes in `ui/src/index.css`. These are the ONLY variables permitted for agent identity coloring. Implementation via `bg-[hsl(var(--chart-N))]` Tailwind utility. | Agent Role | CSS Variable | Tailwind Class | |------------|-------------|----------------| | `ceo` | `--chart-1` | `bg-[hsl(var(--chart-1))]` | | `pm` | `--chart-2` | `bg-[hsl(var(--chart-2))]` | | `engineer` | `--chart-3` | `bg-[hsl(var(--chart-3))]` | | `general` / `generalist` | `--chart-4` | `bg-[hsl(var(--chart-4))]` | | `brainstormer` | `--chart-5` | `bg-[hsl(var(--chart-5))]` | | Unknown / fallback | `--muted` | `bg-muted` | Text on all agent color backgrounds: `text-white` (sufficient contrast on all three themes per chart variable definitions). Do not use `text-foreground` — the chart variables are saturated colors and require white text. **Stop button color:** The Stop button uses `variant="destructive"` from shadcn Button — it uses `--destructive` background with destructive-foreground text. This signals urgency (cancel an ongoing action) without being confused with a data-destructive action. --- ## Component Inventory ### Modified from Phase 21 #### ChatInput (modified) - Existing behavior fully preserved (auto-resize, Enter/Shift+Enter, Escape) - **Addition: Stop button during streaming** - When `streaming === true`: Send button is replaced by a Stop button - Stop button: `variant="destructive"`, icon-only (`Square` from lucide, 16px), `aria-label="Stop generation"` - Stop button is NEVER disabled — it must always be clickable during streaming - Layout: same position as Send button (flex row right of Textarea). Dimensions identical to Send button so the layout does not shift when toggling. - **Addition: Slash command popover** - Trigger: when input value starts with `/` and matches a known prefix - Popover appears above the input (`` with `side="top" align="start"`) - Contents: list of matching commands using `` component (already installed) - Command item format: `/{command}` label (14px / regular) + destination agent name (13px / muted-foreground) - Dismiss: Escape, backspace past `/`, or clicking away - Max 5 items displayed; no scrolling inside the popover - Commands list (hardcoded): | Command | Label | Agent Destination | |---------|-------|-------------------| | `/brainstorm` | Brainstorm an idea | Brainstormer | | `/ask-pm` | Ask the PM | PM | | `/ask-engineer` | Ask the Engineer | Engineer | | `/task` | Create a task | Engineer | | `/search` | Search conversations | Generalist | - **Addition: @mention popover** - Trigger: when input value starts with `@` and has at least 1 character following - Same `` + `` pattern as slash commands - Contents: filtered list of agent names from `useAgents()` cache - Item format: `AgentBadge` (20px avatar) + agent name (14px / regular) - Dismiss: same as slash command popover - **No change to aria-label, placeholder, or keyboard shortcuts** #### ChatMessageList (modified) - **Replace flat div with `` from `virtua` (PERF-03)** - Drop `` wrapper — `virtua` manages scroll internally - `` wraps all message items - Auto-scroll to bottom logic: `isAtBottom` state derived from VList `onScroll` callback — only auto-scroll when `isAtBottom === true` - Threshold: `isAtBottom = scrollOffset + clientHeight >= scrollSize - 80` (80px tolerance) - **Addition: streaming optimistic message** - When `streaming === true`: append a synthetic `ChatMessageItem` at bottom with: - `role: "assistant"` - `content: partialContent` (from `useStreamMessage` hook) - `isStreaming: true` prop - Streaming indicator: a pulsing dot (`w-2 h-2 rounded-full bg-muted animate-pulse`) appended after the last rendered markdown token - The streaming item has no timestamp (omit timestamp row entirely while `isStreaming`) - The streaming item has no action buttons (edit/retry are not shown on in-progress messages) - **Addition: message action buttons (edit/retry) on existing assistant messages** - Visible on hover only (`opacity-0 group-hover:opacity-100 transition-opacity duration-150`) - Position: below the message bubble, right-aligned (`flex justify-end gap-1 mt-1`) - Retry button: `variant="ghost" size="icon-sm"`, icon `RotateCcw` (14px), `aria-label="Retry response"` - Edit button: on user messages only — `variant="ghost" size="icon-sm"`, icon `Pencil` (14px), `aria-label="Edit message"` - While streaming is in progress (`streaming === true`): hide all action buttons on all messages (prevent conflicting actions) - **Addition: `ChatAgentBadge` above each assistant message bubble** - See new component spec below #### ChatPanel (modified) - **Addition: `AgentSelector` in the panel header** - Position: right side of the header bar (flex row, `ml-auto` before close button) - See new component spec below - Header height remains 48px — AgentSelector must fit within this height - No other structural changes ### New Components for Phase 22 #### ChatAgentBadge - Rendered above each assistant message bubble in `ChatMessageList` - Layout: `flex items-center gap-2 mb-1` (8px gap — `sm` token; 4px margin-bottom) - Avatar: 20px circle (`w-5 h-5 rounded-full flex items-center justify-center`), background color from `agentRoleColorClass(agent.role)` (see Color section) - If agent has an `icon` value: render `AgentIcon` at 12px size inside the circle - If no icon: render the first letter of `agent.name` at `text-[10px] font-semibold text-white` - Agent name: Label (13px / regular / `text-muted-foreground`), truncated at 120px max-width - Source: `agent.name` and `agent.role` resolved client-side from `useAgents(companyId)` cache keyed by `agentId` on the message - Fallback when agent not found in cache: show `Bot` icon (lucide, 12px) + "Agent" as name, `bg-muted` background — never throw or show nothing #### AgentSelector - Location: ChatPanel header, right side before the close button - Component: `