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.
Files changed (67) hide show
  1. package/dist/api/anthropic.d.ts +1 -1
  2. package/dist/api/anthropic.js +52 -76
  3. package/dist/api/chat.d.ts +4 -4
  4. package/dist/api/chat.js +32 -17
  5. package/dist/api/gemini.d.ts +1 -1
  6. package/dist/api/gemini.js +20 -13
  7. package/dist/api/responses.d.ts +5 -5
  8. package/dist/api/responses.js +29 -27
  9. package/dist/app.js +4 -1
  10. package/dist/hooks/useClipboard.d.ts +4 -0
  11. package/dist/hooks/useClipboard.js +120 -0
  12. package/dist/hooks/useCommandHandler.d.ts +26 -0
  13. package/dist/hooks/useCommandHandler.js +158 -0
  14. package/dist/hooks/useCommandPanel.d.ts +16 -0
  15. package/dist/hooks/useCommandPanel.js +53 -0
  16. package/dist/hooks/useConversation.d.ts +9 -1
  17. package/dist/hooks/useConversation.js +152 -58
  18. package/dist/hooks/useFilePicker.d.ts +17 -0
  19. package/dist/hooks/useFilePicker.js +91 -0
  20. package/dist/hooks/useHistoryNavigation.d.ts +21 -0
  21. package/dist/hooks/useHistoryNavigation.js +50 -0
  22. package/dist/hooks/useInputBuffer.d.ts +6 -0
  23. package/dist/hooks/useInputBuffer.js +29 -0
  24. package/dist/hooks/useKeyboardInput.d.ts +51 -0
  25. package/dist/hooks/useKeyboardInput.js +272 -0
  26. package/dist/hooks/useSnapshotState.d.ts +12 -0
  27. package/dist/hooks/useSnapshotState.js +28 -0
  28. package/dist/hooks/useStreamingState.d.ts +24 -0
  29. package/dist/hooks/useStreamingState.js +96 -0
  30. package/dist/hooks/useVSCodeState.d.ts +8 -0
  31. package/dist/hooks/useVSCodeState.js +63 -0
  32. package/dist/mcp/filesystem.d.ts +25 -9
  33. package/dist/mcp/filesystem.js +56 -51
  34. package/dist/mcp/todo.js +4 -8
  35. package/dist/ui/components/ChatInput.js +68 -557
  36. package/dist/ui/components/DiffViewer.js +57 -30
  37. package/dist/ui/components/FileList.js +70 -26
  38. package/dist/ui/components/MessageList.d.ts +6 -0
  39. package/dist/ui/components/MessageList.js +47 -15
  40. package/dist/ui/components/ShimmerText.d.ts +9 -0
  41. package/dist/ui/components/ShimmerText.js +30 -0
  42. package/dist/ui/components/TodoTree.d.ts +1 -1
  43. package/dist/ui/components/TodoTree.js +0 -4
  44. package/dist/ui/components/ToolConfirmation.js +14 -6
  45. package/dist/ui/pages/ChatScreen.js +159 -359
  46. package/dist/ui/pages/CustomHeadersScreen.d.ts +6 -0
  47. package/dist/ui/pages/CustomHeadersScreen.js +104 -0
  48. package/dist/ui/pages/WelcomeScreen.js +5 -0
  49. package/dist/utils/apiConfig.d.ts +10 -0
  50. package/dist/utils/apiConfig.js +51 -0
  51. package/dist/utils/incrementalSnapshot.d.ts +8 -0
  52. package/dist/utils/incrementalSnapshot.js +63 -0
  53. package/dist/utils/mcpToolsManager.js +8 -3
  54. package/dist/utils/retryUtils.d.ts +22 -0
  55. package/dist/utils/retryUtils.js +180 -0
  56. package/dist/utils/sessionConverter.js +80 -17
  57. package/dist/utils/sessionManager.js +35 -4
  58. package/dist/utils/textUtils.d.ts +4 -0
  59. package/dist/utils/textUtils.js +19 -0
  60. package/dist/utils/todoPreprocessor.d.ts +1 -1
  61. package/dist/utils/todoPreprocessor.js +0 -1
  62. package/dist/utils/vscodeConnection.d.ts +8 -0
  63. package/dist/utils/vscodeConnection.js +44 -0
  64. package/package.json +1 -13
  65. package/readme.md +6 -2
  66. package/dist/mcp/multiLanguageASTParser.d.ts +0 -67
  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 { compressContext } from '../../utils/contextCompressor.js';
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 [snapshotFileCount, setSnapshotFileCount] = useState(new Map());
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
- // Minimum terminal height required for proper rendering
90
- const MIN_TERMINAL_HEIGHT = 10;
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
- // Animation for streaming/saving indicator
109
- useEffect(() => {
110
- if (!isStreaming && !isSaving)
111
- return;
112
- const interval = setInterval(() => {
113
- setAnimationFrame(prev => (prev + 1) % 5);
114
- }, 300);
115
- return () => {
116
- clearInterval(interval);
117
- setAnimationFrame(0);
118
- };
119
- }, [isStreaming, isSaving]);
120
- // Timer for tracking request duration
121
- useEffect(() => {
122
- if (isStreaming && timerStartTime === null) {
123
- // Start timer when streaming begins
124
- setTimerStartTime(Date.now());
125
- setElapsedSeconds(0);
126
- }
127
- else if (!isStreaming && timerStartTime !== null) {
128
- // Stop timer when streaming ends
129
- setTimerStartTime(null);
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 handleCommandExecution = async (commandName, result) => {
264
- // Handle /compact command
265
- if (commandName === 'compact' &&
266
- result.success &&
267
- result.action === 'compact') {
268
- // Set compressing state (不添加命令面板消息)
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
- else if (result.success && result.action === 'showMcpInfo') {
364
- setShowMcpInfo(true);
365
- setMcpPanelKey(prev => prev + 1);
366
- const commandMessage = {
367
- role: 'command',
368
- content: '',
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
- await incrementalSnapshotManager.rollbackToSnapshot(currentSession.id, selectedIndex);
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(session.messages);
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, { color: "gray", dimColor: true }, "\u2022 Ask for code explanations and debugging help"),
644
- React.createElement(Text, { color: "gray", dimColor: true }, "\u2022 Press ESC during response to interrupt"),
645
- React.createElement(Text, { color: "gray", dimColor: true },
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
- React.createElement(Text, { color: "gray", dimColor: true }, isStreaming ? (React.createElement(React.Fragment, null,
795
- "Thinking... (",
796
- formatElapsedTime(elapsedSeconds),
797
- streamTokenCount > 0 && (React.createElement(React.Fragment, null,
798
- ' · ',
799
- React.createElement(Text, { color: "cyan" },
800
- "\u2193",
801
- ' ',
802
- streamTokenCount >= 1000
803
- ? `${(streamTokenCount / 1000).toFixed(1)}k`
804
- : streamTokenCount,
805
- ' ',
806
- "tokens"))),
807
- ")")) : ('Create the first dialogue record file...'))))),
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" }),