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,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
|
||||
Reference in New Issue
Block a user