Files
boocode/openspec/changes/streaming-codeblocks-messages-components-v2/design.md
indifferentketchup 6fde7002aa 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
2026-06-08 03:49:26 +00:00

4.6 KiB

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.