Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 34 additions & 45 deletions coverage.txt
Original file line number Diff line number Diff line change
@@ -1,24 +1,19 @@
ℹ start of coverage report
ℹ ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
ℹ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
ℹ file | line % | branch % | funcs % | uncovered lines
ℹ ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
ℹ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
ℹ src | | | |
ℹ agent | | | |
ℹ react.js | 95.85 | 90.38 | 100.00 | 44-46 267-272 276-277 422-423 496 500-502 523-524 526-528
ℹ cache | | | |
ℹ llm_cache.js | 87.23 | 66.67 | 100.00 | 28-29 35-36 42-43
ℹ config | | | |
ℹ loader.js | 89.01 | 81.08 | 72.73 | 66-69 87-89 96 114 116 163-164 174-178 188-191
ℹ loader.js | 89.62 | 83.33 | 72.73 | 66-69 87-89 96 114 116 166-170 180-183
ℹ mutate.js | 54.72 | 100.00 | 0.00 | 11-15 25-37 48-53
ℹ schemas.js | 100.00 | 100.00 | 100.00 |
ℹ logger.js | 75.23 | 40.91 | 72.73 | 26-34 39 41-43 64-65 73-77 100-106 112-116 131 163-164 166-167 184-185 191-192 198-199 202-206 209-213 216
ℹ memory | | | |
ℹ context.js | 97.33 | 78.26 | 100.00 | 73-74
ℹ context.js | 98.21 | 82.76 | 100.00 | 110-111
ℹ expireEphemeral.js | 94.29 | 73.68 | 100.00 | 24-25 65-66
ℹ gc.js | 99.30 | 96.00 | 100.00 | 53
ℹ loadMemories.js | 93.70 | 80.00 | 100.00 | 89-90 92-93 95-96 98-99
ℹ profile.js | 98.86 | 96.30 | 100.00 | 74-75
ℹ prompts.js | 100.00 | 85.71 | 100.00 |
ℹ profile.js | 98.87 | 96.23 | 100.00 | 76-77
ℹ prompts.js | 100.00 | 100.00 | 100.00 |
ℹ reader.js | 95.35 | 78.57 | 100.00 | 22-23
ℹ provider | | | |
ℹ openai.js | 100.00 | 100.00 | 100.00 |
Expand All @@ -31,7 +26,7 @@
ℹ urlFilter.js | 100.00 | 93.75 | 100.00 |
ℹ scheduler | | | |
ℹ autoSchedule.js | 91.15 | 73.33 | 100.00 | 53-55 69-71 108-111
ℹ cron.js | 47.28 | 30.00 | 60.00 | 36-38 82-84 86-88 92-93 109-110 115-132 134-147 159-160 167-172 180-183 188-190 202-245 261-263 265-267 278-280 282-284 302-304 306-308 310-317 328-329 342-361 371-394 410-495
ℹ cron.js | 47.65 | 30.00 | 56.25 | 29-33 49-51 95-97 99-101 105-106 122-123 128-145 147-160 172-173 180-185 193-196 201-203 215-258 274-276 278-280 291-293 295-297 315-317 319-321 323-330 341-342 355-374 384-407 423-508
ℹ index.js | 100.00 | 100.00 | 100.00 |
ℹ scheduler.js | 88.55 | 89.66 | 81.82 | 87-99 129-130
ℹ session | | | |
Expand All @@ -45,50 +40,44 @@
ℹ stateManager.js | 100.00 | 100.00 | 100.00 |
ℹ window.js | 100.00 | 91.67 | 100.00 |
ℹ skills | | | |
ℹ discoverer.js | 97.50 | 87.27 | 100.00 | 155-156 188-190
ℹ registry.js | 65.81 | 60.00 | 46.67 | 38-78 108-109 126-127 135-144 155-157 160-162 188-194 202-206 214-218 225-226
ℹ discoverer.js | 96.33 | 87.72 | 100.00 | 61-66 173-174
ℹ registry.js | 78.23 | 55.56 | 50.00 | 46-49 52-54 108-109 126-127 135-144 155-157 160-162 188-194 202-206 214-218 225-226 240-247
ℹ types.js | 100.00 | 100.00 | 100.00 |
ℹ validator.js | 83.21 | 70.59 | 80.00 | 19-20 27-28 68 70 72-73 78 82-84 105-107 119-121 130-134
ℹ validator.js | 86.13 | 77.42 | 80.00 | 19-20 27-28 78 82-84 105-107 119-121 130-134
ℹ tools | | | |
ℹ clarify.js | 100.00 | 94.44 | 80.00 |
ℹ code.js | 100.00 | 89.13 | 92.31 |
ℹ common.js | 100.00 | 93.33 | 83.33 |
ℹ compact_context.js | 69.46 | 82.46 | 81.82 | 126-132 193-269 283-287 327-354 358-359 381-385
ℹ compaction.js | 63.29 | 100.00 | 50.00 | 67-103 118-138
ℹ cron.js | 95.00 | 90.00 | 75.00 | 84-85 97-98 219-220 222-233 237-243
ℹ clarify.js | 100.00 | 94.12 | 100.00 |
ℹ code.js | 100.00 | 81.25 | 100.00 |
ℹ common.js | 100.00 | 92.86 | 83.33 |
ℹ compact_context.js | 23.40 | 100.00 | 14.29 | 18-29 37-39 47-50 58-65 84-288 307-385
ℹ cron.js | 94.64 | 89.90 | 73.68 | 84-85 97-98 219-220 222-233 237-243
ℹ date.js | 100.00 | 100.00 | 100.00 |
ℹ filesystem.js | 93.40 | 82.61 | 80.00 | 44-45 107-110 171-178 189-190 195-207 396-397 421-422 439-443 446-447
ℹ image.js | 97.89 | 95.65 | 50.00 | 92-94
ℹ index.js | 100.00 | 100.00 | 100.00 |
ℹ memory.js | 97.60 | 83.78 | 93.75 | 55 98-99 194-198
ℹ moa.js | 100.00 | 96.77 | 80.00 |
ℹ sampling.js | 92.61 | 87.50 | 62.50 | 27 197 200 205-218
ℹ scanAgents.js | 100.00 | 83.33 | 100.00 |
ℹ session_search.js | 97.23 | 74.14 | 89.47 | 66-67 113-114 123 176-177
ℹ skills.js | 80.37 | 87.10 | 50.00 | 38-58 83-115 171-172 199-200 227-235 246-253 273-280 296-298 313-314 411-417
ℹ subAgent.js | 48.83 | 100.00 | 12.50 | 24-55 63-83 90-91 102-167 250-261 269-291 304-387
ℹ subAgentLog.js | 39.13 | 100.00 | 16.67 | 14-21 28-59 66-76 83-103 112-151
ℹ subAgentMessage.js | 38.14 | 100.00 | 50.00 | 11-70
ℹ terminal.js | 93.73 | 82.00 | 78.95 | 40-43 79 107-108 195-196 202-204 210-211 218-219 226-227 229
ℹ todo_logic.js | 100.00 | 98.21 | 100.00 |
ℹ todo_queue.js | 94.02 | 82.61 | 80.00 | 151-159 168 217 225-228
ℹ todo.js | 100.00 | 76.92 | 64.29 |
ℹ tts.js | 100.00 | 100.00 | 50.00 |
ℹ vision.js | 100.00 | 90.91 | 71.43 |
ℹ web.js | 95.57 | 70.83 | 60.00 | 24-25 39-40 43-45 86-88 123-125 189-191 317-318
ℹ image.js | 97.50 | 91.67 | 50.00 | 95-97
ℹ index.js | 100.00 | 93.94 | 100.00 |
ℹ memory.js | 96.52 | 83.56 | 93.33 | 55 98-99 194-198 298-300
ℹ moa.js | 100.00 | 94.44 | 84.62 |
ℹ sampling.js | 94.97 | 81.82 | 80.00 | 27 180-188
ℹ scanAgents.js | 100.00 | 80.00 | 100.00 |
ℹ session_search.js | 97.06 | 71.19 | 94.12 | 71-72 118-119 128 181-182
ℹ shell.js | 92.52 | 76.47 | 86.67 | 41-44 80 108-109 195-196 202-204 210-211 218-219 226-227 229
ℹ skills.js | 77.41 | 85.48 | 60.00 | 40-60 81-114 167-168 195-196 223-231 242-249 269-276 292-294 309-310
ℹ tts.js | 100.00 | 88.00 | 50.00 |
ℹ vision.js | 100.00 | 84.21 | 80.00 |
ℹ web.js | 95.14 | 71.25 | 62.50 | 27-28 42-43 46-48 89-91 126-128 192-194 325-326
ℹ tui | | | |
ℹ banner.js | 90.00 | 100.00 | 85.71 | 45-52
ℹ commandParser.js | 98.09 | 84.62 | 94.44 | 120-121 134-135
ℹ contextTokens.js | 70.49 | 75.00 | 100.00 | 26-43
ℹ conversationPanel.js | 84.38 | 62.96 | 76.47 | 86-97 102-109 114-125 172-183 271-273 293 330-333 344-348
ℹ conversationPanel.js | 100.00 | 100.00 | 100.00 |
ℹ inputPanel.js | 86.49 | 100.00 | 50.00 | 33-37
ℹ markdownText.js | 94.74 | 90.00 | 72.73 | 75 90-91 99-100 123-126
ℹ markdownText.js | 94.74 | 90.91 | 72.73 | 75 90-91 99-100 123-126
ℹ messageBubble.js | 86.67 | 66.67 | 69.23 | 137-142 162-171 176-183 188-195
ℹ messageList.js | 82.15 | 69.05 | 59.26 | 74 91-94 118-134 144-156 165 172-176 214 222 231-237 245-248 267-269 289 303-304 323-325 330-334
ℹ messages.js | 100.00 | 94.44 | 100.00 |
ℹ panels.js | 100.00 | 100.00 | 100.00 |
ℹ statusBar.js | 91.89 | 84.21 | 100.00 | 36-37 48-54
ℹ workspace | | | |
ℹ loadAgents.js | 100.00 | 87.50 | 100.00 |
ℹ ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
ℹ all files | 87.08 | 84.20 | 79.64 |
ℹ ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
ℹ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
ℹ all files | 86.97 | 82.66 | 80.68 |
ℹ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
ℹ end of coverage report
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
schema: spec-driven
created: 2026-07-18
reconciled: 2026-07-18
notes: |
Delta specs were archived without auto-sync on 2026-07-18 due to header mismatches in the
MODIFIED operations (delta used "Conversation panel" but main had different wording).
Reconciliation committed separately: all delta requirements were applied to main specs
with corrected header names matching the actual codebase (MessageList, not ConversationPanel).
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
## Context

