---
phase: 21
slug: chat-foundation
status: draft
shadcn_initialized: true
preset: new-york / neutral / css-variables
created: 2026-04-01
---

# Phase 21 — UI Design Contract

> Visual and interaction contract for Phase 21: Chat Foundation.
> Generated by gsd-ui-researcher. Verified by gsd-ui-checker.

---

## Design System

| Property | Value | Source |
|----------|-------|--------|
| Tool | shadcn/ui | `ui/components.json` |
| 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` + RESEARCH.md |
| Font | System UI (`font-sans` from Tailwind default, 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:**
- `MarkdownBody` — extend with `rehype-highlight` for syntax highlighting (CHAT-02/03)
- `PanelContext` pattern — mirror for `ChatPanelContext` with `nexus:chat-panel-open` key
- `PropertiesPanel` — reference layout; chat drawer must be a sibling, not a replacement
- `ScrollArea` — use for conversation list and message thread scroll regions

---

## Layout Contract

### Chat Drawer Position

The `ChatPanel` is a fixed-width right-side drawer, positioned as a sibling of `<PropertiesPanel>` within the existing `flex` row in `Layout.tsx`.

```
[ CompanyRail ] [ Sidebar ] [ <main> ] [ ChatPanel ] [ PropertiesPanel ]
```

**Rules:**
- Width when open: 380px (matching `RESEARCH.md` recommendation; same order-of-magnitude as `PropertiesPanel` at 320px)
- Width when closed: 0px (`overflow-hidden`)
- Transition: `transition-[width] duration-100 ease-out` — matches sidebar and PropertiesPanel animations exactly
- When `ChatPanel` opens, `PropertiesPanel` closes (set `panelVisible: false` via `setPanelVisible`). They do not coexist — the combined 700px would crowd a default 1280px viewport.
- Desktop only initially: `hidden md:flex` — same guard as `PropertiesPanel`
- `localStorage` key for open state: `nexus:chat-panel-open`

### Chat Drawer Internal Layout

```
[ Header: title + close button ]  — border-b border-border, px-4 py-2
[ ChatConversationList ]           — fixed width left column, 240px, border-r
[ ChatMessageList ]                — flex-1, overflow-auto via ScrollArea
[ ChatInput ]                      — sticky bottom, border-t border-border
```

Alternatively, the drawer can be a single-column panel toggling between conversation list view and message thread view (simpler for Phase 21; agent streaming in Phase 22 makes the two-column layout more valuable). **Use two-column layout within the 380px drawer**: left column 160px (conversation list), right column flex-1 (message thread + input).

**Focal point:** Primary visual anchor: `ChatMessageList` (flex-1 thread pane); secondary anchor: `ChatInput` (sticky bottom).

### Chat Panel Trigger

Add a `MessageSquare` icon button to the top-right control area of `Layout.tsx` (or to `BreadcrumbBar`), using the same `Button variant="ghost" size="icon-sm"` pattern as the existing theme toggle and settings buttons.

---

## Spacing Scale

Declared values (all multiples of 4):

| Token | Value | Usage |
|-------|-------|-------|
| xs | 4px | Icon gaps (`gap-1`), inline chip padding |
| sm | 8px | Compact element spacing, badge padding (`px-2 py-1`) |
| md | 16px | Default element spacing, panel padding (`p-4`) |
| lg | 24px | Section padding (`px-6`), message bubble vertical rhythm |
| xl | 32px | Layout gaps between major zones |
| 2xl | 48px | Not used in Phase 21 (no full-page sections) |
| 3xl | 64px | Not used in Phase 21 |

**Exceptions:**
- Chat input area: `px-3 py-2` (12px/8px) — matches existing BreadcrumbBar footer pattern
- Message bubble padding: `px-3 py-2` for compact density
- Code block padding: `padding: 0.5rem 0.65rem` — inherited from existing `.paperclip-markdown pre` rule in `index.css`; not introduced or modified in Phase 21
- Touch targets: minimum 44px height on `@media (pointer: coarse)` — already enforced globally in `index.css`

---

## Typography

All sizes and weights are drawn from the existing `.paperclip-markdown` and `index.css` typographic system. No new type tokens are introduced.

| Role | Size | Weight | Line Height | Usage |
|------|------|--------|-------------|-------|
| Body / message text | 15px (0.9375rem) | 400 | 1.6 | Chat message prose (`paperclip-markdown` font-size: 0.9375rem; line-height: 1.6) |
| Label / UI chrome | 13px (0.8125rem) | 400 | 1.4 | Conversation list titles, timestamps, sidebar nav (`text-[13px]` already used in Layout) |
| Subheading | 13px (0.8125rem) | 400 | 1.4 | Code block language labels, drawer section headers — differentiated from body by context and container, not size alone |
| Heading (in markdown) | 20px (1.25rem) | 600 | 1.3 | `## ` headings inside agent responses (`.paperclip-markdown h2`) |

