## 1. Contracts: Channel delta frame schema

- [x] 1.1 Add channel-delta frame types to `packages/contracts/src/ws-frames.ts`: `seq` field, `channel` discriminator (`text | tool_call | tool_result | status | error`), and per-channel payload types
- [x] 1.2 Update `WsFrameSchema` Zod schema with the new channel frame variants
- [x] 1.3 Regenerate downstream types and verify `ws-frames.test.ts` passes

## 2. Streaming layer: Channel-based reducer

- [x] 2.1 Implement `ChannelBuffer` — per-channel out-of-order frame buffer with contiguous-seq flush logic
- [x] 2.2 Rewrite `applyFrame` in `useSessionStream.ts` as a channel-dispatch reducer that fans out to channel buffers then merges into `StreamState`
- [x] 2.3 Add mid-stream reconnection protocol: client sends last `seq` per-channel on reconnect; server sends replay deltas or fallback snapshot
- [x] 2.4 Handle edge cases: empty delta, duplicate seq, channel stall timeout (5s force-snapshot fallback)
- [x] 2.5 Test: manual WS frame injection with out-of-order deltas, reconnect mid-stream, verify state consistency — 29 tests, all pass

## 3. CodeBlock Pro

- [x] 3.1 Add line-number gutter rendering (1-indexed, right-aligned, muted color, hidden for ≥1000 lines)
- [x] 3.2 Add diff-gutter mode: detect `diff-` language prefix, parse `+`/`-` markers, render green/red gutter classes
- [x] 3.3 Add theme toggle button (`github-dark` / `github-light`) with session-persisted choice
- [x] 3.4 Add word-wrap toggle button (default no-wrap, toggle to wrap with CSS `white-space: pre-wrap`)
- [x] 3.5 Add collapsible mode: auto-collapse ≥30 lines, show first 15 + "Show N more" button with gradient fade
- [x] 3.6 Add inline copy with progress states (check/red-X revert after 1200ms)
- [x] 3.7 Add LRU cache (`Map<code+theme+lang, html>`, max 50 entries) to avoid redundant Shiki calls
- [x] 3.8 Test: each feature toggles independently, Shiki fallback to plain `<pre>` on error

## 4. MessageList v2

- [x] 4.1 Replace flat `div` with `react-virtuoso` `Virtuoso` component, configure `followOutput="auto"` and overscan=5
- [x] 4.2 Preserve `isNearBottomRef` scroll-tracking logic for followOutput gating
- [x] 4.3 Add stagger entrance animation (40ms stagger between new items, max 500ms, 0-duration with `prefers-reduced-motion`)
- [x] 4.4 Refactor tool-call grouping: per-group independent expand/collapse state (not all-or-nothing)
- [x] 4.5 Implement message pin/bookmark: URL hash `#pin=<messageId>`, pin indicator bar at top with "Jump to pinned"
- [x] 4.6 Add `react-virtuoso` to `package.json` dependencies
- [x] 4.7 Test: 500-message transcript renders with ~20 DOM nodes, scroll-to-bottom on new stream, pin/unpin works

## 5. Component hardening

- [x] 5.1 Create `<MessageBoundary>` component that catches render errors and shows "Rendering failed" + Retry button
- [x] 5.2 Wrap `MarkdownRenderer` in `<MessageBoundary>`
- [x] 5.3 Wrap `CodeBlock` in `<MessageBoundary>` with plain `<pre>` fallback on Shiki failure
- [x] 5.4 Add streaming skeleton placeholder: `status === 'streaming'` with empty content renders pulse bar
- [x] 5.5 Add keyboard navigation to `ToolCallLine`: `Tab` focus, `Enter`/`Space` toggle, `Escape` collapse, visible focus ring — `tabIndex={0}`, `onKeyDown`, `focus-visible:ring-2`
- [x] 5.6 Add keyboard navigation to `ToolCallGroup`: focusable header, same keybindings
- [x] 5.7 Audit `ActionRow` buttons: every interactive element has matching `aria-label` and `title`
- [x] 5.8 Create `<MessageListErrorBoundary>` top-level wrapper for catastrophic failures

## 6. Build and verify

- [x] 6.1 Run `pnpm build` and fix any type/compile errors — PASSES (tsc -b + vite build)
- [x] 6.2 Run `npx tsc -p apps/web/tsconfig.app.json --noEmit` and fix any type errors — PASSES (no errors)
- [ ] 6.3 Smoke-test streaming with a real LLM turn (send message, verify streaming renders, tool calls, complete) — requires running LLM environment
- [ ] 6.4 Smoke-test code blocks (diff highlight, line numbers, collapse, copy, theme toggle) — requires running environment
- [ ] 6.5 Smoke-test message list (scrolling, pin, error boundary injection) — requires running environment
