aegiscode 3.1.10 → 3.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. package/bin/aegis.js +165 -0
  2. package/package.json +18 -86
  3. package/LICENSE +0 -21
  4. package/README.md +0 -394
  5. package/dist/main.js +0 -2171
  6. package/gui/run-gui.cjs +0 -13
  7. package/src/agent/Agent.ts +0 -903
  8. package/src/agent/SimpleAgent.ts +0 -48
  9. package/src/agent/index.ts +0 -54
  10. package/src/agent/orchestrator/AppBuilder.ts +0 -443
  11. package/src/agent/orchestrator/CouncilAgent.ts +0 -310
  12. package/src/agent/orchestrator/DiscussionRoom.ts +0 -462
  13. package/src/agent/orchestrator/OrchestratorAgent.ts +0 -637
  14. package/src/agent/orchestrator/index.ts +0 -38
  15. package/src/agent/orchestrator/utils.ts +0 -397
  16. package/src/agent/pricing.ts +0 -115
  17. package/src/agent/router.ts +0 -74
  18. package/src/agent/routerStats.ts +0 -121
  19. package/src/agent/types.ts +0 -318
  20. package/src/auth/login.ts +0 -383
  21. package/src/cli/config.ts +0 -189
  22. package/src/cli/index.ts +0 -17
  23. package/src/cli/middleware.ts +0 -119
  24. package/src/cli/types.ts +0 -75
  25. package/src/config/ConfigManager.ts +0 -587
  26. package/src/config/index.ts +0 -7
  27. package/src/config/types.ts +0 -584
  28. package/src/context/CompactionService.ts +0 -300
  29. package/src/context/ContextManager.ts +0 -450
  30. package/src/context/FileAnalyzer.ts +0 -267
  31. package/src/context/TokenCounter.ts +0 -265
  32. package/src/context/index.ts +0 -27
  33. package/src/context/storage/CacheStore.ts +0 -176
  34. package/src/context/storage/JSONLStore.ts +0 -201
  35. package/src/context/storage/MemoryStore.ts +0 -205
  36. package/src/context/storage/PersistentStore.ts +0 -327
  37. package/src/context/storage/index.ts +0 -9
  38. package/src/context/storage/pathUtils.ts +0 -114
  39. package/src/context/test.ts +0 -309
  40. package/src/context/types.ts +0 -268
  41. package/src/hooks/HookExecutor.ts +0 -434
  42. package/src/hooks/HookManager.ts +0 -596
  43. package/src/hooks/HookService.ts +0 -269
  44. package/src/hooks/Matcher.ts +0 -157
  45. package/src/hooks/index.ts +0 -63
  46. package/src/hooks/types.ts +0 -424
  47. package/src/main.tsx +0 -596
  48. package/src/mcp/HealthMonitor.ts +0 -150
  49. package/src/mcp/McpClient.ts +0 -491
  50. package/src/mcp/McpRegistry.ts +0 -321
  51. package/src/mcp/createMcpTool.ts +0 -251
  52. package/src/mcp/index.ts +0 -15
  53. package/src/mcp/server.ts +0 -334
  54. package/src/mcp/test-server.ts +0 -88
  55. package/src/mcp/test.ts +0 -372
  56. package/src/mcp/types.ts +0 -247
  57. package/src/memory/AgentMemoryBus.ts +0 -432
  58. package/src/memory/CloudSync.ts +0 -99
  59. package/src/memory/DriveSync.ts +0 -106
  60. package/src/memory/SharedMemory.ts +0 -951
  61. package/src/memory/index.ts +0 -14
  62. package/src/memory/machineFingerprint.ts +0 -40
  63. package/src/orchestrator/SubAgentMetadata.ts +0 -136
  64. package/src/prompts/builder.ts +0 -213
  65. package/src/prompts/default.ts +0 -144
  66. package/src/prompts/index.ts +0 -16
  67. package/src/prompts/plan.ts +0 -64
  68. package/src/prompts/test.ts +0 -78
  69. package/src/services/AnthropicChatService.ts +0 -341
  70. package/src/services/ChatService.ts +0 -347
  71. package/src/services/ClaudeCliChatService.ts +0 -256
  72. package/src/services/CloudSync.ts +0 -168
  73. package/src/services/CostLedger.ts +0 -211
  74. package/src/services/Heartbeat.ts +0 -135
  75. package/src/services/LearningCollector.ts +0 -291
  76. package/src/services/OllamaInstaller.ts +0 -342
  77. package/src/services/VersionChecker.ts +0 -445
  78. package/src/services/index.ts +0 -57
  79. package/src/services/streaming/OpenAIEventAdapter.ts +0 -126
  80. package/src/services/streaming/RenderingProfile.ts +0 -90
  81. package/src/services/streaming/StreamEventParser.ts +0 -181
  82. package/src/services/streaming/ThrottledRenderer.ts +0 -139
  83. package/src/services/streaming/TranscriptBuffer.ts +0 -574
  84. package/src/services/streaming/eventStatusMap.ts +0 -52
  85. package/src/services/streaming/index.ts +0 -46
  86. package/src/services/streaming/renderFormatting.ts +0 -79
  87. package/src/services/streaming/types.ts +0 -234
  88. package/src/skills/SkillLoader.ts +0 -126
  89. package/src/skills/SkillRegistry.ts +0 -366
  90. package/src/skills/index.ts +0 -48
  91. package/src/skills/types.ts +0 -146
  92. package/src/slash-commands/billing.ts +0 -70
  93. package/src/slash-commands/build.ts +0 -413
  94. package/src/slash-commands/builtinCommands.ts +0 -2733
  95. package/src/slash-commands/clone.ts +0 -242
  96. package/src/slash-commands/council.ts +0 -125
  97. package/src/slash-commands/custom/CustomCommandExecutor.ts +0 -143
  98. package/src/slash-commands/custom/CustomCommandLoader.ts +0 -251
  99. package/src/slash-commands/custom/CustomCommandRegistry.ts +0 -144
  100. package/src/slash-commands/custom/index.ts +0 -7
  101. package/src/slash-commands/debate.ts +0 -254
  102. package/src/slash-commands/gmail.ts +0 -105
  103. package/src/slash-commands/index.ts +0 -388
  104. package/src/slash-commands/mcpCommand.ts +0 -205
  105. package/src/slash-commands/types.ts +0 -201
  106. package/src/store/index.ts +0 -76
  107. package/src/store/selectors.ts +0 -246
  108. package/src/store/slices/appSlice.ts +0 -205
  109. package/src/store/slices/commandSlice.ts +0 -115
  110. package/src/store/slices/configSlice.ts +0 -46
  111. package/src/store/slices/focusSlice.ts +0 -64
  112. package/src/store/slices/index.ts +0 -9
  113. package/src/store/slices/sessionSlice.ts +0 -424
  114. package/src/store/streaming-buffer.ts +0 -425
  115. package/src/store/test.ts +0 -296
  116. package/src/store/types.ts +0 -274
  117. package/src/store/vanilla.ts +0 -186
  118. package/src/tools/builtin/bash.ts +0 -236
  119. package/src/tools/builtin/council.ts +0 -105
  120. package/src/tools/builtin/edit.ts +0 -213
  121. package/src/tools/builtin/glob.ts +0 -136
  122. package/src/tools/builtin/grep.ts +0 -263
  123. package/src/tools/builtin/index.ts +0 -61
  124. package/src/tools/builtin/memory.ts +0 -66
  125. package/src/tools/builtin/read.ts +0 -168
  126. package/src/tools/builtin/skill.ts +0 -97
  127. package/src/tools/builtin/snapshot.ts +0 -40
  128. package/src/tools/builtin/task.ts +0 -106
  129. package/src/tools/builtin/write.ts +0 -134
  130. package/src/tools/createTool.ts +0 -221
  131. package/src/tools/execution/ExecutionPipeline.ts +0 -263
  132. package/src/tools/execution/index.ts +0 -40
  133. package/src/tools/execution/stages/CacheStage.ts +0 -131
  134. package/src/tools/execution/stages/ConfirmationStage.ts +0 -203
  135. package/src/tools/execution/stages/DiscoveryStage.ts +0 -46
  136. package/src/tools/execution/stages/ExecutionStage.ts +0 -48
  137. package/src/tools/execution/stages/FormattingStage.ts +0 -44
  138. package/src/tools/execution/stages/HookStage.ts +0 -72
  139. package/src/tools/execution/stages/PermissionStage.ts +0 -287
  140. package/src/tools/execution/stages/PostHookStage.ts +0 -71
  141. package/src/tools/execution/stages/index.ts +0 -12
  142. package/src/tools/execution/test.ts +0 -266
  143. package/src/tools/execution/types.ts +0 -273
  144. package/src/tools/index.ts +0 -81
  145. package/src/tools/registry.ts +0 -304
  146. package/src/tools/schemas.ts +0 -109
  147. package/src/tools/test.ts +0 -220
  148. package/src/tools/types.ts +0 -175
  149. package/src/tools/validation/PermissionChecker.ts +0 -242
  150. package/src/tools/validation/SensitiveFileDetector.ts +0 -210
  151. package/src/tools/validation/index.ts +0 -11
  152. package/src/ui/App.tsx +0 -166
  153. package/src/ui/components/AegisInterface.tsx +0 -484
  154. package/src/ui/components/common/ChatSearch.tsx +0 -150
  155. package/src/ui/components/common/ErrorBoundary.tsx +0 -82
  156. package/src/ui/components/common/ExitMessage.tsx +0 -120
  157. package/src/ui/components/common/LoadingIndicator.tsx +0 -49
  158. package/src/ui/components/common/index.ts +0 -6
  159. package/src/ui/components/dialog/ConfirmationPrompt.tsx +0 -208
  160. package/src/ui/components/dialog/InteractiveSelector.tsx +0 -149
  161. package/src/ui/components/dialog/SetupWizard.tsx +0 -297
  162. package/src/ui/components/dialog/UpdatePrompt.tsx +0 -155
  163. package/src/ui/components/dialog/index.ts +0 -8
  164. package/src/ui/components/index.ts +0 -28
  165. package/src/ui/components/input/CommandSuggestions.tsx +0 -139
  166. package/src/ui/components/input/CustomTextInput.tsx +0 -220
  167. package/src/ui/components/input/InputArea.tsx +0 -361
  168. package/src/ui/components/input/PromptSuggestions.tsx +0 -66
  169. package/src/ui/components/input/index.ts +0 -6
  170. package/src/ui/components/layout/ChatStatusBar.tsx +0 -90
  171. package/src/ui/components/layout/ContextBar.tsx +0 -79
  172. package/src/ui/components/layout/MessageArea.tsx +0 -96
  173. package/src/ui/components/layout/MessageList.tsx +0 -647
  174. package/src/ui/components/layout/MessageSeparator.tsx +0 -26
  175. package/src/ui/components/layout/WelcomeMessage.tsx +0 -93
  176. package/src/ui/components/layout/index.ts +0 -7
  177. package/src/ui/components/markdown/CodeHighlighter.tsx +0 -292
  178. package/src/ui/components/markdown/MessageRenderer.tsx +0 -1211
  179. package/src/ui/components/markdown/index.ts +0 -8
  180. package/src/ui/components/markdown/parser.ts +0 -336
  181. package/src/ui/components/markdown/types.ts +0 -66
  182. package/src/ui/focus/FocusManager.ts +0 -137
  183. package/src/ui/focus/index.ts +0 -13
  184. package/src/ui/focus/types.ts +0 -54
  185. package/src/ui/focus/useFocus.ts +0 -75
  186. package/src/ui/hooks/index.ts +0 -11
  187. package/src/ui/hooks/useAgent.ts +0 -284
  188. package/src/ui/hooks/useCommandHistory.ts +0 -87
  189. package/src/ui/hooks/useCommandProcessor.ts +0 -443
  190. package/src/ui/hooks/useConfirmation.ts +0 -99
  191. package/src/ui/hooks/useCtrlCHandler.ts +0 -100
  192. package/src/ui/hooks/useInputBuffer.ts +0 -122
  193. package/src/ui/hooks/useTerminalSize.ts +0 -68
  194. package/src/ui/hooks/useTerminalWidth.ts +0 -5
  195. package/src/ui/hooks/useWindowedList.ts +0 -118
  196. package/src/ui/render-debugger.ts +0 -621
  197. package/src/ui/test.ts +0 -189
  198. package/src/ui/themes/ThemeManager.ts +0 -332
  199. package/src/ui/themes/aegisTheme.ts +0 -87
  200. package/src/ui/themes/darkTheme.ts +0 -87
  201. package/src/ui/themes/defaultTheme.ts +0 -85
  202. package/src/ui/themes/index.ts +0 -10
  203. package/src/ui/themes/lightTheme.ts +0 -89
  204. package/src/ui/themes/popularThemes.ts +0 -187
  205. package/src/ui/themes/types.ts +0 -130
  206. package/src/utils/clipboard.ts +0 -48
  207. package/src/utils/debug.ts +0 -43
  208. package/src/utils/environment.ts +0 -68
  209. package/src/utils/index.ts +0 -10
