---
phase: 21-chat-foundation
plan: "04"
subsystem: ui
tags: [chat, context, components, layout, tdd]
dependency_graph:
  requires: ["21-00", "21-02"]
  provides: [ChatPanelContext, ChatPanel, ChatInput, ChatMessage]
  affects: [Layout, main.tsx]
tech_stack:
  added: []
  patterns: [Context + hook pattern with localStorage persistence, TDD with jsdom+createRoot]
key_files:
  created:
    - ui/src/context/ChatPanelContext.tsx
    - ui/src/components/ChatPanel.tsx
    - ui/src/components/ChatInput.tsx
    - ui/src/components/ChatMessage.tsx
  modified:
    - ui/src/components/ChatInput.test.tsx
    - ui/src/components/Layout.tsx
    - ui/src/main.tsx
decisions:
  - ChatPanelProvider inserted inside PanelProvider so ChatPanel can call setPanelVisible to close PropertiesPanel on open
  - Chat toggle button placed in desktop sidebar bottom controls with hidden md:inline-flex (same cluster as settings and theme)
  - ChatMessage uses plain text for user role (no markdown) and ChatMarkdownMessage for assistant role
metrics:
  duration: "~4 minutes"
  completed: "2026-04-01"
  tasks_completed: 2
  files_created: 4
  files_modified: 3
---

# Phase 21 Plan 04: Chat Panel Shell and Context Summary

**One-liner:** Chat drawer shell with ChatPanelProvider (localStorage persistence), auto-resize ChatInput (Enter/Shift+Enter/Escape), ChatMessage bubbles, and Layout integration toggling PropertiesPanel closed on open.

## Tasks Completed

| Task | Name | Commit | Files |
|------|------|--------|-------|
| 1 | Create ChatPanelContext, ChatInput, ChatMessage (TDD) | acfe2a7a | ChatPanelContext.tsx, ChatInput.tsx, ChatMessage.tsx, ChatInput.test.tsx |
| 2 | Create ChatPanel shell and wire into Layout + main.tsx | b1c9dbfb | ChatPanel.tsx, Layout.tsx, main.tsx |

## What Was Built

### ChatPanelContext (ui/src/context/ChatPanelContext.tsx)

Mirrors PanelContext pattern:
- `STORAGE_KEY = "nexus:chat-panel-open"` — localStorage persistence
- Default state: `false` (chat starts closed, unlike PropertiesPanel which defaults to visible)
- Exports `ChatPanelProvider` and `useChatPanel`
- Tracks `chatOpen`, `activeConversationId`, `setChatOpen`, `toggleChat`, `setActiveConversationId`

### ChatInput (ui/src/components/ChatInput.tsx)

- `<textarea>` with `[field-sizing:content]` for modern auto-resize + `useEffect` fallback via `scrollHeight`
- `max-h-[160px] min-h-[40px]` constraints
- `onKeyDown`: Enter (no Shift, no composing) → submit; Escape → clear; Shift+Enter → native newline
- Send `Button variant="ghost" size="icon"` with `aria-label="Send message"`
- Shows `Loader2 animate-spin` when `isSubmitting=true`; button disabled when empty or submitting

### ChatMessage (ui/src/components/ChatMessage.tsx)

- User role: right-aligned bubble with `bg-secondary px-3 py-2` — plain text (no markdown)
- Assistant/system role: full-width, rendered via `ChatMarkdownMessage`

### ChatPanel (ui/src/components/ChatPanel.tsx)

- `<aside aria-label="Chat">` with `hidden md:flex` (desktop only)
- Width animation: `style={{ width: chatOpen ? 380 : 0 }}` + `transition-[width] duration-100 ease-out`
- `min-w-[380px]` on inner containers prevents content collapse during animation
- Two-column: 160px conversation list placeholder (Plan 05) + flex thread area with `ScrollArea` + `ChatInput`
- Header with X close button (`setChatOpen(false)`)

### Layout.tsx Changes

- Added imports: `MessageSquare` from lucide, `ChatPanel`, `useChatPanel`
- `const { chatOpen, toggleChat } = useChatPanel()` in `Layout()`
- `useEffect` closes `PropertiesPanel` when `chatOpen` becomes true via `setPanelVisible(false)`
- Chat toggle `Button` with `aria-label="chat open/close"` in desktop sidebar bottom controls
- `<ChatPanel />` rendered before `<PropertiesPanel />` in the flex row

### main.tsx Changes

- `ChatPanelProvider` wraps app inside `PanelProvider` (ordering ensures ChatPanel can access PanelContext)

## Verification Results

- TypeScript: `pnpm --filter @paperclipai/ui exec -- tsc --noEmit` — passes (no errors)
- Tests: `pnpm vitest run ui/src/components/ChatInput.test.tsx` — 6/6 passing
- Acceptance criteria: all verified via grep checks

## Deviations from Plan

### Auto-fixed Issues

**1. [Rule 1 - Bug] Fixed createRoot warning in tests**
- **Found during:** Task 1 (TDD RED→GREEN)
- **Issue:** The afterEach cleanup was calling `createRoot(container)` on an already-rooted container, producing a React warning
- **Fix:** Track the root in a `let root` variable in the describe scope, unmount it in afterEach rather than creating a new root to unmount
- **Files modified:** ui/src/components/ChatInput.test.tsx
- **Commit:** acfe2a7a

None of the original plan tasks required architectural changes.

## Known Stubs

| File | Location | Description | Resolved by |
|------|----------|-------------|-------------|
| ui/src/components/ChatPanel.tsx | onSend handler | `console.log("send:", content)` — not wired to API | Plan 05 |
| ui/src/components/ChatPanel.tsx | Conversation list column | "No conversations yet" placeholder | Plan 05 |
| ui/src/components/ChatPanel.tsx | Message thread area | "Send a message to start this conversation." placeholder | Plan 05 |

These stubs are intentional — Plan 04 is the shell/skeleton. Plan 05 wires the API.

## Self-Check: PASSED