The TUI conversation panel in `src/tui/app.js` and `src/tui/conversationPanel.js` uses a pattern where messages are stored in a React state array and updated by cloning the array and mutating the last element. This approach was the only viable option before the adoption of `ink-scroll-view` which replaced the custom virtualized renderer (`messages.js`). However, the pattern has become a liability:

- Every streaming tick clones the full messages array and updates the last element, creating GC pressure
- The `ConversationPanel` owns both rendering and scroll logic, making it 365 lines and tightly coupled
- When a message is added, all messages are re-rendered even though only the new message should render
- App.js contains ~12 scattered `setMessages(prev => {...})` call sites across `handleChat`, `handleCommand`, `handleInterrupt`, and `handleNewSession`

The `ink-scroll-view` library (already in use) provides virtualized scrolling, meaning the React tree no longer needs to track every line. This makes a component-based approach (one component per message) viable — the ScrollView handles virtualization efficiently.

## Goals / Non-Goals

**Goals:**
- Each message is a standalone `MessageBubble` component with its own `useState`
- `MessageList` provides an imperative API (`addMessage`, `updateMessage`, `clear`, `setMessages`)
- Scroll management moves from `ConversationPanel` to `MessageList`
- App.js replaces array-mutation state updates with imperative ref calls
- Streaming text appears in real-time; user messages appear immediately
- No visual regression — same colors, layout, behavior as current UI