**Weights used:** 400 (regular) and 600 (semibold). No additional weights.

**Note on Subheading vs Label:** Both are 13px / 400. Subheading elements (language labels, section headers) are differentiated from body prose (15px) by a 2px size difference and by their placement within distinct UI containers (code block toolbar, panel header), not by a separate weight. A 14px intermediate size was considered and rejected — the 1px gap from 15px body was insufficient differentiation without a weight cue.

**Monospace font (code blocks):** `ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace` — matches existing `.paperclip-markdown code` declaration.

---

## Color

The design uses the existing Nexus CSS variable system across all three themes. No new color values are introduced.

| Role | Catppuccin Mocha (.dark) | Tokyo Night (.theme-tokyo-night.dark) | Catppuccin Latte (:root) | Usage |
|------|--------------------------|---------------------------------------|--------------------------|-------|
| Dominant (60%) | `--background` #1e1e2e | `--background` #1a1b26 | `--background` #eff1f5 | Chat panel background, message list |
| Secondary (30%) | `--card` #181825 / `--sidebar` #181825 | `--card` #16161e / `--sidebar` #16161e | `--card` #e6e9ef | Conversation list pane, code block background, input area |
| Accent (10%) | `--accent` #45475a | `--accent` #3b4261 | `--accent` #bcc0cc | Hovered conversation row, active conversation highlight |
| Primary | `--primary` #89b4fa | `--primary` #7aa2f7 | `--primary` #1e66f5 | Send button, focus rings, active conversation indicator |
| Destructive | `--destructive` #f38ba8 | `--destructive` #f7768e | `--destructive` #d20f39 | Delete conversation confirmation only |
| Muted text | `--muted-foreground` #6c7086 | `--muted-foreground` #565f89 | `--muted-foreground` #9ca0b0 | Timestamps, conversation preview text, empty state body |

**Accent reserved for:**
1. Hovered conversation list row (`hover:bg-accent`)
2. Currently active/selected conversation row (`bg-accent/60`)
3. Code block toolbar background on hover (existing `index.css` pattern)
4. Input area focus-within ring (`focus-within:ring-1 ring-ring`)

**Accent is NOT used for:** send button, message bubbles, badges, or any primary action affordance.

### Code Block Syntax Highlighting — Theme Mapping

| Nexus Theme | highlight.js CSS Theme | `THEME_META[theme].dark` |
|-------------|----------------------|--------------------------|
| catppuccin-mocha | `highlight.js/styles/base16/catppuccin.css` (dark variant) | true |
| tokyo-night | `highlight.js/styles/tokyo-night-dark.css` | true |
| catppuccin-latte | `highlight.js/styles/base16/catppuccin.css` (light variant) | false |

Theme CSS must be loaded via a single CSS file that gates `.hljs` color overrides on `.dark` and `.theme-tokyo-night` — never import all three theme CSS files simultaneously. Override approach: use CSS custom properties per theme class, matching the existing pattern in `index.css`.

**Existing code block colors (hardcoded in index.css, keep consistent):**
- Dark code block background: `#1e1e2e` (Catppuccin Mocha base)
- Dark code block text: `#cdd6f4`
- These hardcoded values apply for Mocha; hljs theme overrides layer on top for Tokyo Night

---

## Component Inventory

Components to build in Phase 21:

| Component | shadcn base | Notes |
|-----------|-------------|-------|
| `ChatPanelContext.tsx` | none | localStorage persistence, mirrors `PanelContext` pattern |
| `ChatPanel.tsx` | `ScrollArea`, `Button` | Right drawer shell; two-column (list + thread) |
| `ChatConversationList.tsx` | `ScrollArea`, `skeleton`, `button` | Infinite scroll via `useInfiniteQuery`; skeleton on load |
| `ChatConversationItem.tsx` | `dropdown-menu`, `button` | Row with title, preview, timestamp; hover reveals action menu |
| `ChatMessageList.tsx` | `ScrollArea` | Message thread; virtualization deferred to Phase 22 |
| `ChatMessage.tsx` | none | Wrapper for user vs assistant messages; role-based alignment |
| `ChatMarkdownMessage.tsx` | none | Extends `MarkdownBody` with `rehype-highlight`; adds copy button on `pre` |
| `ChatInput.tsx` | `textarea`, `button` | Auto-resize textarea; keyboard shortcuts |
| `ChatCodeBlock.tsx` | `button`, `tooltip` | Wraps `pre`; adds language label + copy button |