@@ -1,647 +0,0 @@
1
- /**
2
- * MessageList - renders messages with RAF-throttled streaming content.
3
- *
4
- * Owns scroll state internally — no split state with parent.
5
- * Handles PgUp/PgDn/Ctrl+W/Ctrl+S/Ctrl+Up/Down/Home/End via useInput.
6
- * No auto-scroll; user controls position fully via keyboard.
7
- *
8
- * Why the RAF loop?
9
- * vanillaStore.setState() triggers the subscription on every delta, forcing
10
- * full Ink reconciliation and a visible terminal "blink" at every tick.
11
- * The RAF loop polls the mutable buffer directly and only bumps a LOCAL
12
- * counter (streamingVersion) — only MessageList re-renders, nothing else.
13
- */
14
-
15
- import React, { useState, useEffect, useRef, useMemo } from 'react';
16
- import { Box, Text, useInput } from 'ink';
17
- import { MessageRenderer } from '../markdown/MessageRenderer.js';
18
- import { getState } from '../../../store/index.js';
19
- import { vanillaStore } from '../../../store/vanilla.js';
20
- import { getStreamingContent, isActiveStreamingMessage, getStreamingLatencyMs, getBufferedToolCalls } from '../../../store/streaming-buffer.js';
21
- import { themeManager } from '../../themes/index.js';
22
- import type { ContentBlock } from '../../../store/types.js';
23
-
24
- // ── Line estimation for line-based scrolling ──────────────────────────
25
-
26
- /** Rough estimate of rendered terminal lines for a text string. */
27
- function estimateContentLines(text: string, terminalWidth: number): number {
28
- if (!text) return 0;
29
- const w = Math.max(terminalWidth, 20);
30
- const raw = text.split('\n');
31
- let total = 0;
32
- for (const line of raw) {
33
- const l = line.length;
34
- total += l === 0 ? 1 : Math.max(1, Math.ceil(l / w));
35
- }
36
- return total;
37
- }
38
-
39
- /** Estimate total rendered lines for a message (role line + thinking + content + tool blocks). */
40
- function estimateMessageLines(
41
- content: string,
42
- thinking: string | undefined,
43
- terminalWidth: number,
44
- contentBlocks?: ContentBlock[],
45
- ): number {
46
- let total = 1; // role prefix line
47
-
48
- // Thinking section
49
- if (thinking && thinking.length > 0) {
50
- total += 1; // "thought" header
51
- total += estimateContentLines(thinking, terminalWidth);
52
- }
53
-
54
- // Structured content blocks (Claude Code style)
55
- if (contentBlocks && contentBlocks.length > 0) {
56
- for (const b of contentBlocks) {
57
- switch (b.type) {
58
- case 'thinking':
59
- total += 1; // "thought" header
60
- total += estimateContentLines(b.thinking || '', terminalWidth);
61
- break;
62
- case 'text':
63
- total += estimateContentLines(b.text || '', terminalWidth);
64
- break;
65
- case 'tool_use':
66
- total += 2; // ● line + at least one result line
67
- break;
68
- case 'tool_result':
69
- // ToolUseBlock folds results into their paired tool_use block and
70
- // truncates to a handful of sublines (see MessageRenderer.tsx) —
71
- // it's never rendered standalone or proportional to full content
72
- // length. Estimating it that way previously inflated the line
73
- // budget for messages with large tool output (file reads, bash
74
- // output), which could exhaust the entire windowing budget in one
75
- // message and silently drop every message after it — including
76
- // the next user message — from the visible render.
77
- total += Math.min(estimateContentLines(b.content || '', terminalWidth), 6);
78
- break;
79
- }
80
- }
81
- return total;
82
- }
83
-
84
- // Legacy markdown content
85
- total += estimateContentLines(content, terminalWidth);
86
- return total;
87
- }
88
-
89
- interface MessageListProps {
90
- terminalWidth: number;
91
- terminalHeight: number;
92
- /** Called when scroll state changes (for status bar indicator) */
93
- onScrolledUpChange?: (isScrolledUp: boolean) => void;
94
- /** Called with render latency (ms) during streaming — for status bar display */
95
- onRenderLatency?: (ms: number) => void;
96
- /** Number of queued commands shown below the message list (adjusts viewport budget) */
97
- pendingCommandCount?: number;
98
- /** Measured height (rows) of whatever renders below the message list besides the
99
- * fixed UI_OVERHEAD (e.g. ContextBar, QueuedCommands) — keeps the viewport budget
100
- * in sync with variable-height elements instead of clipping/dropping messages. */
101
- extraOverheadLines?: number;
102
- }
103
-
104
- const RAF_INTERVAL_MS = 30; // ~33fps redraws
105
- const CONTENT_THRESHOLD = 1; // re-render on every content character
106
- const THINKING_THRESHOLD = 1; // update thinking content every 1 char for real-time visibility
107
- const UI_OVERHEAD = 6; // rows for input area, status bar, etc.
108
- const MAX_HISTORY_MESSAGES = 200; // hard cap: only show last N completed messages
109
-
110
- export const MessageList: React.FC<MessageListProps> = React.memo(({
111
- terminalWidth,
112
- terminalHeight,
113
- onScrolledUpChange,
114
- onRenderLatency,
115
- pendingCommandCount = 0,
116
- extraOverheadLines = 0,
117
- }) => {
118
- // ==================== Internal Scroll State ====================
119
- const [messages, setMessages] = useState(() => getState().session.messages);
120
- const [showAllThinking, setShowAllThinking] = useState(() => vanillaStore.getState().app.showAllThinking);
121
- const [streamingVersion, setStreamingVersion] = useState(0);
122
-
123
- // Manual scroll offset — only meaningful while autoFollow is false.
124
- const [scrollLineOffset, setScrollLineOffset] = useState(0);
125
-
126
- // When true, the view always tracks the live bottom (recomputed fresh every
127
- // render from maxLineOffset) instead of a stored offset number. estimateMessageLines
128
- // is only an approximation of what Ink actually renders, so storing the "bottom"
129
- // as a literal offset let per-message estimate error compound across messages
130
- // until the stored value drifted from the true bottom — causing messages to
131
- // appear/disappear inconsistently after a handful of exchanges. Recomputing the
132
- // bottom fresh every render instead of trusting a remembered number makes it
133
- // self-correcting, regardless of estimate accuracy.
134
- const [autoFollow, setAutoFollow] = useState(true);
135
- const autoFollowRef = useRef(autoFollow);
136
- autoFollowRef.current = autoFollow;
137
-
138
- // Refs for values used in callbacks/RAF
139
- const messagesRef = useRef(messages);
140
- messagesRef.current = messages;
141
- const showAllThinkingRef = useRef(showAllThinking);
142
- showAllThinkingRef.current = showAllThinking;
143
- const terminalWidthRef = useRef(terminalWidth);
144
- terminalWidthRef.current = terminalWidth;
145
- const terminalHeightRef = useRef(terminalHeight);
146
- terminalHeightRef.current = terminalHeight;
147
- const onScrolledUpChangeRef = useRef(onScrolledUpChange);
148
- onScrolledUpChangeRef.current = onScrolledUpChange;
149
- const onRenderLatencyRef = useRef(onRenderLatency);
150
- onRenderLatencyRef.current = onRenderLatency;
151
- const scrollLineOffsetRef = useRef(scrollLineOffset);
152
- scrollLineOffsetRef.current = scrollLineOffset;
153
- const maxLineOffsetRef = useRef(0);
154
-
155
- // ==================== Computed values ====================
156
- const viewportLines = Math.max(terminalHeight - UI_OVERHEAD - extraOverheadLines, 5);
157
- const allCompleted = messages.filter(msg => !msg.isStreaming);
158
- const completedMessages = allCompleted.length > MAX_HISTORY_MESSAGES
159
- ? allCompleted.slice(-MAX_HISTORY_MESSAGES)
160
- : allCompleted;
161
- const streamingMsg = messages.find(msg => msg.isStreaming && isActiveStreamingMessage(msg));
162
- const buffer = streamingMsg ? getStreamingContent() : null;
163
-
164
- // Estimate line counts for each completed message
165
- const messageLines = useMemo(
166
- () => completedMessages.map(msg =>
167
- estimateMessageLines(msg.content, msg.thinking, terminalWidth, msg.contentBlocks),
168
- ),
169
- [completedMessages, terminalWidth],
170
- );
171
-
172
- // Prefix-sum of line offsets: lineOffsets[i] = lines before message i
173
- // lineOffsets[0] = 0, lineOffsets[N] = totalLines
174
- const lineOffsets = useMemo(() => {
175
- const offs: number[] = [0];
176
- for (let i = 0; i < messageLines.length; i++) {
177
- offs.push(offs[i] + messageLines[i]);
178
- }
179
- return offs;
180
- }, [messageLines]);
181
-
182
- // Account for streaming message lines in total/max offset.
183
- // During streaming, the message content lives in the mutable buffer (not the store),
184
- // so its estimated lines must be added separately. Without this, the scroll position
185
- // is computed as if the streaming message doesn't exist, causing auto-scroll to land
186
- // short and push user input off-screen.
187
- const streamingLines = streamingMsg && buffer
188
- ? estimateMessageLines(
189
- streamingMsg.content + buffer.content,
190
- (streamingMsg.thinking || '') + buffer.thinking,
191
- terminalWidth,
192
- streamingMsg.contentBlocks,
193
- )
194
- : 0;
195
-
196
- const totalLines = (lineOffsets[lineOffsets.length - 1] || 0) + streamingLines;
197
- const maxLineOffset = Math.max(0, totalLines - viewportLines);
198
- maxLineOffsetRef.current = maxLineOffset;
199
- // While auto-following, always use the freshly-computed bottom rather than a
200
- // stored offset — see the autoFollow declaration above for why.
201
- const clampedLineOffset = autoFollow ? maxLineOffset : Math.min(scrollLineOffset, maxLineOffset);
202
- const isAtBottom = clampedLineOffset >= maxLineOffset;
203
-
204
- // Find which message index corresponds to the current line offset
205
- const startMsgIndex = useMemo(() => {
206
- if (lineOffsets.length <= 1) return 0;
207
- // Binary search: largest i where lineOffsets[i] <= clampedLineOffset
208
- let lo = 0;
209
- let hi = lineOffsets.length - 1;
210
- while (lo < hi) {
211
- const mid = Math.floor((lo + hi + 1) / 2);
212
- if (lineOffsets[mid] <= clampedLineOffset) lo = mid;
213
- else hi = mid - 1;
214
- }
215
- return lo;
216
- }, [clampedLineOffset, lineOffsets]);
217
-
218
- // Clamp scrollLineOffset when viewport lines or total content changes
219
- useEffect(() => {
220
- setScrollLineOffset(prev => Math.min(prev, maxLineOffset));
221
- }, [maxLineOffset]);
222
-
223
- // Build visible window: messages whose lines fit within the viewport
224
- const useWindowing = totalLines > viewportLines;
225
-
226
- let visibleMessages = completedMessages;
227
- let showLastMsgOutside = false;
228
-
229
- if (useWindowing) {
230
- visibleMessages = [];
231
- let linesLeft = viewportLines;
232
- for (let i = startMsgIndex; i < completedMessages.length && linesLeft > 0; i++) {
233
- visibleMessages.push(completedMessages[i]);
234
- linesLeft -= messageLines[i];
235
- }
236
-
237
- // FIX: When the last completed message was excluded because a large
238
- // assistant message consumed the entire viewport budget, the user's
239
- // latest message silently disappears. Always guarantee the last
240
- // completed message is visible — either via the window itself or
241
- // via the pin-below mechanism (showLastMsgOutside). This is especially
242
- // critical when streaming, since showLastMsgOutside was previously
243
- // disabled during streaming (the !streamingMsg check), leaving the
244
- // user's just-sent message completely invisible.
245
- const windowReachesEnd = visibleMessages.length > 0 &&
246
- visibleMessages[visibleMessages.length - 1].id === completedMessages[completedMessages.length - 1]?.id;
247
-
248
- // Backfill: if the window doesn't reach the end but there's room
249
- // (linesLeft went negative from a too-large message), replace the last
250
- // visible message with the actual last completed one so the user can
251
- // always see their latest message.
252
- if (!windowReachesEnd && completedMessages.length > 0) {
253
- const lastMsg = completedMessages[completedMessages.length - 1];
254
- // If the window is non-empty, swap the last item for the very last
255
- // completed message (it's more important than the message at the
256
- // window boundary). If empty, just add the last message.
257
- if (visibleMessages.length > 0) {
258
- // Remove the last item to free its budget, then add the real last message
259
- const freedLines = messageLines[completedMessages.indexOf(visibleMessages[visibleMessages.length - 1])];
260
- linesLeft += freedLines;
261
- visibleMessages[visibleMessages.length - 1] = lastMsg;
262
- linesLeft -= messageLines[completedMessages.length - 1];
263
- } else {
264
- visibleMessages.push(lastMsg);
265
- }
266
- }
267
-
268
- // Pin the latest message below the window when scrolled up, so the user
269
- // never loses track of "what just happened" while reading history — but
270
- // only if the window doesn't already end on it, otherwise it'd render twice.
271
- showLastMsgOutside = !isAtBottom && completedMessages.length > 0 && !windowReachesEnd;
272
- }
273
-
274
- // ==================== Notify parent about scroll state ====================
275
- const prevScrolledUpRef = useRef(false);
276
- useEffect(() => {
277
- const isScrolledUp = useWindowing && !isAtBottom;
278
- if (isScrolledUp !== prevScrolledUpRef.current) {
279
- prevScrolledUpRef.current = isScrolledUp;
280
- onScrolledUpChangeRef.current?.(isScrolledUp);
281
- }
282
- }, [useWindowing, isAtBottom]);
283
-
284
- // ==================== Keyboard Scrolling ====================
285
- // RAF-batched scroll commits — same fix as the streaming RAF loop above:
286
- // every setState() here forces a full Ink reconciliation (visible blink).
287
- // Held keys / terminal key-repeat can fire many discrete events per second;
288
- // without batching, each one triggers its own redraw. Coalescing into a
289
- // single commit per animation frame makes fast/held scrolling smooth
290
- // instead of strobing.
291
- const pendingScrollOffsetRef = useRef<number | null>(null);
292
- const pendingAutoFollowRef = useRef<boolean | null>(null);
293
- const scrollFlushScheduledRef = useRef(false);
294
-
295
- function flushScroll() {
296
- scrollFlushScheduledRef.current = false;
297
- if (pendingScrollOffsetRef.current !== null) {
298
- setScrollLineOffset(pendingScrollOffsetRef.current);
299
- pendingScrollOffsetRef.current = null;
300
- }
301
- if (pendingAutoFollowRef.current !== null) {
302
- setAutoFollow(pendingAutoFollowRef.current);
303
- pendingAutoFollowRef.current = null;
304
- }
305
- }
306
-
307
- function scheduleScrollFlush() {
308
- if (scrollFlushScheduledRef.current) return;
309
- scrollFlushScheduledRef.current = true;
310
- requestAnimationFrame(flushScroll);
311
- }
312
-
313
- // Manual scroll-up moves: leave autoFollow, basing the new offset on the
314
- // live bottom (maxLineOffsetRef) if we were following, or the stored
315
- // offset otherwise. Reads/writes the pending ref (not state) so repeated
316
- // calls within the same frame accumulate correctly before the flush.
317
- function scrollUpBy(amount: number) {
318
- const wasFollowing = pendingAutoFollowRef.current ?? autoFollowRef.current;
319
- const base = wasFollowing
320
- ? maxLineOffsetRef.current
321
- : pendingScrollOffsetRef.current ?? scrollLineOffsetRef.current;
322
- pendingAutoFollowRef.current = false;
323
- pendingScrollOffsetRef.current = Math.max(0, base - amount);
324
- scheduleScrollFlush();
325
- }
326
-
327
- // Manual scroll-down moves: re-enable autoFollow once we reach the bottom.
328
- function scrollDownBy(amount: number) {
329
- const wasFollowing = pendingAutoFollowRef.current ?? autoFollowRef.current;
330
- const base = wasFollowing
331
- ? maxLineOffsetRef.current
332
- : pendingScrollOffsetRef.current ?? scrollLineOffsetRef.current;
333
- const max = maxLineOffsetRef.current;
334
- const next = Math.min(max, base + amount);
335
- pendingScrollOffsetRef.current = next;
336
- pendingAutoFollowRef.current = next >= max;
337
- scheduleScrollFlush();
338
- }
339
-
340
- useInput((_input, key) => {
341
- // Page Up — also handle Ctrl+W since terminals often intercept
342
- // PageUp/PageDown for their own scrollback buffer.
343
- if (key.pageUp || (key.ctrl && _input === '\x17')) {
344
- scrollUpBy(viewportLines);
345
- return;
346
- }
347
- if (key.pageDown || (key.ctrl && _input === '\x13')) {
348
- scrollDownBy(viewportLines);
349
- return;
350
- }
351
- // Half-page scroll (vim Ctrl+U / Ctrl+D)
352
- if (key.ctrl && _input === '\x15') {
353
- scrollUpBy(Math.max(1, Math.floor(viewportLines / 2)));
354
- return;
355
- }
356
- if (key.ctrl && _input === '\x04') {
357
- scrollDownBy(Math.max(1, Math.floor(viewportLines / 2)));
358
- return;
359
- }
360
- if (key.upArrow && key.ctrl) {
361
- scrollUpBy(1);
362
- return;
363
- }
364
- if (key.downArrow && key.ctrl) {
365
- scrollDownBy(1);
366
- return;
367
- }
368
- if (key.home) {
369
- pendingAutoFollowRef.current = false;
370
- pendingScrollOffsetRef.current = 0;
371
- scheduleScrollFlush();
372
- return;
373
- }
374
- if (key.end) {
375
- pendingAutoFollowRef.current = true;
376
- scheduleScrollFlush();
377
- return;
378
- }
379
- });
380
-
381
- // ==================== RAF Streaming Loop ====================
382
-
383
- const pendingMessagesRef = useRef<typeof messages | null>(null);
384
- const lastContentLenRef = useRef<Record<string, { content: number; thinking: number }>>({});
385
- const rafActiveRef = useRef(false);
386
- const rafIdRef = useRef<ReturnType<typeof requestAnimationFrame> | null>(null);
387
- const lastRafTimeRef = useRef(0);
388
-
389
- function ensureRafRunning() {
390
- if (rafActiveRef.current) return;
391
- rafActiveRef.current = true;
392
-
393
- const flushTick = (now: number) => {
394
- if (!rafActiveRef.current) return;
395
- if (now - lastRafTimeRef.current < RAF_INTERVAL_MS) {
396
- rafIdRef.current = requestAnimationFrame(flushTick);
397
- return;
398
- }
399
- lastRafTimeRef.current = now;
400
-
401
- // 1. Apply pending messages in a single batch
402
- if (pendingMessagesRef.current) {
403
- const newMessages = pendingMessagesRef.current;
404
- pendingMessagesRef.current = null;
405
-
406
- setMessages(newMessages);
407
- }
408
-
409
- // 2. Check and apply buffered streaming content
410
- const buf = getStreamingContent();
411
- const state = vanillaStore.getState();
412
- const streaming = state.session.messages.find(m => m.isStreaming && isActiveStreamingMessage(m));
413
-
414
- if (buf && streaming) {
415
- const lastLen = lastContentLenRef.current[streaming.id] || { content: 0, thinking: 0 };
416
-
417
- if (lastLen.content > buf.content.length || lastLen.thinking > buf.thinking.length) {
418
- lastContentLenRef.current[streaming.id] = { content: 0, thinking: 0 };
419
- }
420
-
421
- const updatedLastLen = lastContentLenRef.current[streaming.id] || { content: 0, thinking: 0 };
422
- const deltaContent = buf.content.length - updatedLastLen.content;
423
- const deltaThinking = buf.thinking.length - updatedLastLen.thinking;
424
- const thinkingJustStarted = updatedLastLen.thinking === 0 && buf.thinking.length > 0;
425
-
426
- if (deltaContent >= CONTENT_THRESHOLD || deltaThinking >= THINKING_THRESHOLD || thinkingJustStarted) {
427
- lastContentLenRef.current[streaming.id] = {
428
- content: buf.content.length,
429
- thinking: buf.thinking.length,
430
- };
431
- setStreamingVersion(v => v + 1);
432
-
433
- // While autoFollow is true, the bottom is recomputed fresh every render
434
- // (see clampedLineOffset above) — no offset bump needed here.
435
-
436
- // Report render latency to status bar
437
- const latency = getStreamingLatencyMs();
438
- if (latency > 30) {
439
- onRenderLatencyRef.current?.(latency);
440
- }
441
- }
442
- }
443
-
444
- // Keep polling if there's pending work.
445
- // Read `stillStreaming` FRESH from both the buffer and store — the captured
446
- // `streaming` variable (line 287) is stale by the time we reach here if
447
- // finishStreamingMessage fired between our read and this check.
448
- // Race condition: clearBuffer() + set({isStreaming:false}) can flip both
449
- // sources synchronously between our getStreamingContent() and store reads
450
- // on different lines. Single-expression evaluation avoids this window.
451
- const stillHasPending = pendingMessagesRef.current !== null;
452
- const liveStoreState = vanillaStore.getState();
453
- const liveStreaming = liveStoreState.session.messages.find(
454
- m => m.isStreaming && isActiveStreamingMessage(m),
455
- );
456
- const stillStreaming = !!(getStreamingContent() && liveStreaming);
457
- if (stillHasPending || stillStreaming) {
458
- rafIdRef.current = requestAnimationFrame(flushTick);
459
- } else {
460
- rafActiveRef.current = false;
461
- rafIdRef.current = null;
462
- }
463
- };
464
-
465
- rafIdRef.current = requestAnimationFrame(flushTick);
466
- }
467
-
468
- // ==================== Store Subscription ====================
469
- useEffect(() => {
470
- const unsub = vanillaStore.subscribe((state) => {
471
- const newMessages = state.session.messages;
472
- const newShowAllThinking = state.app.showAllThinking;
473
- const prevMessages = messagesRef.current;
474
-
475
- let messagesChanged = false;
476
-
477
- if (newMessages.length !== prevMessages.length) {
478
- messagesChanged = true;
479
- } else {
480
- for (let i = 0; i < newMessages.length; i++) {
481
- const a = prevMessages[i];
482
- const b = newMessages[i];
483
- if (a.id !== b.id || a.isStreaming !== b.isStreaming || a.content !== b.content || a.thinking !== b.thinking || a.contentBlocks !== b.contentBlocks) {
484
- messagesChanged = true;
485
- break;
486
- }
487
- }
488
- }
489
-
490
- if (messagesChanged) {
491
- // Auto-scroll to bottom when new content arrives.
492
- // Re-enables follow mode when:
493
- // - The last new message is a user message (just submitted)
494
- // - The last new message is streaming (just started streaming)
495
- // - The user was already following (so they keep following the conversation)
496
- // No offset math here — autoFollow makes the render-time clampedLineOffset
497
- // track the live bottom every render, so there's nothing to compute or store.
498
- const prevLen = prevMessages.length;
499
- const newLen = newMessages.length;
500
- if (newLen > prevLen) {
501
- const lastNew = newMessages[newLen - 1];
502
- if (lastNew?.role === 'user' || lastNew?.isStreaming || autoFollowRef.current) {
503
- setAutoFollow(true);
504
- }
505
- }
506
-
507
- // Always flow through RAF — never bypass with pendingMessagesRef.current = null.
508
- // The bypass path caused user messages to be lost when a streaming message finished
509
- // and a new user message arrived in the same synchronous tick.
510
- pendingMessagesRef.current = [...newMessages];
511
- ensureRafRunning();
512
- }
513
-
514
- // Start RAF loop if new streaming message appeared
515
- if (newMessages.length > 0 && newMessages[newMessages.length - 1].isStreaming) {
516
- ensureRafRunning();
517
- }
518
-
519
- // showAllThinking is safe to apply immediately
520
- if (newShowAllThinking !== showAllThinkingRef.current) {
521
- setShowAllThinking(newShowAllThinking);
522
- }
523
-
524
- newMessages.forEach(msg => {
525
- if (!msg.isStreaming) delete lastContentLenRef.current[msg.id];
526
- });
527
- });
528
-
529
- return () => {
530
- rafActiveRef.current = false;
531
- if (rafIdRef.current !== null) cancelAnimationFrame(rafIdRef.current);
532
- rafIdRef.current = null;
533
- unsub();
534
- };
535
- // eslint-disable-next-line react-hooks/exhaustive-deps
536
- }, []);
537
-
538
- // ==================== Render ====================
539
-
540
- return (
541
- <Box flexDirection="column">
542
- {/* Scroll-up indicator — shows lines above current viewport */}
543
- {useWindowing && !isAtBottom && !streamingMsg && (
544
- <Box>
545
- <Text color={themeManager.getTheme().colors.text.muted} dimColor>↑ {clampedLineOffset} lines above</Text>
546
- </Box>
547
- )}
548
-
549
- {/* Visible messages */}
550
- {visibleMessages.map((msg) => (
551
- <MessageRenderer
552
- key={msg.id}
553
- content={msg.content}
554
- role={msg.role}
555
- terminalWidth={terminalWidth}
556
- showPrefix={true}
557
- thinking={msg.thinking}
558
- isStreaming={false}
559
- showAllThinking={showAllThinking}
560
- contentBlocks={msg.contentBlocks}
561
- />
562
- ))}
563
-
564
- {/* Streaming message */}
565
- {streamingMsg && (() => {
566
- // Merge content from store + buffer into a single render surface.
567
- // During streaming, text/thinking live in the mutable buffer as raw strings
568
- // but the store's contentBlocks array doesn't get text/thinking blocks in
569
- // real-time (only tool_use blocks are added via addContentBlock).
570
- //
571
- // To give ContentBlockRenderer the full picture, we:
572
- // 1. Keep store tool_use/tool_result blocks (with buffer-merged tool args)
573
- // 2. Synthesize ONE text block from the entire combined text content
574
- // 3. Synthesize ONE thinking block from the entire combined thinking
575
- const combinedContent = buffer ? streamingMsg.content + buffer.content : streamingMsg.content;
576
- const combinedThinking = buffer ? (streamingMsg.thinking || '') + buffer.thinking : streamingMsg.thinking;
577
- const storeBlocks = streamingMsg.contentBlocks || [];
578
- const bufferToolCalls = buffer ? getBufferedToolCalls() : [];
579
-
580
- // Merge buffer tool call arguments into store tool_use blocks
581
- // (buffer accumulates args via input_json_delta, store block starts as '')
582
- const enhancedBlocks = storeBlocks.map(b => {
583
- if (b.type === 'tool_use') {
584
- const buffered = bufferToolCalls.find(tc => tc.id === b.id);
585
- if (buffered && buffered.arguments) {
586
- return { ...b, input: buffered.arguments };
587
- }
588
- }
589
- return b;
590
- });
591
-
592
- // Filter out store text/thinking blocks — replace with combined versions
593
- const nonTextBlocks = enhancedBlocks.filter(b => b.type !== 'text' && b.type !== 'thinking');
594
- const mergedBlocks: ContentBlock[] = [];
595
-
596
- // Natural block order: thinking → text → tool_use → tool_result
597
- if (combinedThinking) {
598
- mergedBlocks.push({ type: 'thinking', thinking: combinedThinking });
599
- }
600
- if (combinedContent) {
601
- mergedBlocks.push({ type: 'text', text: combinedContent });
602
- }
603
- mergedBlocks.push(...nonTextBlocks);
604
-
605
- return (
606
- <MessageRenderer
607
- key={streamingMsg.id}
608
- content={combinedContent}
609
- role={streamingMsg.role}
610
- terminalWidth={terminalWidth}
611
- showPrefix={true}
612
- thinking={combinedThinking}
613
- isStreaming={true}
614
- showAllThinking={showAllThinking}
615
- contentBlocks={mergedBlocks}
616
- />
617
- );
618
- })()}
619
-
620
- {/* Last completed message pinned at bottom when scrolled up — divider makes
621
- clear this jumps past whatever's between the window and the latest
622
- message, instead of looking like that content silently vanished. */}
623
- {showLastMsgOutside && (
624
- <Box>
625
- <Text color={themeManager.getTheme().colors.text.muted} dimColor>⋯ latest (End to jump here) ⋯</Text>
626
- </Box>
627
- )}
628
- {showLastMsgOutside && (
629
- <MessageRenderer
630
- key={'last-' + completedMessages[completedMessages.length - 1].id}
631
- content={completedMessages[completedMessages.length - 1].content}
632
- role={completedMessages[completedMessages.length - 1].role}
633
- terminalWidth={terminalWidth}
634
- showPrefix={true}
635
- thinking={completedMessages[completedMessages.length - 1].thinking}
636
- isStreaming={false}
637
- showAllThinking={showAllThinking}
638
- contentBlocks={completedMessages[completedMessages.length - 1].contentBlocks}
639
- />
640
- )}
641
- </Box>
642
- );
643
- });
644
-
645
- MessageList.displayName = 'MessageList';
646
-
647
- export default MessageList;
@@ -1,26 +0,0 @@
1
- /**
2
- * MessageSeparator - visual divider between messages
3
- */
4
-
5
- import React from 'react';
6
- import { Box, Text } from 'ink';
7
- import { themeManager } from '../../themes/index.js';
8
-
9
- interface MessageSeparatorProps {
10
- isLast: boolean;
11
- }
12
-
13
- export const MessageSeparator: React.FC<MessageSeparatorProps> = React.memo(({ isLast }) => {
14
- if (isLast) return null;
15
- const theme = themeManager.getTheme();
16
- return (
17
- <Box marginY={0}>
18
- <Text color={theme.colors.border.light} dimColor>
19
- {'\u2500'}{'\u00B7'}{'\u2500'}
20
- </Text>
21
- </Box>
22
- );
23
- });
24
- MessageSeparator.displayName = 'MessageSeparator';
25
-
26
- export default MessageSeparator;