**Non-Goals:**
- Changing message styling or visual design
- Adding new message types (images, cards, etc.)
- Adding message search or filtering
- Changing the session loading/persistence mechanism
- Adding message reactions, editing, or deletion
- Migrating away from React state in App.js entirely (messages state remains as sync boundary)

## Decisions

### Decision 1: Message Update via Pub/Sub Topics (Replaces Ref Callbacks)
Use a pub/sub topic system for streaming updates to individual bubbles. Each bubble subscribes to a unique topic (`msg-{id}`) on mount. When `MessageList.updateMessage(id, updates)` is called, it publishes to that topic. The subscribed bubble receives the update and appends to its local chunks state, triggering a re-render.

**Rationale:** The original plan used `React.forwardRef` + `useImperativeHandle` to expose an `update()` method on each bubble, with MessageList holding a `Map<string, RefObject>` of bubble refs. During implementation, a pub/sub architecture was adopted instead because:

1. **Simpler architecture:** No need for ref Map management, forwardRef on every bubble, or useImperativeHandle boilerplate. MessageList only needs to maintain its own data store and topic registry.
2. **No child refs in parent:** MessageList doesn't need to track bubble instances at all. Bubbles self-register their subscriptions.
3. **Natural 1-to-N support:** If a bubble were ever to have multiple subscribers, pub/sub handles it trivially. Ref callback updates would need manual iteration.
4. **Test isolation:** Pub/sub topics can be verified independently of React rendering (see tests section).
5. **Decouples lifecycle:** Bubble unmount automatically unsubscribes. No need for cleanup of stale refs when messages are cleared or windowed.

