diff --git a/.planning/phases/22-agent-streaming/22-UI-SPEC.md b/.planning/phases/22-agent-streaming/22-UI-SPEC.md new file mode 100644 index 00000000..9e38a15d --- /dev/null +++ b/.planning/phases/22-agent-streaming/22-UI-SPEC.md @@ -0,0 +1,376 @@ +--- +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: `