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:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-06-08
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user