docs: boocode-lift-analysis, openspec change docs, codesight cache, deps

- Add boocode-lift-analysis.md: comprehensive 30-repo lift matrix across 25 domains
- Add openspec/ change docs: domain2-code-intelligence, domain3-multi-agent, impeccable-wave, streaming-codeblocks
- Update .gitignore: .impeccable/, .omo/, bun.lock, DESIGN.md, PRODUCT.md
- Update dependencies in package.json + pnpm-lock.yaml
- Update .codesight/ analysis cache
This commit is contained in:
2026-06-08 03:49:26 +00:00
parent 50de80ee75
commit 6fde7002aa
29 changed files with 3624 additions and 138 deletions

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-06-08

View File

@@ -0,0 +1,45 @@
## Context
BooCode's chat UI streams LLM responses over a per-session WebSocket. The current `useSessionStream` hook uses a flat `applyFrame` reducer that maps WS frames to a `Message[]` array. This works for the basic case but has limitations: reconnection during streaming replaces the entire message list (snapshot-refetch), frame ordering depends on implicit WS ordering, and there's no concept of channel isolation between text, tool calls, and status.
`CodeBlock.tsx` wraps Shiki's async `codeToHtml` with basic copy and a language label. No line numbers, no diff markers, no theme switching.
`MessageList.tsx` does three sequential passes (flatten → group → stampCapHits) on every message array. It uses framer-motion `fadeSlideIn` for all items with no virtualization — beyond ~200 messages the DOM cost is noticeable on mobile.
The component suite (`MarkdownRenderer`, `MessageBubble`, `ToolCallLine`, `ToolCallGroup`) evolved organically with inconsistent error handling and accessibility.
LangGraph (referenced conceptually, not as a dependency) uses typed channels with Pregel-style superstep execution, where each channel is a typed slot that receives deltas in deterministic order with checkpoints. We port the channel pattern — not the library.
## Goals / Non-Goals
**Goals:**
- Channel-based streaming reducer with typed deltas and ordered frame processing
- CodeBlock with line numbers, diff gutter, theme toggle, word-wrap, collapsible, inline copy progress
- MessageList with virtualized rendering for 500+ messages and smoother animations
- Error boundaries on all render-heavy components so one failure doesn't crash the list
- Keyboard-navigable tool calls in ToolCallLine/ToolCallGroup
- Consistent `aria-label` protocol across all action buttons
**Non-Goals:**
- Replacing Shiki with another highlighter (stays)
- Porting langgraphjs as a runtime dependency (pattern only)
- Server-side streaming changes outside the WS frame protocol
- Adding undo/redo for message mutations
- Full i18n of the UI
## Decisions
1. **Channel-based reducer over flat pattern matching.** Each WS frame type maps to a channel (`text`, `tool_call`, `tool_result`, `status`, `error`). Channels process independently but merge into a single `StreamState` via deterministic ordering rules. This allows mid-stream reconnection without full snapshot — missed deltas can be replayed per-channel.
2. **react-virtuoso over react-window.** `react-virtuoso` handles variable-height items natively (markdown renders at unpredictable heights), supports sticky headers, and has a built-in `followOutput` mode for streaming chat. `react-window` requires fixed heights or complex measurement.
3. **Diff mode via CSS gutter classes, not Shiki transformers.** Shiki's `codeToHtml` with `transformers` adds diff line metadata, but the current async-only pipeline makes it hard to cache both diff and plain renders. Instead, `CodeBlock` will detect a `diff-` language prefix, parse `+`/`-` line markers, and render gutter decorations via CSS classes — simpler and works with any Shiki theme.
4. **Error boundaries at two granularities.** `MarkdownRenderer` and `CodeBlock` each get a `<MessageBoundary>` wrapper that catches render errors and shows a "Rendering failed" fallback + retry button. A top-level `<MessageListErrorBoundary>` catches everything else and shows a compact "Something went wrong" bar. This prevents Shiki failures from taking down the entire transcript.
## Risks / Trade-offs
- **[Risk] Virtualized list breaks streaming cursor.** When a new message arrives mid-stream, react-virtuoso's `followOutput` may skip ahead if the user is scrolled up reading history. **Mitigation**: `followOutput="auto"` only scrolls when the user is within a threshold of the bottom; manual `scrollToIndex` on new assistant turns when `isNearBottomRef` is true.
- **[Risk] Channel ordering complexity.** Multiple channels processing frames out of order could produce inconsistent state (e.g., `tool_result` channel fires before the `tool_call` channel's frame arrives). **Mitigation**: Each channel frame carries a monotonic `seq` counter from the server; the reducer buffers out-of-order frames per-channel and flushes only when `seq` is contiguous.
- **[Risk] Shiki async loading delay.** First paint for a code block waits for Shiki's WASM + theme load. **Mitigation**: Keep the existing `Suspense` + skeleton fallback pattern; preload Shiki in the app shell via `import('shiki')` at bootstrap.
- **[Trade-off] No code-block caching.** Every re-render re-runs `codeToHtml`. For large files this is wasteful. A `Map<code+theme+lang, html>` LRU cache (max 50 entries) avoids recomputation without adding a dependency.

View File

@@ -0,0 +1,32 @@
## Why
The chat UI is the core surface of BooCode, but its streaming layer, code blocks, message list, and shared components have accumulated organic complexity without a unified overhaul. Streaming uses a basic `useSessionStream` + `applyFrame` reducer with manual exponential backoff; `CodeBlock.tsx` lacks line numbers, diff highlighting, and multi-theme support; `MessageList.tsx`'s three-pass pre-render (flatten → group → stampCapHits) works but doesn't virtualize or animate smoothly at scale; and shared components (`MarkdownRenderer`, `MessageBubble`, `ChatInput`, `ToolCallLine`) lack consistent error boundaries, loading skeletons, and accessibility. These together create the highest-impact surface for UX improvement.
## What Changes
- **Streaming layer v2**: Port LangGraph-inspired channel-based state management patterns into the `useSessionStream` reducer architecture — typed channel deltas, predictable frame ordering, interrupt/resume-aware message grafting, and reconnection that preserves mid-stream state instead of a full snapshot-refetch.
- **CodeBlock overhaul**: Add line numbers, diff-highlight mode (gutter markers for `+`/`-` lines), multi-theme toggle (light/dark), word-wrap toggle, collapsible long blocks, and inline copy with progress feedback. Shiki stays as the highlighter.
- **MessageList v2**: Virtualized rendering via `react-virtuoso` for long transcripts, smoother streaming entrance animations (staggered fade-slide per message), smarter tool-call grouping with collapse/expand per group, and message pin/bookmark support (local-only, persisted in URL hash).
- **Component consistency pass**: Error boundaries on `MarkdownRenderer` and `CodeBlock` so Shiki failures don't crash the entire message list; loading skeletons for streaming messages; accessible keyboard navigation in `ToolCallLine` and `ToolCallGroup`; consistent `aria-label` protocol across all `ActionRow` buttons.
## Capabilities
### New Capabilities
- `channel-streaming`: Channel-based streaming reducer inspired by LangGraph Pregel — typed channel deltas (`text`, `tool_call`, `tool_result`, `status`) map to structured states, interrupt/resume-aware mid-stream reconnection, and predictable frame ordering via monotonic seq counters.
- `code-block-pro`: Line numbers, diff gutter markers, multi-theme toggle, word-wrap toggle, collapsible blocks (≥30 lines auto-collapse), inline copy with progress.
- `message-list-v2`: Virtualized rendering for 500+ message transcripts, smarter tool-call grouping with per-group expand/collapse, stagger entrance animations, message pin/bookmark (local via URL hash).
- `component-hardening`: Error boundaries on `MarkdownRenderer`/`CodeBlock`, `Suspense` loading skeletons for streaming content, keyboard-navigable tool calls (`Tab`/`Enter`/`Arrow`), consistent `aria-label` convention.
### Modified Capabilities
*(None — no existing spec files in `openspec/specs/` are changing)*
## Impact
- **`apps/web/src/hooks/useSessionStream.ts`**: Core rewrite from flat `applyFrame` reducer to channel-based dispatch with typed channel deltas.
- **`apps/web/src/components/CodeBlock.tsx`**: Full rewrite — line numbering, diff mode, theme toggle, collapsible.
- **`apps/web/src/components/MessageList.tsx`**: Virtualized rendering, new entrance animation system, smarter grouping.
- **`apps/web/src/components/MessageBubble.tsx`**: Error boundaries, skeleton loading for streaming messages.
- **`apps/web/src/components/MarkdownRenderer.tsx`**: Wrap in error boundary, add `Suspense` fallback.
- **`apps/web/src/components/ToolCallLine.tsx`**, **`ToolCallGroup.tsx`**: Keyboard navigation, aria-labels.
- **`apps/web/package.json`**: New dep `@virtuoso-dev/react-virtuoso` for virtualized list.
- **`@boocode/contracts`**: New channel-delta frame types in `ws-frames.ts` schema for the streaming v2 protocol.

View File

@@ -0,0 +1,39 @@
## ADDED Requirements
### Requirement: Channel-based streaming reducer
The streaming layer SHALL dispatch WS frames into typed channels (`text`, `tool_call`, `tool_result`, `status`, `error`) instead of a flat `applyFrame` pattern match. Each channel SHALL produce deltas independently but merge into a single `StreamState` via a deterministic ordering function.
#### Scenario: Text delta dispatched to text channel
- **WHEN** a `delta` frame with `content` arrives
- **THEN** the text channel accumulates the chunk and the merged state appends it to the target message's content
#### Scenario: Tool call dispatched to tool_call channel
- **WHEN** a `tool_call` frame with a `tool_call` object arrives
- **THEN** the tool_call channel pushes it to `message.tool_calls` on the target message id
### Requirement: Mid-stream reconnection without snapshot
The hook SHALL support mid-stream reconnection by replaying missed deltas per-channel, using a monotonic `seq` counter on each frame. If the server cannot replay, a full `snapshot` frame SHALL be used as fallback.
#### Scenario: Reconnection with missed deltas
- **WHEN** the WebSocket reconnects during an active stream
- **THEN** the client sends the last processed `seq` per-channel and the server replays missed deltas
- **AND** if replay is unavailable, the server sends a `snapshot` frame and the reducer resets to that state
### Requirement: Out-of-order frame buffering
The reducer SHALL buffer out-of-order frames per-channel and flush them only when the sequence number is contiguous from the last known value.
#### Scenario: Frame arrives out of order
- **WHEN** a frame with `seq=5` arrives before `seq=4` for the same channel
- **THEN** the reducer holds `seq=5` in a per-channel buffer
- **AND** when `seq=4` arrives, both frames SHALL be applied in order
### Requirement: Channel frame schema in ws-frames
The `@boocode/contracts` ws-frames schema SHALL define typed channel delta frames with a `seq` field, a `channel` discriminator, and channel-specific payloads.
#### Scenario: Channel frame validates correctly
- **WHEN** a channel delta frame matches the schema
- **THEN** `WsFrameSchema.safeParse` returns success

View File

@@ -0,0 +1,59 @@
## ADDED Requirements
### Requirement: Line numbers
`CodeBlock` SHALL render line numbers in a left gutter for blocks with >1 line. Line numbers SHALL be 1-indexed, right-aligned, and muted in color (`text-muted-foreground`). Long files (≥1000 lines) SHALL not show line numbers by default (toggle override).
#### Scenario: Multi-line block shows line numbers
- **WHEN** a code block with 5 lines renders
- **THEN** a left gutter shows numbers 1 through 5
- **AND** each number is right-aligned and muted
### Requirement: Diff gutter markers
When the language prefix starts with `diff-` (e.g. `diff-typescript`), `CodeBlock` SHALL parse leading `+` / `-` / (space) characters on each line. Lines starting with `+` SHALL get a green left border and background tint. Lines starting with `-` SHALL get a red left border and background tint. Common lines SHALL render normally. The `+`/`-` prefix characters SHALL be stripped from the rendered text.
#### Scenario: Diff block renders with gutter markers
- **WHEN** a code block with language `diff-typescript` contains lines `+ const x = 1` and `- const y = 2`
- **THEN** the first line has a green gutter marker and shows `const x = 1`
- **AND** the second line has a red gutter marker and shows `const y = 2`
### Requirement: Multi-theme toggle
`CodeBlock` SHALL display a theme toggle button that switches between `github-dark` and `github-light`. The initial theme SHALL match the app's current color scheme. The user's choice SHALL persist for the session (no localStorage).
#### Scenario: User toggles theme
- **WHEN** the user clicks the theme toggle button
- **THEN** the code block re-renders with the alternate Shiki theme
- **AND** the button icon reflects the current theme
### Requirement: Word-wrap toggle
`CodeBlock` SHALL provide a word-wrap toggle button. When active, long lines wrap; when inactive, they overflow with horizontal scrolling. The default state SHALL be no-wrap (scroll). The toggle SHALL persist per-code-block for the session.
#### Scenario: User toggles word wrap
- **WHEN** the user clicks the word-wrap toggle
- **THEN** long lines wrap to the container width
- **AND** clicking again restores horizontal scroll
### Requirement: Collapsible long blocks
`CodeBlock` SHALL auto-collapse blocks with ≥30 lines, showing the first 15 lines and a "Show {N} more lines" button. Clicking expands the full block. The collapsed state SHALL be indicated by a gradient fade at the cutoff.
#### Scenario: Long block collapses
- **WHEN** a code block has 50 lines
- **THEN** the first 15 lines render with a fade gradient at the bottom
- **AND** a "Show 35 more lines" button appears below
- **AND** clicking the button expands all 50 lines
### Requirement: Inline copy with progress
The copy button SHALL show an animated check icon on success and a brief "Copied" label that resolves after 1200ms. On copy failure, the button SHALL briefly show a red X before reverting.
#### Scenario: Copy succeeds
- **WHEN** the user clicks Copy and the clipboard write succeeds
- **THEN** the button shows a check icon and "Copied" label for 1200ms
#### Scenario: Copy fails
- **WHEN** the user clicks Copy and the clipboard write fails
- **THEN** the button shows a red X for 1200ms before reverting

View File

@@ -0,0 +1,55 @@
## ADDED Requirements
### Requirement: Error boundary on MarkdownRenderer
`MarkdownRenderer` SHALL be wrapped in a `<MessageBoundary>` that catches render errors from remark-gfm or react-markdown and shows a compact "Content rendering failed" fallback with a retry button.
#### Scenario: Markdown render throws
- **WHEN** `react-markdown` or `remark-gfm` throws during rendering
- **THEN** the error boundary catches the exception and renders a fallback UI
- **AND** the fallback shows "Content rendering failed" with a Retry button
- **AND** clicking Retry re-mounts `MarkdownRenderer` with the same content
### Requirement: Error boundary on CodeBlock
`CodeBlock` SHALL be wrapped in a `<MessageBoundary>` that catches errors from Shiki's `codeToHtml` and renders a plain-text `<pre>` fallback with the source code.
#### Scenario: Shiki highlight fails
- **WHEN** `codeToHtml` throws (e.g., unknown language, WASM load failure)
- **THEN** the error boundary catches the exception and renders a plain `<pre>` block with the original code
### Requirement: Loading skeleton for streaming messages
Messages with `status === 'streaming'` and no content yet SHALL render a pulse-animated skeleton placeholder instead of an empty bubble.
#### Scenario: Streaming message starts with no content
- **WHEN** a `message_started` frame arrives with `role: 'assistant'`
- **THEN** a skeleton placeholder renders (animated pulse bar) until the first `delta` frame arrives with content
### Requirement: Keyboard-navigable ToolCallLine
`ToolCallLine` SHALL support full keyboard navigation: `Tab` to focus, `Enter`/`Space` to toggle expand/collapse, `Escape` to collapse if expanded.
#### Scenario: User navigates tool call via keyboard
- **WHEN** the user presses `Tab` to focus a `ToolCallLine`
- **THEN** a visible focus ring appears
- **AND** pressing `Enter` toggles expand/collapse
- **AND** pressing `Escape` collapses it if expanded
### Requirement: Keyboard-navigable ToolCallGroup
`ToolCallGroup` SHALL support the same keyboard navigation as `ToolCallLine`. The group header SHALL be the focusable element.
#### Scenario: User navigates tool group via keyboard
- **WHEN** the user presses `Tab` to focus a `ToolCallGroup` header
- **THEN** a visible focus ring appears
- **AND** pressing `Enter` toggles expand/collapse of the group
### Requirement: Consistent aria-label protocol
Every interactive element in `ActionRow` (Copy, Resend, Regenerate, Fork, Delete, Restore, Pin) SHALL have an `aria-label` attribute that matches its `title` text.
#### Scenario: Action row button has aria-label
- **WHEN** an ActionRow button renders
- **THEN** it SHALL have an `aria-label` matching its `title`
- **AND** the label SHALL be unique within the message bubble

View File

@@ -0,0 +1,49 @@
## ADDED Requirements
### Requirement: Virtualized rendering
`MessageList` SHALL use `react-virtuoso` to virtualize rendering of messages beyond the viewport. Only visible messages and a configurable overscan buffer (default 5 items above/below) SHALL be in the DOM. The virtualizer SHALL support variable-height items (Markdown renders at unpredictable heights).
#### Scenario: Long transcript renders with virtualization
- **WHEN** a chat has 500 messages
- **THEN** only ~20 messages are in the DOM at any time (viewport ± overscan)
- **AND** scrolling maintains correct scroll position
#### Scenario: Streaming follows bottom by default
- **WHEN** a new streaming message arrives and the user is near the bottom
- **THEN** the list auto-scrolls to follow the streaming content
- **AND** if the user has scrolled up reading history, auto-scroll is suppressed
### Requirement: Smarter tool-call grouping
Consecutive tool runs of the same name SHALL collapse into a grouped control with a count header (same as current behavior for ≥3). Additionally, each group SHALL support independent expand/collapse state (not just the current all-or-nothing). User can expand a group to see individual calls, or collapse them back.
#### Scenario: Grouped tool calls are individually expandable
- **WHEN** a tool group with 5 `view_file` calls renders
- **THEN** the collapsed group shows "5 view_file calls" with an expand chevron
- **AND** clicking expand shows each call as a separate item
- **AND** clicking collapse returns to the grouped header
### Requirement: Stagger entrance animations
New messages SHALL enter with a staggered fade-slide animation. Each new message SHALL animate with `opacity: 0 → 1, y: 8 → 0`. Consecutive new messages (from a batch delta or snapshot) SHALL stagger by 40ms between items, up to a max of 500ms total stagger.
#### Scenario: Batch of messages arrives
- **WHEN** 10 new messages arrive in a `snapshot` frame
- **THEN** each message fades in sequentially with a 40ms stagger between them
- **AND** animation is skipped (zero-duration) when `prefers-reduced-motion` is set
### Requirement: Message pin/bookmark
Users SHALL be able to pin a message by clicking a pin icon in the message's ActionRow. Pinned messages SHALL be tracked locally via URL hash (`#pin=<messageId>`). Only one message can be pinned at a time. A pinned indicator SHALL show at the top of the message list with a "Jump to pinned" link.
#### Scenario: User pins a message
- **WHEN** the user clicks the pin icon on a message
- **THEN** the URL hash updates to `#pin=<messageId>`
- **AND** a pin indicator appears at the top of the list with "Jump to pinned" link
- **AND** clicking the pin icon again unpins and clears the hash
#### Scenario: Pinned message navigates on click
- **WHEN** the user clicks "Jump to pinned"
- **THEN** the list scrolls to the pinned message
- **AND** the pinned message briefly highlights

View File

@@ -0,0 +1,53 @@
## 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