**Icons (lucide-react):**
- `MessageSquare` — chat panel trigger button in Layout
- `Plus` — new conversation button
- `Pin` / `PinOff` — pin/unpin conversation (dropdown action)
- `Archive` — archive conversation (dropdown action)
- `Trash2` — delete conversation (dropdown action, triggers confirmation)
- `Copy` — copy code block content
- `Check` — copy success state (shown 1500ms, then reverts to Copy)
- `Send` — send message button
- `X` — close chat panel, dismiss input

---

## Interaction Contract

### Conversation List

| Interaction | Behavior |
|-------------|---------|
| Click conversation row | Load that conversation's messages into the thread pane |
| Hover conversation row | Reveal `...` (MoreHorizontal) icon button at row end |
| Click `...` | Open `dropdown-menu` with: Rename, Pin/Unpin, Archive, Delete |
| Click `+` New conversation | Create new conversation via POST, optimistic insert at top of list |
| Scroll to bottom of list | Trigger `fetchNextPage` for infinite scroll (intersection observer on last item) |
| Long-press (mobile) | Same as hover — reveal action menu |

### Conversation CRUD States

| Action | UI Response |
|--------|------------|
| Create conversation | Optimistic insert at top of list; title shows "New Conversation" placeholder until first message auto-sets it |
| Rename (title edit) | Inline contenteditable or input field replacing the title text; blur or Enter confirms; Escape cancels |
| Pin conversation | Row moves to top of list within a "Pinned" group (visually separated); pin icon visible on row |
| Archive conversation | Row removed from default list; no undo in Phase 21 |
| Delete conversation | Confirmation dialog (shadcn `dialog`): "Delete conversation? This cannot be undone." with "Delete" (destructive) + "Cancel" buttons |

### Message Thread

| Interaction | Behavior |
|-------------|---------|
| User message | Right-aligned bubble, `bg-secondary` background, `text-secondary-foreground` |
| Assistant message | Left-aligned, no bubble background, full width, `ChatMarkdownMessage` renders content |
| New message sent | Textarea clears; optimistic message appends at bottom; thread scrolls to bottom |
| Thread empty state | Centered message: "No messages yet" + instruction text (see Copywriting) |

### Chat Input

| Interaction | Behavior |
|-------------|---------|
| Type in textarea | Auto-resizes: `field-sizing: content` (fallback: scrollHeight calculation); max-height 160px before scrolling |
| Enter (no modifier) | Submit message via `chatApi.postMessage`; clears textarea |
| Shift+Enter | Insert newline; do not submit |
| Escape | Clear textarea content if non-empty; if already empty, do nothing |
| Send button click | Same as Enter; button is disabled when textarea is empty |

### Code Block Copy Button

| Interaction | Behavior |
|-------------|---------|
| Hover code block | Copy button (`Copy` icon) appears in top-right corner of `pre` block |
| Click copy | `navigator.clipboard.writeText(code)` executes; icon changes to `Check` for 1500ms; then reverts to `Copy` |
| Copy failure | Log to console; no user-visible error (local trusted mode) |

### Keyboard Shortcuts (INPUT-07)

| Shortcut | Scope | Action |
|----------|-------|--------|
| Enter | Chat input focused | Send message |
| Shift+Enter | Chat input focused | Insert newline |
| Escape | Chat input focused | Clear input |
| Cmd+K | Global | Opens search (Phase 24 — wire shortcut now, handler is no-op in Phase 21) |

---

## Copywriting Contract

