snow-ai 0.2.14 → 0.2.16
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/dist/api/anthropic.d.ts +1 -1
- package/dist/api/anthropic.js +52 -76
- package/dist/api/chat.d.ts +4 -4
- package/dist/api/chat.js +32 -17
- package/dist/api/gemini.d.ts +1 -1
- package/dist/api/gemini.js +20 -13
- package/dist/api/responses.d.ts +5 -5
- package/dist/api/responses.js +29 -27
- package/dist/app.js +4 -1
- package/dist/hooks/useClipboard.d.ts +4 -0
- package/dist/hooks/useClipboard.js +120 -0
- package/dist/hooks/useCommandHandler.d.ts +26 -0
- package/dist/hooks/useCommandHandler.js +158 -0
- package/dist/hooks/useCommandPanel.d.ts +16 -0
- package/dist/hooks/useCommandPanel.js +53 -0
- package/dist/hooks/useConversation.d.ts +9 -1
- package/dist/hooks/useConversation.js +152 -58
- package/dist/hooks/useFilePicker.d.ts +17 -0
- package/dist/hooks/useFilePicker.js +91 -0
- package/dist/hooks/useHistoryNavigation.d.ts +21 -0
- package/dist/hooks/useHistoryNavigation.js +50 -0
- package/dist/hooks/useInputBuffer.d.ts +6 -0
- package/dist/hooks/useInputBuffer.js +29 -0
- package/dist/hooks/useKeyboardInput.d.ts +51 -0
- package/dist/hooks/useKeyboardInput.js +272 -0
- package/dist/hooks/useSnapshotState.d.ts +12 -0
- package/dist/hooks/useSnapshotState.js +28 -0
- package/dist/hooks/useStreamingState.d.ts +24 -0
- package/dist/hooks/useStreamingState.js +96 -0
- package/dist/hooks/useVSCodeState.d.ts +8 -0
- package/dist/hooks/useVSCodeState.js +63 -0
- package/dist/mcp/filesystem.d.ts +25 -9
- package/dist/mcp/filesystem.js +56 -51
- package/dist/mcp/todo.js +4 -8
- package/dist/ui/components/ChatInput.js +68 -557
- package/dist/ui/components/DiffViewer.js +57 -30
- package/dist/ui/components/FileList.js +70 -26
- package/dist/ui/components/MessageList.d.ts +6 -0
- package/dist/ui/components/MessageList.js +47 -15
- package/dist/ui/components/ShimmerText.d.ts +9 -0
- package/dist/ui/components/ShimmerText.js +30 -0
- package/dist/ui/components/TodoTree.d.ts +1 -1
- package/dist/ui/components/TodoTree.js +0 -4
- package/dist/ui/components/ToolConfirmation.js +14 -6
- package/dist/ui/pages/ChatScreen.js +159 -359
- package/dist/ui/pages/CustomHeadersScreen.d.ts +6 -0
- package/dist/ui/pages/CustomHeadersScreen.js +104 -0
- package/dist/ui/pages/WelcomeScreen.js +5 -0
- package/dist/utils/apiConfig.d.ts +10 -0
- package/dist/utils/apiConfig.js +51 -0
- package/dist/utils/incrementalSnapshot.d.ts +8 -0
- package/dist/utils/incrementalSnapshot.js +63 -0
- package/dist/utils/mcpToolsManager.js +8 -3
- package/dist/utils/retryUtils.d.ts +22 -0
- package/dist/utils/retryUtils.js +180 -0
- package/dist/utils/sessionConverter.js +80 -17
- package/dist/utils/sessionManager.js +35 -4
- package/dist/utils/textUtils.d.ts +4 -0
- package/dist/utils/textUtils.js +19 -0
- package/dist/utils/todoPreprocessor.d.ts +1 -1
- package/dist/utils/todoPreprocessor.js +0 -1
- package/dist/utils/vscodeConnection.d.ts +8 -0
- package/dist/utils/vscodeConnection.js +44 -0
- package/package.json +1 -13
- package/readme.md +6 -2
- package/dist/mcp/multiLanguageASTParser.d.ts +0 -67
- package/dist/mcp/multiLanguageASTParser.js +0 -360
|
@@ -13,14 +13,20 @@ import DiffViewer from '../components/DiffViewer.js';
|
|
|
13
13
|
import ToolResultPreview from '../components/ToolResultPreview.js';
|
|
14
14
|
import TodoTree from '../components/TodoTree.js';
|
|
15
15
|
import FileRollbackConfirmation from '../components/FileRollbackConfirmation.js';
|
|
16
|
+
import ShimmerText from '../components/ShimmerText.js';
|
|
16
17
|
import { getOpenAiConfig } from '../../utils/apiConfig.js';
|
|
17
18
|
import { sessionManager } from '../../utils/sessionManager.js';
|
|
18
19
|
import { useSessionSave } from '../../hooks/useSessionSave.js';
|
|
19
20
|
import { useToolConfirmation } from '../../hooks/useToolConfirmation.js';
|
|
20
21
|
import { handleConversationWithTools } from '../../hooks/useConversation.js';
|
|
22
|
+
import { useVSCodeState } from '../../hooks/useVSCodeState.js';
|
|
23
|
+
import { useSnapshotState } from '../../hooks/useSnapshotState.js';
|
|
24
|
+
import { useStreamingState } from '../../hooks/useStreamingState.js';
|
|
25
|
+
import { useCommandHandler } from '../../hooks/useCommandHandler.js';
|
|
21
26
|
import { parseAndValidateFileReferences, createMessageWithFileInstructions, getSystemInfo, } from '../../utils/fileUtils.js';
|
|
22
|
-
import {
|
|
27
|
+
import { convertSessionMessagesToUI } from '../../utils/sessionConverter.js';
|
|
23
28
|
import { incrementalSnapshotManager } from '../../utils/incrementalSnapshot.js';
|
|
29
|
+
import { formatElapsedTime } from '../../utils/textUtils.js';
|
|
24
30
|
// Import commands to register them
|
|
25
31
|
import '../../utils/commands/clear.js';
|
|
26
32
|
import '../../utils/commands/resume.js';
|
|
@@ -29,38 +35,15 @@ import '../../utils/commands/yolo.js';
|
|
|
29
35
|
import '../../utils/commands/init.js';
|
|
30
36
|
import '../../utils/commands/ide.js';
|
|
31
37
|
import '../../utils/commands/compact.js';
|
|
32
|
-
import { navigateTo } from '../../hooks/useGlobalNavigation.js';
|
|
33
|
-
import { vscodeConnection, } from '../../utils/vscodeConnection.js';
|
|
34
|
-
// Format elapsed time to human readable format
|
|
35
|
-
function formatElapsedTime(seconds) {
|
|
36
|
-
if (seconds < 60) {
|
|
37
|
-
return `${seconds}s`;
|
|
38
|
-
}
|
|
39
|
-
else if (seconds < 3600) {
|
|
40
|
-
const minutes = Math.floor(seconds / 60);
|
|
41
|
-
const remainingSeconds = seconds % 60;
|
|
42
|
-
return `${minutes}m ${remainingSeconds}s`;
|
|
43
|
-
}
|
|
44
|
-
else {
|
|
45
|
-
const hours = Math.floor(seconds / 3600);
|
|
46
|
-
const remainingMinutes = Math.floor((seconds % 3600) / 60);
|
|
47
|
-
const remainingSeconds = seconds % 60;
|
|
48
|
-
return `${hours}h ${remainingMinutes}m ${remainingSeconds}s`;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
38
|
export default function ChatScreen({}) {
|
|
52
39
|
const [messages, setMessages] = useState([]);
|
|
53
|
-
const [isStreaming, setIsStreaming] = useState(false);
|
|
54
40
|
const [isSaving] = useState(false);
|
|
55
41
|
const [currentTodos, setCurrentTodos] = useState([]);
|
|
56
|
-
const [animationFrame, setAnimationFrame] = useState(0);
|
|
57
|
-
const [abortController, setAbortController] = useState(null);
|
|
58
42
|
const [pendingMessages, setPendingMessages] = useState([]);
|
|
59
43
|
const pendingMessagesRef = useRef([]);
|
|
60
44
|
const [remountKey, setRemountKey] = useState(0);
|
|
61
45
|
const [showMcpInfo, setShowMcpInfo] = useState(false);
|
|
62
46
|
const [mcpPanelKey, setMcpPanelKey] = useState(0);
|
|
63
|
-
const [streamTokenCount, setStreamTokenCount] = useState(0);
|
|
64
47
|
const [yoloMode, setYoloMode] = useState(() => {
|
|
65
48
|
// Load yolo mode from localStorage on initialization
|
|
66
49
|
try {
|
|
@@ -71,23 +54,18 @@ export default function ChatScreen({}) {
|
|
|
71
54
|
return false;
|
|
72
55
|
}
|
|
73
56
|
});
|
|
74
|
-
const [contextUsage, setContextUsage] = useState(null);
|
|
75
|
-
const [elapsedSeconds, setElapsedSeconds] = useState(0);
|
|
76
|
-
const [timerStartTime, setTimerStartTime] = useState(null);
|
|
77
|
-
const [vscodeConnected, setVscodeConnected] = useState(false);
|
|
78
|
-
const [vscodeConnectionStatus, setVscodeConnectionStatus] = useState('disconnected');
|
|
79
|
-
const [editorContext, setEditorContext] = useState({});
|
|
80
57
|
const [isCompressing, setIsCompressing] = useState(false);
|
|
81
58
|
const [compressionError, setCompressionError] = useState(null);
|
|
82
59
|
const [showSessionPanel, setShowSessionPanel] = useState(false);
|
|
83
60
|
const [showMcpPanel, setShowMcpPanel] = useState(false);
|
|
84
|
-
const [
|
|
85
|
-
const [pendingRollback, setPendingRollback] = useState(null);
|
|
61
|
+
const [shouldIncludeSystemInfo, setShouldIncludeSystemInfo] = useState(true); // Include on first message
|
|
86
62
|
const { stdout } = useStdout();
|
|
87
63
|
const terminalHeight = stdout?.rows || 24;
|
|
88
64
|
const workingDirectory = process.cwd();
|
|
89
|
-
//
|
|
90
|
-
const
|
|
65
|
+
// Use custom hooks
|
|
66
|
+
const streamingState = useStreamingState();
|
|
67
|
+
const vscodeState = useVSCodeState();
|
|
68
|
+
const snapshotState = useSnapshotState(messages.length);
|
|
91
69
|
// Use session save hook
|
|
92
70
|
const { saveMessage, clearSavedMessages, initializeFromSession } = useSessionSave();
|
|
93
71
|
// Sync pendingMessages to ref for real-time access in callbacks
|
|
@@ -105,121 +83,44 @@ export default function ChatScreen({}) {
|
|
|
105
83
|
}, [yoloMode]);
|
|
106
84
|
// Use tool confirmation hook
|
|
107
85
|
const { pendingToolConfirmation, requestToolConfirmation, isToolAutoApproved, addMultipleToAlwaysApproved, } = useToolConfirmation();
|
|
108
|
-
//
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
131
|
-
}, [isStreaming, timerStartTime]);
|
|
132
|
-
// Update elapsed time every second
|
|
133
|
-
useEffect(() => {
|
|
134
|
-
if (timerStartTime === null)
|
|
135
|
-
return;
|
|
136
|
-
const interval = setInterval(() => {
|
|
137
|
-
const elapsed = Math.floor((Date.now() - timerStartTime) / 1000);
|
|
138
|
-
setElapsedSeconds(elapsed);
|
|
139
|
-
}, 1000);
|
|
140
|
-
return () => clearInterval(interval);
|
|
141
|
-
}, [timerStartTime]);
|
|
142
|
-
// Monitor VSCode connection status and editor context
|
|
143
|
-
useEffect(() => {
|
|
144
|
-
const checkConnectionInterval = setInterval(() => {
|
|
145
|
-
const isConnected = vscodeConnection.isConnected();
|
|
146
|
-
setVscodeConnected(isConnected);
|
|
147
|
-
// Update connection status based on actual connection state
|
|
148
|
-
if (isConnected && vscodeConnectionStatus !== 'connected') {
|
|
149
|
-
setVscodeConnectionStatus('connected');
|
|
150
|
-
}
|
|
151
|
-
else if (!isConnected && vscodeConnectionStatus === 'connected') {
|
|
152
|
-
setVscodeConnectionStatus('disconnected');
|
|
153
|
-
}
|
|
154
|
-
}, 1000);
|
|
155
|
-
const unsubscribe = vscodeConnection.onContextUpdate(context => {
|
|
156
|
-
setEditorContext(context);
|
|
157
|
-
// When we receive context, it means connection is successful
|
|
158
|
-
if (vscodeConnectionStatus !== 'connected') {
|
|
159
|
-
setVscodeConnectionStatus('connected');
|
|
160
|
-
}
|
|
161
|
-
});
|
|
162
|
-
return () => {
|
|
163
|
-
clearInterval(checkConnectionInterval);
|
|
164
|
-
unsubscribe();
|
|
165
|
-
};
|
|
166
|
-
}, [vscodeConnectionStatus]);
|
|
167
|
-
// Separate effect for handling connecting timeout
|
|
168
|
-
useEffect(() => {
|
|
169
|
-
if (vscodeConnectionStatus !== 'connecting') {
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
// Set timeout for connecting state (30 seconds to allow for VSCode extension reconnection)
|
|
173
|
-
const connectingTimeout = setTimeout(() => {
|
|
174
|
-
const isConnected = vscodeConnection.isConnected();
|
|
175
|
-
const isServerRunning = vscodeConnection.isServerRunning();
|
|
176
|
-
// Only set error if still not connected after timeout
|
|
177
|
-
if (!isConnected) {
|
|
178
|
-
if (isServerRunning) {
|
|
179
|
-
// Server is running but no connection - show error with helpful message
|
|
180
|
-
setVscodeConnectionStatus('error');
|
|
181
|
-
}
|
|
182
|
-
else {
|
|
183
|
-
// Server not running - go back to disconnected
|
|
184
|
-
setVscodeConnectionStatus('disconnected');
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
}, 30000); // Increased to 30 seconds
|
|
188
|
-
return () => {
|
|
189
|
-
clearTimeout(connectingTimeout);
|
|
190
|
-
};
|
|
191
|
-
}, [vscodeConnectionStatus]);
|
|
192
|
-
// Load snapshot file counts when session changes
|
|
193
|
-
useEffect(() => {
|
|
194
|
-
const loadSnapshotFileCounts = async () => {
|
|
195
|
-
const currentSession = sessionManager.getCurrentSession();
|
|
196
|
-
if (!currentSession)
|
|
197
|
-
return;
|
|
198
|
-
const snapshots = await incrementalSnapshotManager.listSnapshots(currentSession.id);
|
|
199
|
-
const counts = new Map();
|
|
200
|
-
for (const snapshot of snapshots) {
|
|
201
|
-
counts.set(snapshot.messageIndex, snapshot.fileCount);
|
|
202
|
-
}
|
|
203
|
-
setSnapshotFileCount(counts);
|
|
204
|
-
};
|
|
205
|
-
loadSnapshotFileCounts();
|
|
206
|
-
}, [messages.length]); // Reload when messages change
|
|
86
|
+
// Minimum terminal height required for proper rendering
|
|
87
|
+
const MIN_TERMINAL_HEIGHT = 10;
|
|
88
|
+
// Forward reference for processMessage (defined below)
|
|
89
|
+
const processMessageRef = useRef();
|
|
90
|
+
// Use command handler hook
|
|
91
|
+
const { handleCommandExecution } = useCommandHandler({
|
|
92
|
+
messages,
|
|
93
|
+
setMessages,
|
|
94
|
+
setRemountKey,
|
|
95
|
+
clearSavedMessages,
|
|
96
|
+
setIsCompressing,
|
|
97
|
+
setCompressionError,
|
|
98
|
+
setShowSessionPanel,
|
|
99
|
+
setShowMcpInfo,
|
|
100
|
+
setShowMcpPanel,
|
|
101
|
+
setMcpPanelKey,
|
|
102
|
+
setYoloMode,
|
|
103
|
+
setContextUsage: streamingState.setContextUsage,
|
|
104
|
+
setShouldIncludeSystemInfo,
|
|
105
|
+
setVscodeConnectionStatus: vscodeState.setVscodeConnectionStatus,
|
|
106
|
+
processMessage: (message, images, useBasicModel, hideUserMessage) => processMessageRef.current?.(message, images, useBasicModel, hideUserMessage) || Promise.resolve(),
|
|
107
|
+
});
|
|
207
108
|
// Pending messages are now handled inline during tool execution in useConversation
|
|
208
109
|
// Auto-send pending messages when streaming completely stops (as fallback)
|
|
209
110
|
useEffect(() => {
|
|
210
|
-
if (!isStreaming && pendingMessages.length > 0) {
|
|
111
|
+
if (!streamingState.isStreaming && pendingMessages.length > 0) {
|
|
211
112
|
const timer = setTimeout(() => {
|
|
212
113
|
processPendingMessages();
|
|
213
114
|
}, 100);
|
|
214
115
|
return () => clearTimeout(timer);
|
|
215
116
|
}
|
|
216
117
|
return undefined;
|
|
217
|
-
}, [isStreaming, pendingMessages.length]);
|
|
118
|
+
}, [streamingState.isStreaming, pendingMessages.length]);
|
|
218
119
|
// ESC key handler to interrupt streaming or close overlays
|
|
219
120
|
useInput((_, key) => {
|
|
220
|
-
if (pendingRollback) {
|
|
121
|
+
if (snapshotState.pendingRollback) {
|
|
221
122
|
if (key.escape) {
|
|
222
|
-
setPendingRollback(null);
|
|
123
|
+
snapshotState.setPendingRollback(null);
|
|
223
124
|
}
|
|
224
125
|
return;
|
|
225
126
|
}
|
|
@@ -241,9 +142,9 @@ export default function ChatScreen({}) {
|
|
|
241
142
|
}
|
|
242
143
|
return;
|
|
243
144
|
}
|
|
244
|
-
if (key.escape && isStreaming && abortController) {
|
|
145
|
+
if (key.escape && streamingState.isStreaming && streamingState.abortController) {
|
|
245
146
|
// Abort the controller
|
|
246
|
-
abortController.abort();
|
|
147
|
+
streamingState.abortController.abort();
|
|
247
148
|
// Add discontinued message
|
|
248
149
|
setMessages(prev => [
|
|
249
150
|
...prev,
|
|
@@ -255,162 +156,25 @@ export default function ChatScreen({}) {
|
|
|
255
156
|
},
|
|
256
157
|
]);
|
|
257
158
|
// Stop streaming state
|
|
258
|
-
setIsStreaming(false);
|
|
259
|
-
setAbortController(null);
|
|
260
|
-
setStreamTokenCount(0);
|
|
159
|
+
streamingState.setIsStreaming(false);
|
|
160
|
+
streamingState.setAbortController(null);
|
|
161
|
+
streamingState.setStreamTokenCount(0);
|
|
261
162
|
}
|
|
262
163
|
});
|
|
263
|
-
const
|
|
264
|
-
//
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
setIsCompressing(true);
|
|
270
|
-
setCompressionError(null);
|
|
271
|
-
try {
|
|
272
|
-
// Convert messages to ChatMessage format for compression
|
|
273
|
-
const chatMessages = messages
|
|
274
|
-
.filter(msg => msg.role !== 'command')
|
|
275
|
-
.map(msg => ({
|
|
276
|
-
role: msg.role,
|
|
277
|
-
content: msg.content,
|
|
278
|
-
tool_call_id: msg.toolCallId,
|
|
279
|
-
}));
|
|
280
|
-
// Compress the context
|
|
281
|
-
const result = await compressContext(chatMessages);
|
|
282
|
-
// Replace all messages with a summary message (不包含 "Context Compressed" 标题)
|
|
283
|
-
const summaryMessage = {
|
|
284
|
-
role: 'assistant',
|
|
285
|
-
content: result.summary,
|
|
286
|
-
streaming: false,
|
|
287
|
-
};
|
|
288
|
-
// Clear session and set new compressed state
|
|
289
|
-
sessionManager.clearCurrentSession();
|
|
290
|
-
clearSavedMessages();
|
|
291
|
-
setMessages([summaryMessage]);
|
|
292
|
-
setRemountKey(prev => prev + 1);
|
|
293
|
-
// Update token usage with compression result
|
|
294
|
-
setContextUsage({
|
|
295
|
-
prompt_tokens: result.usage.prompt_tokens,
|
|
296
|
-
completion_tokens: result.usage.completion_tokens,
|
|
297
|
-
total_tokens: result.usage.total_tokens,
|
|
298
|
-
});
|
|
299
|
-
}
|
|
300
|
-
catch (error) {
|
|
301
|
-
// Show error message
|
|
302
|
-
const errorMsg = error instanceof Error ? error.message : 'Unknown compression error';
|
|
303
|
-
setCompressionError(errorMsg);
|
|
304
|
-
const errorMessage = {
|
|
305
|
-
role: 'assistant',
|
|
306
|
-
content: `**Compression Failed**\n\n${errorMsg}`,
|
|
307
|
-
streaming: false,
|
|
308
|
-
};
|
|
309
|
-
setMessages(prev => [...prev, errorMessage]);
|
|
310
|
-
}
|
|
311
|
-
finally {
|
|
312
|
-
setIsCompressing(false);
|
|
313
|
-
}
|
|
314
|
-
return;
|
|
315
|
-
}
|
|
316
|
-
// Handle /ide command
|
|
317
|
-
if (commandName === 'ide') {
|
|
318
|
-
if (result.success) {
|
|
319
|
-
setVscodeConnectionStatus('connecting');
|
|
320
|
-
// Add command execution feedback
|
|
321
|
-
const commandMessage = {
|
|
322
|
-
role: 'command',
|
|
323
|
-
content: '',
|
|
324
|
-
commandName: commandName,
|
|
325
|
-
};
|
|
326
|
-
setMessages(prev => [...prev, commandMessage]);
|
|
327
|
-
}
|
|
328
|
-
else {
|
|
329
|
-
setVscodeConnectionStatus('error');
|
|
330
|
-
}
|
|
331
|
-
return;
|
|
332
|
-
}
|
|
333
|
-
if (result.success && result.action === 'clear') {
|
|
334
|
-
if (stdout && typeof stdout.write === 'function') {
|
|
335
|
-
stdout.write('\x1B[3J\x1B[2J\x1B[H');
|
|
164
|
+
const handleHistorySelect = async (selectedIndex, _message) => {
|
|
165
|
+
// Count total files that will be rolled back (from selectedIndex onwards)
|
|
166
|
+
let totalFileCount = 0;
|
|
167
|
+
for (const [index, count] of snapshotState.snapshotFileCount.entries()) {
|
|
168
|
+
if (index >= selectedIndex) {
|
|
169
|
+
totalFileCount += count;
|
|
336
170
|
}
|
|
337
|
-
// Clear current session and start new one
|
|
338
|
-
sessionManager.clearCurrentSession();
|
|
339
|
-
clearSavedMessages();
|
|
340
|
-
setMessages([]);
|
|
341
|
-
setRemountKey(prev => prev + 1);
|
|
342
|
-
// Reset context usage (token statistics)
|
|
343
|
-
setContextUsage(null);
|
|
344
|
-
// Note: yoloMode is preserved via localStorage (lines 68-76, 104-111)
|
|
345
|
-
// Note: VSCode connection is preserved and managed by vscodeConnection utility
|
|
346
|
-
// Add command execution feedback
|
|
347
|
-
const commandMessage = {
|
|
348
|
-
role: 'command',
|
|
349
|
-
content: '',
|
|
350
|
-
commandName: commandName,
|
|
351
|
-
};
|
|
352
|
-
setMessages([commandMessage]);
|
|
353
|
-
}
|
|
354
|
-
else if (result.success && result.action === 'showSessionPanel') {
|
|
355
|
-
setShowSessionPanel(true);
|
|
356
|
-
const commandMessage = {
|
|
357
|
-
role: 'command',
|
|
358
|
-
content: '',
|
|
359
|
-
commandName: commandName,
|
|
360
|
-
};
|
|
361
|
-
setMessages(prev => [...prev, commandMessage]);
|
|
362
171
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
commandName: commandName,
|
|
370
|
-
};
|
|
371
|
-
setMessages(prev => [...prev, commandMessage]);
|
|
372
|
-
}
|
|
373
|
-
else if (result.success && result.action === 'showMcpPanel') {
|
|
374
|
-
setShowMcpPanel(true);
|
|
375
|
-
const commandMessage = {
|
|
376
|
-
role: 'command',
|
|
377
|
-
content: '',
|
|
378
|
-
commandName: commandName,
|
|
379
|
-
};
|
|
380
|
-
setMessages(prev => [...prev, commandMessage]);
|
|
381
|
-
}
|
|
382
|
-
else if (result.success && result.action === 'goHome') {
|
|
383
|
-
navigateTo('welcome');
|
|
384
|
-
}
|
|
385
|
-
else if (result.success && result.action === 'toggleYolo') {
|
|
386
|
-
setYoloMode(prev => !prev);
|
|
387
|
-
const commandMessage = {
|
|
388
|
-
role: 'command',
|
|
389
|
-
content: '',
|
|
390
|
-
commandName: commandName,
|
|
391
|
-
};
|
|
392
|
-
setMessages(prev => [...prev, commandMessage]);
|
|
393
|
-
}
|
|
394
|
-
else if (result.success &&
|
|
395
|
-
result.action === 'initProject' &&
|
|
396
|
-
result.prompt) {
|
|
397
|
-
// Add command execution feedback
|
|
398
|
-
const commandMessage = {
|
|
399
|
-
role: 'command',
|
|
400
|
-
content: '',
|
|
401
|
-
commandName: commandName,
|
|
402
|
-
};
|
|
403
|
-
setMessages(prev => [...prev, commandMessage]);
|
|
404
|
-
// Auto-send the prompt using basicModel, hide the prompt from UI
|
|
405
|
-
processMessage(result.prompt, undefined, true, true);
|
|
406
|
-
}
|
|
407
|
-
};
|
|
408
|
-
const handleHistorySelect = async (selectedIndex, _message) => {
|
|
409
|
-
// Check if there are files to rollback
|
|
410
|
-
const fileCount = snapshotFileCount.get(selectedIndex) || 0;
|
|
411
|
-
if (fileCount > 0) {
|
|
412
|
-
// Show confirmation dialog
|
|
413
|
-
setPendingRollback({ messageIndex: selectedIndex, fileCount });
|
|
172
|
+
if (totalFileCount > 0) {
|
|
173
|
+
// Show confirmation dialog with total file count
|
|
174
|
+
snapshotState.setPendingRollback({
|
|
175
|
+
messageIndex: selectedIndex,
|
|
176
|
+
fileCount: totalFileCount,
|
|
177
|
+
});
|
|
414
178
|
}
|
|
415
179
|
else {
|
|
416
180
|
// No files to rollback, just rollback conversation
|
|
@@ -422,7 +186,8 @@ export default function ChatScreen({}) {
|
|
|
422
186
|
if (rollbackFiles) {
|
|
423
187
|
const currentSession = sessionManager.getCurrentSession();
|
|
424
188
|
if (currentSession) {
|
|
425
|
-
|
|
189
|
+
// Use rollbackToMessageIndex to rollback all snapshots >= selectedIndex
|
|
190
|
+
await incrementalSnapshotManager.rollbackToMessageIndex(currentSession.id, selectedIndex);
|
|
426
191
|
}
|
|
427
192
|
}
|
|
428
193
|
// Truncate messages array to remove the selected user message and everything after it
|
|
@@ -430,11 +195,11 @@ export default function ChatScreen({}) {
|
|
|
430
195
|
clearSavedMessages();
|
|
431
196
|
setRemountKey(prev => prev + 1);
|
|
432
197
|
// Clear pending rollback dialog
|
|
433
|
-
setPendingRollback(null);
|
|
198
|
+
snapshotState.setPendingRollback(null);
|
|
434
199
|
};
|
|
435
200
|
const handleRollbackConfirm = (rollbackFiles) => {
|
|
436
|
-
if (pendingRollback) {
|
|
437
|
-
performRollback(pendingRollback.messageIndex, rollbackFiles);
|
|
201
|
+
if (snapshotState.pendingRollback) {
|
|
202
|
+
performRollback(snapshotState.pendingRollback.messageIndex, rollbackFiles);
|
|
438
203
|
}
|
|
439
204
|
};
|
|
440
205
|
const handleSessionPanelSelect = async (sessionId) => {
|
|
@@ -442,11 +207,20 @@ export default function ChatScreen({}) {
|
|
|
442
207
|
try {
|
|
443
208
|
const session = await sessionManager.loadSession(sessionId);
|
|
444
209
|
if (session) {
|
|
210
|
+
// Convert API format messages to UI format for proper rendering
|
|
211
|
+
const uiMessages = convertSessionMessagesToUI(session.messages);
|
|
445
212
|
initializeFromSession(session.messages);
|
|
446
|
-
setMessages(
|
|
213
|
+
setMessages(uiMessages);
|
|
447
214
|
setPendingMessages([]);
|
|
448
|
-
setIsStreaming(false);
|
|
215
|
+
streamingState.setIsStreaming(false);
|
|
449
216
|
setRemountKey(prev => prev + 1);
|
|
217
|
+
// Load snapshot file counts for the loaded session
|
|
218
|
+
const snapshots = await incrementalSnapshotManager.listSnapshots(session.id);
|
|
219
|
+
const counts = new Map();
|
|
220
|
+
for (const snapshot of snapshots) {
|
|
221
|
+
counts.set(snapshot.messageIndex, snapshot.fileCount);
|
|
222
|
+
}
|
|
223
|
+
snapshotState.setSnapshotFileCount(counts);
|
|
450
224
|
}
|
|
451
225
|
}
|
|
452
226
|
catch (error) {
|
|
@@ -455,7 +229,7 @@ export default function ChatScreen({}) {
|
|
|
455
229
|
};
|
|
456
230
|
const handleMessageSubmit = async (message, images) => {
|
|
457
231
|
// If streaming, add to pending messages instead of sending immediately
|
|
458
|
-
if (isStreaming) {
|
|
232
|
+
if (streamingState.isStreaming) {
|
|
459
233
|
setPendingMessages(prev => [...prev, message]);
|
|
460
234
|
return;
|
|
461
235
|
}
|
|
@@ -490,8 +264,8 @@ export default function ChatScreen({}) {
|
|
|
490
264
|
mimeType: f.mimeType,
|
|
491
265
|
})),
|
|
492
266
|
];
|
|
493
|
-
// Get system information
|
|
494
|
-
const systemInfo = getSystemInfo();
|
|
267
|
+
// Get system information only if needed
|
|
268
|
+
const systemInfo = shouldIncludeSystemInfo ? getSystemInfo() : undefined;
|
|
495
269
|
// Only add user message to UI if not hidden
|
|
496
270
|
if (!hideUserMessage) {
|
|
497
271
|
const userMessage = {
|
|
@@ -502,14 +276,18 @@ export default function ChatScreen({}) {
|
|
|
502
276
|
systemInfo,
|
|
503
277
|
};
|
|
504
278
|
setMessages(prev => [...prev, userMessage]);
|
|
279
|
+
// After including system info once, don't include it again
|
|
280
|
+
if (shouldIncludeSystemInfo) {
|
|
281
|
+
setShouldIncludeSystemInfo(false);
|
|
282
|
+
}
|
|
505
283
|
}
|
|
506
|
-
setIsStreaming(true);
|
|
284
|
+
streamingState.setIsStreaming(true);
|
|
507
285
|
// Create new abort controller for this request
|
|
508
286
|
const controller = new AbortController();
|
|
509
|
-
setAbortController(controller);
|
|
287
|
+
streamingState.setAbortController(controller);
|
|
510
288
|
try {
|
|
511
289
|
// Create message for AI with file read instructions, system info, and editor context
|
|
512
|
-
const messageForAI = createMessageWithFileInstructions(cleanContent, regularFiles, systemInfo, vscodeConnected ? editorContext : undefined);
|
|
290
|
+
const messageForAI = createMessageWithFileInstructions(cleanContent, regularFiles, systemInfo, vscodeState.vscodeConnected ? vscodeState.editorContext : undefined);
|
|
513
291
|
// Start conversation with tool support
|
|
514
292
|
await handleConversationWithTools({
|
|
515
293
|
userContent: messageForAI,
|
|
@@ -518,17 +296,19 @@ export default function ChatScreen({}) {
|
|
|
518
296
|
messages,
|
|
519
297
|
saveMessage,
|
|
520
298
|
setMessages,
|
|
521
|
-
setStreamTokenCount,
|
|
299
|
+
setStreamTokenCount: streamingState.setStreamTokenCount,
|
|
522
300
|
setCurrentTodos,
|
|
523
301
|
requestToolConfirmation,
|
|
524
302
|
isToolAutoApproved,
|
|
525
303
|
addMultipleToAlwaysApproved,
|
|
526
304
|
yoloMode,
|
|
527
|
-
setContextUsage,
|
|
305
|
+
setContextUsage: streamingState.setContextUsage,
|
|
528
306
|
useBasicModel,
|
|
529
307
|
getPendingMessages: () => pendingMessagesRef.current,
|
|
530
308
|
clearPendingMessages: () => setPendingMessages([]),
|
|
531
|
-
setIsStreaming,
|
|
309
|
+
setIsStreaming: streamingState.setIsStreaming,
|
|
310
|
+
setIsReasoning: streamingState.setIsReasoning,
|
|
311
|
+
setRetryStatus: streamingState.setRetryStatus,
|
|
532
312
|
});
|
|
533
313
|
}
|
|
534
314
|
catch (error) {
|
|
@@ -545,11 +325,13 @@ export default function ChatScreen({}) {
|
|
|
545
325
|
}
|
|
546
326
|
finally {
|
|
547
327
|
// End streaming
|
|
548
|
-
setIsStreaming(false);
|
|
549
|
-
setAbortController(null);
|
|
550
|
-
setStreamTokenCount(0);
|
|
328
|
+
streamingState.setIsStreaming(false);
|
|
329
|
+
streamingState.setAbortController(null);
|
|
330
|
+
streamingState.setStreamTokenCount(0);
|
|
551
331
|
}
|
|
552
332
|
};
|
|
333
|
+
// Set the ref to the actual function
|
|
334
|
+
processMessageRef.current = processMessage;
|
|
553
335
|
const processPendingMessages = async () => {
|
|
554
336
|
if (pendingMessages.length === 0)
|
|
555
337
|
return;
|
|
@@ -562,10 +344,10 @@ export default function ChatScreen({}) {
|
|
|
562
344
|
const userMessage = { role: 'user', content: combinedMessage };
|
|
563
345
|
setMessages(prev => [...prev, userMessage]);
|
|
564
346
|
// Start streaming response
|
|
565
|
-
setIsStreaming(true);
|
|
347
|
+
streamingState.setIsStreaming(true);
|
|
566
348
|
// Create new abort controller for this request
|
|
567
349
|
const controller = new AbortController();
|
|
568
|
-
setAbortController(controller);
|
|
350
|
+
streamingState.setAbortController(controller);
|
|
569
351
|
// Save user message
|
|
570
352
|
saveMessage({
|
|
571
353
|
role: 'user',
|
|
@@ -582,16 +364,18 @@ export default function ChatScreen({}) {
|
|
|
582
364
|
messages,
|
|
583
365
|
saveMessage,
|
|
584
366
|
setMessages,
|
|
585
|
-
setStreamTokenCount,
|
|
367
|
+
setStreamTokenCount: streamingState.setStreamTokenCount,
|
|
586
368
|
setCurrentTodos,
|
|
587
369
|
requestToolConfirmation,
|
|
588
370
|
isToolAutoApproved,
|
|
589
371
|
addMultipleToAlwaysApproved,
|
|
590
372
|
yoloMode,
|
|
591
|
-
setContextUsage,
|
|
373
|
+
setContextUsage: streamingState.setContextUsage,
|
|
592
374
|
getPendingMessages: () => pendingMessagesRef.current,
|
|
593
375
|
clearPendingMessages: () => setPendingMessages([]),
|
|
594
|
-
setIsStreaming,
|
|
376
|
+
setIsStreaming: streamingState.setIsStreaming,
|
|
377
|
+
setIsReasoning: streamingState.setIsReasoning,
|
|
378
|
+
setRetryStatus: streamingState.setRetryStatus,
|
|
595
379
|
});
|
|
596
380
|
}
|
|
597
381
|
catch (error) {
|
|
@@ -608,9 +392,9 @@ export default function ChatScreen({}) {
|
|
|
608
392
|
}
|
|
609
393
|
finally {
|
|
610
394
|
// End streaming
|
|
611
|
-
setIsStreaming(false);
|
|
612
|
-
setAbortController(null);
|
|
613
|
-
setStreamTokenCount(0);
|
|
395
|
+
streamingState.setIsStreaming(false);
|
|
396
|
+
streamingState.setAbortController(null);
|
|
397
|
+
streamingState.setStreamTokenCount(0);
|
|
614
398
|
}
|
|
615
399
|
};
|
|
616
400
|
if (showMcpInfo) {
|
|
@@ -640,9 +424,9 @@ export default function ChatScreen({}) {
|
|
|
640
424
|
React.createElement(Text, { color: "cyan" }, "\u2746 "),
|
|
641
425
|
React.createElement(Gradient, { name: "rainbow" }, "Programming efficiency x10!"),
|
|
642
426
|
React.createElement(Text, { color: "white" }, " \u26C7")),
|
|
643
|
-
React.createElement(Text,
|
|
644
|
-
React.createElement(Text,
|
|
645
|
-
React.createElement(Text,
|
|
427
|
+
React.createElement(Text, null, "\u2022 Ask for code explanations and debugging help"),
|
|
428
|
+
React.createElement(Text, null, "\u2022 Press ESC during response to interrupt"),
|
|
429
|
+
React.createElement(Text, null,
|
|
646
430
|
"\u2022 Working directory: ",
|
|
647
431
|
workingDirectory))),
|
|
648
432
|
...messages
|
|
@@ -788,23 +572,39 @@ export default function ChatScreen({}) {
|
|
|
788
572
|
React.createElement(Box, { marginLeft: 1 },
|
|
789
573
|
React.createElement(Text, { color: "yellow" },
|
|
790
574
|
React.createElement(Spinner, { type: "dots" }))))))),
|
|
791
|
-
(isStreaming || isSaving) && !pendingToolConfirmation && (React.createElement(Box, { marginBottom: 1, marginX: 1 },
|
|
792
|
-
React.createElement(Text, { color: ['#FF6EBF', 'green', 'blue', 'cyan', '#B588F8'][animationFrame], bold: true }, "\u2746"),
|
|
793
|
-
React.createElement(Box, { marginLeft: 1, marginBottom: 1 },
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
")")) : (
|
|
575
|
+
(streamingState.isStreaming || isSaving) && !pendingToolConfirmation && (React.createElement(Box, { marginBottom: 1, marginX: 1 },
|
|
576
|
+
React.createElement(Text, { color: ['#FF6EBF', 'green', 'blue', 'cyan', '#B588F8'][streamingState.animationFrame], bold: true }, "\u2746"),
|
|
577
|
+
React.createElement(Box, { marginLeft: 1, marginBottom: 1, flexDirection: "column" }, streamingState.isStreaming ? (React.createElement(React.Fragment, null, streamingState.retryStatus && streamingState.retryStatus.isRetrying ? (
|
|
578
|
+
// Retry status display - hide "Thinking" and show retry info
|
|
579
|
+
React.createElement(Box, { flexDirection: "column" },
|
|
580
|
+
streamingState.retryStatus.errorMessage && (React.createElement(Text, { color: "red", dimColor: true },
|
|
581
|
+
"\u2717 Error: ",
|
|
582
|
+
streamingState.retryStatus.errorMessage)),
|
|
583
|
+
streamingState.retryStatus.remainingSeconds !== undefined && streamingState.retryStatus.remainingSeconds > 0 ? (React.createElement(Text, { color: "yellow", dimColor: true },
|
|
584
|
+
"\u27F3 Retry ",
|
|
585
|
+
streamingState.retryStatus.attempt,
|
|
586
|
+
"/5 in ",
|
|
587
|
+
streamingState.retryStatus.remainingSeconds,
|
|
588
|
+
"s...")) : (React.createElement(Text, { color: "yellow", dimColor: true },
|
|
589
|
+
"\u27F3 Resending... (Attempt ",
|
|
590
|
+
streamingState.retryStatus.attempt,
|
|
591
|
+
"/5)")))) : (
|
|
592
|
+
// Normal thinking status
|
|
593
|
+
React.createElement(Text, { color: "gray", dimColor: true },
|
|
594
|
+
React.createElement(ShimmerText, { text: streamingState.isReasoning ? 'Deep thinking...' : 'Thinking...' }),
|
|
595
|
+
' ',
|
|
596
|
+
"(",
|
|
597
|
+
formatElapsedTime(streamingState.elapsedSeconds),
|
|
598
|
+
' · ',
|
|
599
|
+
React.createElement(Text, { color: "cyan" },
|
|
600
|
+
"\u2193",
|
|
601
|
+
' ',
|
|
602
|
+
streamingState.streamTokenCount >= 1000
|
|
603
|
+
? `${(streamingState.streamTokenCount / 1000).toFixed(1)}k`
|
|
604
|
+
: streamingState.streamTokenCount,
|
|
605
|
+
' ',
|
|
606
|
+
"tokens"),
|
|
607
|
+
")")))) : (React.createElement(Text, { color: "gray", dimColor: true }, "Create the first dialogue record file..."))))),
|
|
808
608
|
React.createElement(Box, { marginX: 1 },
|
|
809
609
|
React.createElement(PendingMessages, { pendingMessages: pendingMessages })),
|
|
810
610
|
pendingToolConfirmation && (React.createElement(ToolConfirmation, { toolName: pendingToolConfirmation.batchToolNames ||
|
|
@@ -817,44 +617,44 @@ export default function ChatScreen({}) {
|
|
|
817
617
|
React.createElement(MCPInfoPanel, null),
|
|
818
618
|
React.createElement(Box, { marginTop: 1 },
|
|
819
619
|
React.createElement(Text, { color: "gray", dimColor: true }, "Press ESC to close")))),
|
|
820
|
-
pendingRollback && (React.createElement(FileRollbackConfirmation, { fileCount: pendingRollback.fileCount, onConfirm: handleRollbackConfirm })),
|
|
620
|
+
snapshotState.pendingRollback && (React.createElement(FileRollbackConfirmation, { fileCount: snapshotState.pendingRollback.fileCount, onConfirm: handleRollbackConfirm })),
|
|
821
621
|
!pendingToolConfirmation &&
|
|
822
622
|
!isCompressing &&
|
|
823
623
|
!showSessionPanel &&
|
|
824
624
|
!showMcpPanel &&
|
|
825
|
-
!pendingRollback && (React.createElement(React.Fragment, null,
|
|
826
|
-
React.createElement(ChatInput, { onSubmit: handleMessageSubmit, onCommand: handleCommandExecution, placeholder: "Ask me anything about coding...", disabled: !!pendingToolConfirmation, chatHistory: messages, onHistorySelect: handleHistorySelect, yoloMode: yoloMode, contextUsage: contextUsage
|
|
625
|
+
!snapshotState.pendingRollback && (React.createElement(React.Fragment, null,
|
|
626
|
+
React.createElement(ChatInput, { onSubmit: handleMessageSubmit, onCommand: handleCommandExecution, placeholder: "Ask me anything about coding...", disabled: !!pendingToolConfirmation, chatHistory: messages, onHistorySelect: handleHistorySelect, yoloMode: yoloMode, contextUsage: streamingState.contextUsage
|
|
827
627
|
? {
|
|
828
|
-
inputTokens: contextUsage.prompt_tokens,
|
|
628
|
+
inputTokens: streamingState.contextUsage.prompt_tokens,
|
|
829
629
|
maxContextTokens: getOpenAiConfig().maxContextTokens || 4000,
|
|
830
|
-
cacheCreationTokens: contextUsage.cache_creation_input_tokens,
|
|
831
|
-
cacheReadTokens: contextUsage.cache_read_input_tokens,
|
|
832
|
-
cachedTokens: contextUsage.cached_tokens,
|
|
630
|
+
cacheCreationTokens: streamingState.contextUsage.cache_creation_input_tokens,
|
|
631
|
+
cacheReadTokens: streamingState.contextUsage.cache_read_input_tokens,
|
|
632
|
+
cachedTokens: streamingState.contextUsage.cached_tokens,
|
|
833
633
|
}
|
|
834
|
-
: undefined, snapshotFileCount: snapshotFileCount }),
|
|
835
|
-
vscodeConnectionStatus !== 'disconnected' && (React.createElement(Box, { marginTop: 1 },
|
|
836
|
-
React.createElement(Text, { color: vscodeConnectionStatus === 'connecting'
|
|
634
|
+
: undefined, snapshotFileCount: snapshotState.snapshotFileCount }),
|
|
635
|
+
vscodeState.vscodeConnectionStatus !== 'disconnected' && (React.createElement(Box, { marginTop: 1 },
|
|
636
|
+
React.createElement(Text, { color: vscodeState.vscodeConnectionStatus === 'connecting'
|
|
837
637
|
? 'yellow'
|
|
838
|
-
: vscodeConnectionStatus === 'connected'
|
|
638
|
+
: vscodeState.vscodeConnectionStatus === 'connected'
|
|
839
639
|
? 'green'
|
|
840
|
-
: vscodeConnectionStatus === 'error'
|
|
640
|
+
: vscodeState.vscodeConnectionStatus === 'error'
|
|
841
641
|
? 'red'
|
|
842
|
-
: 'gray', dimColor: vscodeConnectionStatus !== 'error' },
|
|
642
|
+
: 'gray', dimColor: vscodeState.vscodeConnectionStatus !== 'error' },
|
|
843
643
|
"\u25CF",
|
|
844
644
|
' ',
|
|
845
|
-
vscodeConnectionStatus === 'connecting'
|
|
645
|
+
vscodeState.vscodeConnectionStatus === 'connecting'
|
|
846
646
|
? 'Waiting for VSCode extension to connect...'
|
|
847
|
-
: vscodeConnectionStatus === 'connected'
|
|
647
|
+
: vscodeState.vscodeConnectionStatus === 'connected'
|
|
848
648
|
? 'VSCode Connected'
|
|
849
|
-
: vscodeConnectionStatus === 'error'
|
|
649
|
+
: vscodeState.vscodeConnectionStatus === 'error'
|
|
850
650
|
? 'Connection Failed - Make sure Snow CLI extension is installed and active in VSCode'
|
|
851
651
|
: 'VSCode',
|
|
852
|
-
vscodeConnectionStatus === 'connected' &&
|
|
853
|
-
editorContext.activeFile &&
|
|
854
|
-
` | ${editorContext.activeFile}`,
|
|
855
|
-
vscodeConnectionStatus === 'connected' &&
|
|
856
|
-
editorContext.selectedText &&
|
|
857
|
-
` | ${editorContext.selectedText.length} chars selected`))))),
|
|
652
|
+
vscodeState.vscodeConnectionStatus === 'connected' &&
|
|
653
|
+
vscodeState.editorContext.activeFile &&
|
|
654
|
+
` | ${vscodeState.editorContext.activeFile}`,
|
|
655
|
+
vscodeState.vscodeConnectionStatus === 'connected' &&
|
|
656
|
+
vscodeState.editorContext.selectedText &&
|
|
657
|
+
` | ${vscodeState.editorContext.selectedText.length} chars selected`))))),
|
|
858
658
|
isCompressing && (React.createElement(Box, { marginTop: 1 },
|
|
859
659
|
React.createElement(Text, { color: "cyan" },
|
|
860
660
|
React.createElement(Spinner, { type: "dots" }),
|