**Alternatives considered:**
- **forwardRef + useImperativeHandle:** The original plan. More React-idiomatic for imperative child methods but requires ref tracking in parent, adds boilerplate, and tightly couples MessageList to bubble implementation details.
- **Context provider:** Pass a central update function via React Context. Adds overhead for a simple message→bubble pattern. Topics are more targeted.
- **Direct state in MessageList:** Keep all state in MessageList, pass props down to bubbles. Works for simple cases but makes streaming updates (cursor character cycling) require full list re-renders.

### Decision 2: Chunk Accumulation with Deduplication
Each `MessageBubble` tracks a `chunks` state (`useState([])`). Streaming updates arrive as string chunks via pub/sub. Each chunk is appended to the array (with deduplication: `if (prev[prev.length-1] === chunk) return prev`). The content rendered is `chunks.join('')`.

**Rationale:** Chunk-based accumulation avoids string concatenation on every render tick. Deduplication prevents duplicate chars when the same content is published multiple times (handles race conditions in streaming handlers). Joining a small array is cheap.

**Alternatives considered:**
- **streamingId counter:** Track a separate counter state to force re-renders when content hasn't changed. Works but adds a useless state variable. Chunk append naturally triggers re-render while also carrying payload.
- **useEffect dependency:** Use a separate `key` prop. Changes component identity, losing scroll position. Not compatible with ink-scroll-view.

### Decision 3: MessageList Owns ScrollView and Scroll Logic
All scroll management (throttle, resize, manual scroll detection, keyboard scroll) moves from `ConversationPanel` to `MessageList`. `MessageList` exposes an internal scroll ref via `getScrollRef()`, which App.js uses for keyboard navigation.

**Rationale:** The ScrollView is part of `MessageList`'s visual output. Having MessageList own scroll behavior keeps the component self-contained. App.js accesses the ScrollView ref through the imperative handle (`messageListRef.current?.getScrollRef()`) rather than prop-based forwarding, which simplifies the ConversationPanel's prop interface.

**Alternatives considered:**
- **Prop-based scrollRef forwarding:** Pass `scrollRef` as a prop from App.js → ConversationPanel → MessageList. More explicit but requires threading through an extra layer.
- **Keep scroll in ConversationPanel:** Would require passing MessageList ref to ConversationPanel for scroll-to-bottom. More indirection and tighter coupling.

### Decision 4: Keep messages useState in App.js as Sync Boundary
The `messages` state in App.js is not removed. It stays as the source of truth for session persistence and is synced into MessageList via an `initialize(msgs)` call when a session loads.

**Rationale:** MessageList needs to know what messages to display on session restore. The App.js → MessageList interface is: initial state via `setMessages()` on mount, subsequent updates via imperative methods. This hybrid approach is simpler than making MessageList the sole source of truth and dealing with session state synchronization complexity.

## Risks / Trade-offs

1. **[Risk: Pub/sub overhead]** Topics are created per message and stored in a Map. For long conversations with 100+ messages, this is 100 topic arrays in memory.
→ [Mitigation: Topics are lightweight (just event lists). Bubbles unsubscribe on unmount. For 100 visible messages this is negligible.]

2. **[Risk: Scroll regression]** The scroll behavior (throttle, manual-detection) is non-trivial. Moving it could introduce regressions.
→ [Mitigation: Port the scroll code verbatim from ConversationPanel.js, only changing the ref targets. Use the same throttling and content-hash patterns.]

3. **[Risk: Bubble ID stability]** Message IDs must be stable across renders for `updateMessage` to find the right bubble.
→ [Mitigation: Use a monotonic counter (assigned at add time) rather than random IDs. Map ID → data, not ref → ID.]

## Migration Plan

1. Create `messageBubble.js` and `messageList.js` with all new code
2. Update `conversationPanel.js` to be a thin wrapper
3. Update `app.js` — replace all `setMessages` with imperative calls
4. Run tests and fix any failures
5. Run `npm run lint` and fix any lint issues
6. Run `npm start` briefly to verify the app boots without errors

## Open Questions

1. Should the cursor character be sourced globally (context) or per-component (prop from MessageList from app props)?
2. Is a monotonic counter sufficient for bubble IDs, or should we use `crypto.randomUUID()` for uniqueness across session loads? (Answer for now: monotonic counter is simpler and sufficient within a session.)
3. How should `MessageBubble` handle empty content during streaming (just a blinking cursor)?
4. Should the legacy `MessageBubble` and `renderMessages` in `conversationPanel.js` be removed after this change, or kept as utilities? (Decision: kept as exports for backward compatibility, but they are unused.)
Loading