| Element | Copy | Notes |
|---------|------|-------|
| Chat panel toggle button aria-label | "Open chat" / "Close chat" | Toggle based on `chatOpen` state |
| New conversation button | "New conversation" | Tooltip + aria-label |
| Conversation list empty state heading | "No conversations yet" | Shown when list is empty |
| Conversation list empty state body | "Start a conversation to get help from your agents." | Directs user toward action |
| Message thread empty state | "Send a message to start this conversation." | Shown when conversation exists but has no messages |
| Delete conversation dialog title | "Delete conversation?" | shadcn Dialog title |
| Delete conversation dialog body | "This conversation and all its messages will be permanently deleted." | Confirms scope |
| Delete confirmation button | "Delete" | `variant="destructive"` |
| Delete cancel button | "Keep conversation" | `variant="outline"` |
| Archive action label | "Archive" | Dropdown menu item |
| Pin action label | "Pin" / "Unpin" | Toggle based on pinned state |
| Rename action label | "Rename" | Dropdown menu item |
| Input placeholder | "Message your agent..." | Textarea placeholder |
| Send button aria-label | "Send message" | Icon-only button needs label |
| Copy code button aria-label | "Copy code" | Before copy; reverts after success |
| Copy code success aria-label | "Copied!" | 1500ms, then reverts |
| Title auto-generated pattern | First 60 chars of first message, truncated with ellipsis | Server-side; shown in list immediately |

**Tone:** Direct, functional, no corporate language. No exclamation marks except "Copied!" (which is a status, not marketing).

---

## States and Loading

| Component | Loading state | Empty state | Error state |
|-----------|--------------|-------------|-------------|
| `ChatConversationList` | 5x `Skeleton` rows (h-10, w-full, rounded) | "No conversations yet" with CTA | "Could not load conversations. Refresh to try again." |
| `ChatMessageList` | No skeleton — load is fast (persisted local server) | "Send a message to start this conversation." | "Could not load messages. Refresh to try again." |
| `ChatInput` send | Send button shows `Loader2` spinning icon while POST is in flight; disabled state | n/a | Toast: "Message failed to send. Try again." |

**Optimistic updates:** New conversations and new messages insert immediately into the UI before server confirmation. On failure, remove the optimistic item and show the error toast.

---

## Theme Integration Contract

Requirements THEME-01 and THEME-02 require zero new plumbing — `useTheme()` + `THEME_META` + existing CSS variables handle everything.

**Checklist:**
- All `ChatPanel` backgrounds use `var(--background)` and `var(--card)` — never hardcoded hex
- Conversation list hover uses `hover:bg-accent` — resolves correctly in all three themes
- Border colors use `border-border` — resolves correctly in all three themes
- `ChatMarkdownMessage` passes `THEME_META[theme].dark` to `rehype-highlight` theme selection
- Code block syntax highlight CSS: load via scoped CSS selector approach (`.dark .hljs` / `.theme-tokyo-night .hljs`), NOT via multiple `<link>` imports

---

## Accessibility

| Concern | Requirement |
|---------|-------------|
| Chat panel | `<aside>` with `aria-label="Chat"` |
| Conversation list | `<ul>` with `role="listbox"` or `<nav>`; active item has `aria-current="true"` |
| Message thread | `<ol>` with messages as `<li>`; `aria-live="polite"` on the list for new message announcements |
| Input submit | `<form>` wrapper with `onSubmit`; Enter key handled via form submission |
| Icon-only buttons | All have `aria-label` — see Copywriting Contract |
| Focus management | When chat panel opens, focus moves to the input textarea |
| Keyboard trap | Chat panel is NOT a modal — no focus trap; users navigate freely |
| Color contrast | All text uses existing CSS variables which are WCAG-AA compliant for their respective themes |

---

## Registry Safety

| Registry | Blocks Used | Safety Gate |
|----------|-------------|-------------|
| shadcn official | `scroll-area`, `skeleton`, `button`, `textarea`, `dialog`, `dropdown-menu`, `tooltip` (all already installed) | not required |
| Third-party | none | not applicable |

**No third-party registries used in Phase 21.** All shadcn components are already installed in `ui/src/components/ui/`. The one new package (`rehype-highlight`) is an npm package installed via `pnpm`, not a shadcn registry block.

---

## Animation and Motion

| Element | Animation | Duration | Easing |
|---------|-----------|----------|--------|
| Chat panel open/close | `transition-[width]` | 100ms | `ease-out` — matches sidebar |
| New message insert | No animation (Phase 21; streaming animations in Phase 22) | — | — |
| Copy button icon swap | CSS transition `opacity` | 150ms | `ease` |
| Conversation row highlight (new) | `activity-row-enter` keyframe (reuse existing) | 520ms | `cubic-bezier(0.16, 1, 0.3, 1)` |
| Skeleton loading | Tailwind `animate-pulse` (shadcn default) | continuous | — |

**Reduced motion:** Wrap `conversation-row-enter` in `@media (prefers-reduced-motion: reduce)` — matching the existing `activity-row-enter` guard in `index.css`.

---

## 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
