Skip to content
Open
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
75 changes: 32 additions & 43 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 | 88.14 | 100.00 | 61-66 173-174
ℹ registry.js | 78.23 | 57.89 | 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
ℹ 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 | 98.36 | 86.36 | 100.00 | 56 74
ℹ 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
ℹ messageBubble.js | 26.32 | 100.00 | 0.00 | 43-168
ℹ messageList.js | 68.91 | 40.00 | 41.18 | 53-55 75-85 99-112 119-120 127-130 145 168-169 171-175 177-190 204-212 221-223 227-229 233-235
ℹ 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 | 85.56 | 82.43 | 80.67 |
ℹ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
ℹ end of coverage report
160 changes: 159 additions & 1 deletion docs/FLOWS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Code Flows

Call chains and data flows for all primary code paths in the project, excluding the TUI (see [TUI_FLOWS.md](./TUI_FLOWS.md)).
Call chains and data flows for all primary code paths in the project.

## Table of Contents

Expand Down Expand Up @@ -30,6 +30,7 @@ Call chains and data flows for all primary code paths in the project, excluding
- [Profile Management](#profile-management)
- [Shutdown Flow](#shutdown-flow)
- [Additional Tool Flows](#additional-tool-flows)
- [Terminal User Interface (TUI)](#terminal-user-interface-tui)
- [File Dependencies](#file-dependencies)

## Application Startup
Expand Down Expand Up @@ -1039,6 +1040,163 @@ shutdownTelemetry()
└── [see Telemetry Initialization]
```

---

## Terminal User Interface (TUI)

**Entry:** `src/tui/app.js` → renders `<ConversationPanel>` within the Ink-based TUI shell.

The TUI message rendering pipeline is split across four modules: `conversationPanel` (wrapper + helpers), `messageList` (list management + scrolling), `messageBubble` (individual message rendering), and `messages` (utility functions).

### ConversationPanel

```
ConversationPanel({ scrollRef, height, messages })
├── Thin wrapper around MessageList
│ ├── Does NOT contain message data, scroll logic, or refs
│ └── Delegates all rendering to MessageList
├── Exports:
│ ├── formatTime(date) → "HH:MM"
│ ├── getRoleColors(role) → { label, content } color scheme
│ │ ├── user → { label: "green", content: "white" }
│ │ ├── assistant → { label: "cyan", content: "white" }
│ │ └── system → { label: "yellow", content: "yellow" }
│ ├── getBubbleStyle(role) → { alignment, border }
│ │ ├── user → { alignment: "flex-end", border: "green" }
│ │ ├── assistant → { alignment: "flex-start", border: "cyan" }
│ │ └── system → { alignment: "flex-start", border: "yellow" }
│ └── renderMessages(messages, assistantName, maxMessages)
│ ├── Returns empty state if no messages
│ ├── Slices to last maxMessages (if provided)
│ └── Maps each message → MessageBubble with:
│ ├── role, content, time, streaming, reasoningContent, activeToolCall, toolCallDisplay
│ └── key: "msg-" + globalIdx
└── Renders: <MessageList scrollRef height messages />
```

### MessageList

```
MessageList({ scrollRef, height, messages }, ref)
├── State:
│ ├── bubbles: MessageBubble[] — internal bubble state synced from messages prop
│ ├── internalScrollRef / externalScrollRef — scroll target
│ ├── isUserScrollingRef — tracks manual scroll-up to suppress auto-scroll
│ ├── bubbleRefs — Map<id, bubbleRef> for imperative updates
│ └── prevMessageCountRef — change detection for useEffect
├── Sync bubbles from messages prop:
│ ├── useEffect on [messages?.length]
│ ├── Skip if count unchanged (change detection)
│ ├── Map messages → bubble objects:
│ │ ├── id: `msg-${idx}`
│ │ ├── role, content, assistantName, time
│ │ ├── streaming, reasoningContent, activeToolCall, toolCallDisplay
│ │ └── time fallback: "HH:MM" from toLocaleTimeString
│ └── setBubbles(newBubbles)
├── Imperative methods (exposed via useImperativeHandle):
│ ├── addMessage(role, content, options) → id
│ │ ├── Creates new bubble with randomUUID()
│ │ ├── Appends to bubbles state
│ │ └── Returns bubble ID
│ ├── updateMessage(id, updates)
│ │ ├── Calls bubbleRef.update(updates) on the target bubble
│ │ └── Also updates bubble data in state for rendering consistency
│ └── clear() → sets bubbles to [], clears bubbleRefs
├── Auto-scroll to bottom:
│ ├── Detects new content via bubbles length change or streaming content hash
│ ├── Respects isUserScrollingRef — suppresses auto-scroll when user scrolled up
│ ├── Throttles scroll-to-bottom during active streaming (100ms interval)
│ ├── Uses setTimeout(scrollHandle, 0) for deferred scroll
│ └── Resets previousMessageCount and lastScrollTime on scroll
├── User scroll detection:
│ ├── Checks scrollRef.current.getScrollOffset() vs getMaxScrollOffset()
│ ├── If not at bottom (tolerance: 2 chars) → sets isUserScrollingRef = true
│ ├── Clears flag when user returns to bottom or streaming completes
├── Resize handling:
│ ├── Listens for process "resize" events
│ ├── Calls scrollRef.current.remeasure() on resize
│ └── Skips in CI environment
├── Windowing:
│ ├── MAX_RENDER_MESSAGES = 100
│ ├── Only renders last N bubbles: bubbles.slice(Math.max(0, bubbles.length - 100))
│ └── Prevents React tree degradation in long conversations
└── Renders: <ScrollView><MessageBubble>[]</ScrollView>
```

### MessageBubble

```
MessageBubble({ role, content: initialContent, assistantName, time: initialTime }, ref)
├── State (each bubble manages its own):
│ ├── content — message text
│ ├── streaming — boolean, shows cursor during streaming
│ ├── toolCallDisplay — formatted tool call output
│ ├── reasoningContent — assistant thinking/thought content
│ ├── activeToolCall — {name: string} for currently running tool
│ └── time — timestamp string (HH:MM)
├── Imperative update() via useImperativeHandle:
│ ├── Called by streaming handlers during message streaming
│ ├── Accepts partial updates: { content, streaming, toolCallDisplay, reasoningContent, activeToolCall }
│ └── Only updates provided fields (undefined fields are ignored)
├── Rendering:
│ ├── Outer Box: flexDirection "row", justifyContent based on bubble.alignment
│ │ ├── user → flex-end (right-aligned)
│ │ └── assistant/system → flex-start (left-aligned)
│ ├── Inner Box: borderStyle "round", borderColor based on role, maxWidth "90%"
│ │ ├── Header: [time] RoleLabel: (gray timestamp, bold role label)
│ │ ├── Content: MarkdownText component with content + streaming cursor (█)
│ │ ├── Reasoning (assistant only): "(thinking) ..." truncated at 200 chars
│ │ ├── Active tool call: "- Running: {toolName} ..."
│ │ └── Tool call display: formatted output with indentation
│ └── Wrapped in memo() for performance
└── Exports: default memo(MessageBubble)
```

### Messages Utility Module

```
messages.js utilities:
├── getRoleLabel(role, assistantName) → display label
│ ├── user → "You"
│ ├── assistant → assistantName || "Assistant"
│ └── system → "System"
├── formatMessage(message, assistantName) → "Label (timestamp)\ncontent"
├── isStreamingMessage(message) → message.streaming === true
├── countMessageLines(messages, lineWidth) → total line count for scroll height
│ ├── 2 lines per message (label + content start)
│ ├── content lines = ceil(contentLength / lineWidth)
│ └── +1 separator line per message
└── getToolCallLines(toolCallDisplay) → split("\n") array
```

### Data Flow: Streaming Message Update

```
Streaming event (on_chat_model_stream)
├── callback({ type: "text", text }) → app.js state update
├── state.conversation updated with streaming message
├── ConversationPanel receives new messages prop
├── MessageList detects messages?.length change
├── setBubbles(newBubbles) → triggers re-render
├── MessageBubble receives updated content prop
├── OR: updateMessage(id, { content: newText, streaming: true })
│ └── bubbleRef.update({ content, streaming }) → direct state update
└── Bubble re-renders with new content + streaming cursor (█)
```

### Scroll Behavior Summary

| Scenario | Auto-scroll? | Reason |
|----------|-------------|--------|
| New message arrives (not streaming) | Yes | User is assumed to want to see new content |
| Streaming in progress | Yes (throttled to 100ms) | Smooth cursor progression |
| User scrolled up during streaming | No | User is reading, don't interrupt |
| User scrolled up, streaming completes | No | User has opted out of auto-scroll |
| User returns to bottom | Yes | User wants to follow conversation |
| Resize event | No scroll, remeasure only | Layout changed, need to recalculate heights |

---

## File Dependencies

```
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-07-07
Loading
Loading