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.
- package/bin/aegis.js +165 -0
- package/package.json +18 -86
- package/LICENSE +0 -21
- package/README.md +0 -394
- package/dist/main.js +0 -2171
- package/gui/run-gui.cjs +0 -13
- package/src/agent/Agent.ts +0 -903
- package/src/agent/SimpleAgent.ts +0 -48
- package/src/agent/index.ts +0 -54
- package/src/agent/orchestrator/AppBuilder.ts +0 -443
- package/src/agent/orchestrator/CouncilAgent.ts +0 -310
- package/src/agent/orchestrator/DiscussionRoom.ts +0 -462
- package/src/agent/orchestrator/OrchestratorAgent.ts +0 -637
- package/src/agent/orchestrator/index.ts +0 -38
- package/src/agent/orchestrator/utils.ts +0 -397
- package/src/agent/pricing.ts +0 -115
- package/src/agent/router.ts +0 -74
- package/src/agent/routerStats.ts +0 -121
- package/src/agent/types.ts +0 -318
- package/src/auth/login.ts +0 -383
- package/src/cli/config.ts +0 -189
- package/src/cli/index.ts +0 -17
- package/src/cli/middleware.ts +0 -119
- package/src/cli/types.ts +0 -75
- package/src/config/ConfigManager.ts +0 -587
- package/src/config/index.ts +0 -7
- package/src/config/types.ts +0 -584
- package/src/context/CompactionService.ts +0 -300
- package/src/context/ContextManager.ts +0 -450
- package/src/context/FileAnalyzer.ts +0 -267
- package/src/context/TokenCounter.ts +0 -265
- package/src/context/index.ts +0 -27
- package/src/context/storage/CacheStore.ts +0 -176
- package/src/context/storage/JSONLStore.ts +0 -201
- package/src/context/storage/MemoryStore.ts +0 -205
- package/src/context/storage/PersistentStore.ts +0 -327
- package/src/context/storage/index.ts +0 -9
- package/src/context/storage/pathUtils.ts +0 -114
- package/src/context/test.ts +0 -309
- package/src/context/types.ts +0 -268
- package/src/hooks/HookExecutor.ts +0 -434
- package/src/hooks/HookManager.ts +0 -596
- package/src/hooks/HookService.ts +0 -269
- package/src/hooks/Matcher.ts +0 -157
- package/src/hooks/index.ts +0 -63
- package/src/hooks/types.ts +0 -424
- package/src/main.tsx +0 -596
- package/src/mcp/HealthMonitor.ts +0 -150
- package/src/mcp/McpClient.ts +0 -491
- package/src/mcp/McpRegistry.ts +0 -321
- package/src/mcp/createMcpTool.ts +0 -251
- package/src/mcp/index.ts +0 -15
- package/src/mcp/server.ts +0 -334
- package/src/mcp/test-server.ts +0 -88
- package/src/mcp/test.ts +0 -372
- package/src/mcp/types.ts +0 -247
- package/src/memory/AgentMemoryBus.ts +0 -432
- package/src/memory/CloudSync.ts +0 -99
- package/src/memory/DriveSync.ts +0 -106
- package/src/memory/SharedMemory.ts +0 -951
- package/src/memory/index.ts +0 -14
- package/src/memory/machineFingerprint.ts +0 -40
- package/src/orchestrator/SubAgentMetadata.ts +0 -136
- package/src/prompts/builder.ts +0 -213
- package/src/prompts/default.ts +0 -144
- package/src/prompts/index.ts +0 -16
- package/src/prompts/plan.ts +0 -64
- package/src/prompts/test.ts +0 -78
- package/src/services/AnthropicChatService.ts +0 -341
- package/src/services/ChatService.ts +0 -347
- package/src/services/ClaudeCliChatService.ts +0 -256
- package/src/services/CloudSync.ts +0 -168
- package/src/services/CostLedger.ts +0 -211
- package/src/services/Heartbeat.ts +0 -135
- package/src/services/LearningCollector.ts +0 -291
- package/src/services/OllamaInstaller.ts +0 -342
- package/src/services/VersionChecker.ts +0 -445
- package/src/services/index.ts +0 -57
- package/src/services/streaming/OpenAIEventAdapter.ts +0 -126
- package/src/services/streaming/RenderingProfile.ts +0 -90
- package/src/services/streaming/StreamEventParser.ts +0 -181
- package/src/services/streaming/ThrottledRenderer.ts +0 -139
- package/src/services/streaming/TranscriptBuffer.ts +0 -574
- package/src/services/streaming/eventStatusMap.ts +0 -52
- package/src/services/streaming/index.ts +0 -46
- package/src/services/streaming/renderFormatting.ts +0 -79
- package/src/services/streaming/types.ts +0 -234
- package/src/skills/SkillLoader.ts +0 -126
- package/src/skills/SkillRegistry.ts +0 -366
- package/src/skills/index.ts +0 -48
- package/src/skills/types.ts +0 -146
- package/src/slash-commands/billing.ts +0 -70
- package/src/slash-commands/build.ts +0 -413
- package/src/slash-commands/builtinCommands.ts +0 -2733
- package/src/slash-commands/clone.ts +0 -242
- package/src/slash-commands/council.ts +0 -125
- package/src/slash-commands/custom/CustomCommandExecutor.ts +0 -143
- package/src/slash-commands/custom/CustomCommandLoader.ts +0 -251
- package/src/slash-commands/custom/CustomCommandRegistry.ts +0 -144
- package/src/slash-commands/custom/index.ts +0 -7
- package/src/slash-commands/debate.ts +0 -254
- package/src/slash-commands/gmail.ts +0 -105
- package/src/slash-commands/index.ts +0 -388
- package/src/slash-commands/mcpCommand.ts +0 -205
- package/src/slash-commands/types.ts +0 -201
- package/src/store/index.ts +0 -76
- package/src/store/selectors.ts +0 -246
- package/src/store/slices/appSlice.ts +0 -205
- package/src/store/slices/commandSlice.ts +0 -115
- package/src/store/slices/configSlice.ts +0 -46
- package/src/store/slices/focusSlice.ts +0 -64
- package/src/store/slices/index.ts +0 -9
- package/src/store/slices/sessionSlice.ts +0 -424
- package/src/store/streaming-buffer.ts +0 -425
- package/src/store/test.ts +0 -296
- package/src/store/types.ts +0 -274
- package/src/store/vanilla.ts +0 -186
- package/src/tools/builtin/bash.ts +0 -236
- package/src/tools/builtin/council.ts +0 -105
- package/src/tools/builtin/edit.ts +0 -213
- package/src/tools/builtin/glob.ts +0 -136
- package/src/tools/builtin/grep.ts +0 -263
- package/src/tools/builtin/index.ts +0 -61
- package/src/tools/builtin/memory.ts +0 -66
- package/src/tools/builtin/read.ts +0 -168
- package/src/tools/builtin/skill.ts +0 -97
- package/src/tools/builtin/snapshot.ts +0 -40
- package/src/tools/builtin/task.ts +0 -106
- package/src/tools/builtin/write.ts +0 -134
- package/src/tools/createTool.ts +0 -221
- package/src/tools/execution/ExecutionPipeline.ts +0 -263
- package/src/tools/execution/index.ts +0 -40
- package/src/tools/execution/stages/CacheStage.ts +0 -131
- package/src/tools/execution/stages/ConfirmationStage.ts +0 -203
- package/src/tools/execution/stages/DiscoveryStage.ts +0 -46
- package/src/tools/execution/stages/ExecutionStage.ts +0 -48
- package/src/tools/execution/stages/FormattingStage.ts +0 -44
- package/src/tools/execution/stages/HookStage.ts +0 -72
- package/src/tools/execution/stages/PermissionStage.ts +0 -287
- package/src/tools/execution/stages/PostHookStage.ts +0 -71
- package/src/tools/execution/stages/index.ts +0 -12
- package/src/tools/execution/test.ts +0 -266
- package/src/tools/execution/types.ts +0 -273
- package/src/tools/index.ts +0 -81
- package/src/tools/registry.ts +0 -304
- package/src/tools/schemas.ts +0 -109
- package/src/tools/test.ts +0 -220
- package/src/tools/types.ts +0 -175
- package/src/tools/validation/PermissionChecker.ts +0 -242
- package/src/tools/validation/SensitiveFileDetector.ts +0 -210
- package/src/tools/validation/index.ts +0 -11
- package/src/ui/App.tsx +0 -166
- package/src/ui/components/AegisInterface.tsx +0 -484
- package/src/ui/components/common/ChatSearch.tsx +0 -150
- package/src/ui/components/common/ErrorBoundary.tsx +0 -82
- package/src/ui/components/common/ExitMessage.tsx +0 -120
- package/src/ui/components/common/LoadingIndicator.tsx +0 -49
- package/src/ui/components/common/index.ts +0 -6
- package/src/ui/components/dialog/ConfirmationPrompt.tsx +0 -208
- package/src/ui/components/dialog/InteractiveSelector.tsx +0 -149
- package/src/ui/components/dialog/SetupWizard.tsx +0 -297
- package/src/ui/components/dialog/UpdatePrompt.tsx +0 -155
- package/src/ui/components/dialog/index.ts +0 -8
- package/src/ui/components/index.ts +0 -28
- package/src/ui/components/input/CommandSuggestions.tsx +0 -139
- package/src/ui/components/input/CustomTextInput.tsx +0 -220
- package/src/ui/components/input/InputArea.tsx +0 -361
- package/src/ui/components/input/PromptSuggestions.tsx +0 -66
- package/src/ui/components/input/index.ts +0 -6
- package/src/ui/components/layout/ChatStatusBar.tsx +0 -90
- package/src/ui/components/layout/ContextBar.tsx +0 -79
- package/src/ui/components/layout/MessageArea.tsx +0 -96
- package/src/ui/components/layout/MessageList.tsx +0 -647
- package/src/ui/components/layout/MessageSeparator.tsx +0 -26
- package/src/ui/components/layout/WelcomeMessage.tsx +0 -93
- package/src/ui/components/layout/index.ts +0 -7
- package/src/ui/components/markdown/CodeHighlighter.tsx +0 -292
- package/src/ui/components/markdown/MessageRenderer.tsx +0 -1211
- package/src/ui/components/markdown/index.ts +0 -8
- package/src/ui/components/markdown/parser.ts +0 -336
- package/src/ui/components/markdown/types.ts +0 -66
- package/src/ui/focus/FocusManager.ts +0 -137
- package/src/ui/focus/index.ts +0 -13
- package/src/ui/focus/types.ts +0 -54
- package/src/ui/focus/useFocus.ts +0 -75
- package/src/ui/hooks/index.ts +0 -11
- package/src/ui/hooks/useAgent.ts +0 -284
- package/src/ui/hooks/useCommandHistory.ts +0 -87
- package/src/ui/hooks/useCommandProcessor.ts +0 -443
- package/src/ui/hooks/useConfirmation.ts +0 -99
- package/src/ui/hooks/useCtrlCHandler.ts +0 -100
- package/src/ui/hooks/useInputBuffer.ts +0 -122
- package/src/ui/hooks/useTerminalSize.ts +0 -68
- package/src/ui/hooks/useTerminalWidth.ts +0 -5
- package/src/ui/hooks/useWindowedList.ts +0 -118
- package/src/ui/render-debugger.ts +0 -621
- package/src/ui/test.ts +0 -189
- package/src/ui/themes/ThemeManager.ts +0 -332
- package/src/ui/themes/aegisTheme.ts +0 -87
- package/src/ui/themes/darkTheme.ts +0 -87
- package/src/ui/themes/defaultTheme.ts +0 -85
- package/src/ui/themes/index.ts +0 -10
- package/src/ui/themes/lightTheme.ts +0 -89
- package/src/ui/themes/popularThemes.ts +0 -187
- package/src/ui/themes/types.ts +0 -130
- package/src/utils/clipboard.ts +0 -48
- package/src/utils/debug.ts +0 -43
- package/src/utils/environment.ts +0 -68
- 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;
|