snow-ai 0.3.6 → 0.3.8

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 (127) hide show
  1. package/dist/agents/compactAgent.js +7 -3
  2. package/dist/agents/reviewAgent.d.ts +50 -0
  3. package/dist/agents/reviewAgent.js +264 -0
  4. package/dist/agents/summaryAgent.d.ts +34 -8
  5. package/dist/agents/summaryAgent.js +167 -164
  6. package/dist/api/anthropic.d.ts +1 -0
  7. package/dist/api/anthropic.js +118 -78
  8. package/dist/api/chat.d.ts +2 -1
  9. package/dist/api/chat.js +82 -52
  10. package/dist/api/gemini.d.ts +1 -0
  11. package/dist/api/gemini.js +110 -64
  12. package/dist/api/responses.d.ts +10 -1
  13. package/dist/api/responses.js +127 -79
  14. package/dist/api/systemPrompt.d.ts +1 -1
  15. package/dist/api/systemPrompt.js +36 -7
  16. package/dist/api/types.d.ts +8 -0
  17. package/dist/app.js +15 -2
  18. package/dist/hooks/useCommandHandler.d.ts +1 -0
  19. package/dist/hooks/useCommandHandler.js +102 -1
  20. package/dist/hooks/useCommandPanel.d.ts +2 -1
  21. package/dist/hooks/useCommandPanel.js +19 -1
  22. package/dist/hooks/useConversation.d.ts +4 -1
  23. package/dist/hooks/useConversation.js +91 -29
  24. package/dist/hooks/useKeyboardInput.js +19 -0
  25. package/dist/hooks/useSnapshotState.d.ts +2 -0
  26. package/dist/hooks/useTerminalFocus.js +13 -3
  27. package/dist/mcp/aceCodeSearch.d.ts +2 -76
  28. package/dist/mcp/aceCodeSearch.js +31 -467
  29. package/dist/mcp/bash.d.ts +1 -8
  30. package/dist/mcp/bash.js +20 -40
  31. package/dist/mcp/filesystem.d.ts +131 -111
  32. package/dist/mcp/filesystem.js +212 -375
  33. package/dist/mcp/ideDiagnostics.js +2 -4
  34. package/dist/mcp/todo.d.ts +1 -17
  35. package/dist/mcp/todo.js +11 -15
  36. package/dist/mcp/types/aceCodeSearch.types.d.ts +92 -0
  37. package/dist/mcp/types/aceCodeSearch.types.js +4 -0
  38. package/dist/mcp/types/bash.types.d.ts +13 -0
  39. package/dist/mcp/types/bash.types.js +4 -0
  40. package/dist/mcp/types/filesystem.types.d.ts +135 -0
  41. package/dist/mcp/types/filesystem.types.js +4 -0
  42. package/dist/mcp/types/todo.types.d.ts +27 -0
  43. package/dist/mcp/types/todo.types.js +4 -0
  44. package/dist/mcp/types/websearch.types.d.ts +30 -0
  45. package/dist/mcp/types/websearch.types.js +4 -0
  46. package/dist/mcp/utils/aceCodeSearch/filesystem.utils.d.ts +34 -0
  47. package/dist/mcp/utils/aceCodeSearch/filesystem.utils.js +146 -0
  48. package/dist/mcp/utils/aceCodeSearch/language.utils.d.ts +14 -0
  49. package/dist/mcp/utils/aceCodeSearch/language.utils.js +99 -0
  50. package/dist/mcp/utils/aceCodeSearch/search.utils.d.ts +31 -0
  51. package/dist/mcp/utils/aceCodeSearch/search.utils.js +136 -0
  52. package/dist/mcp/utils/aceCodeSearch/symbol.utils.d.ts +20 -0
  53. package/dist/mcp/utils/aceCodeSearch/symbol.utils.js +141 -0
  54. package/dist/mcp/utils/bash/security.utils.d.ts +20 -0
  55. package/dist/mcp/utils/bash/security.utils.js +34 -0
  56. package/dist/mcp/utils/filesystem/batch-operations.utils.d.ts +39 -0
  57. package/dist/mcp/utils/filesystem/batch-operations.utils.js +182 -0
  58. package/dist/mcp/utils/filesystem/code-analysis.utils.d.ts +18 -0
  59. package/dist/mcp/utils/filesystem/code-analysis.utils.js +165 -0
  60. package/dist/mcp/utils/filesystem/match-finder.utils.d.ts +16 -0
  61. package/dist/mcp/utils/filesystem/match-finder.utils.js +85 -0
  62. package/dist/mcp/utils/filesystem/similarity.utils.d.ts +22 -0
  63. package/dist/mcp/utils/filesystem/similarity.utils.js +75 -0
  64. package/dist/mcp/utils/todo/date.utils.d.ts +9 -0
  65. package/dist/mcp/utils/todo/date.utils.js +14 -0
  66. package/dist/mcp/utils/websearch/browser.utils.d.ts +8 -0
  67. package/dist/mcp/utils/websearch/browser.utils.js +58 -0
  68. package/dist/mcp/utils/websearch/text.utils.d.ts +16 -0
  69. package/dist/mcp/utils/websearch/text.utils.js +39 -0
  70. package/dist/mcp/websearch.d.ts +1 -31
  71. package/dist/mcp/websearch.js +21 -97
  72. package/dist/ui/components/ChatInput.d.ts +3 -1
  73. package/dist/ui/components/ChatInput.js +12 -5
  74. package/dist/ui/components/CommandPanel.d.ts +2 -1
  75. package/dist/ui/components/CommandPanel.js +18 -3
  76. package/dist/ui/components/MarkdownRenderer.d.ts +1 -2
  77. package/dist/ui/components/MarkdownRenderer.js +25 -153
  78. package/dist/ui/components/MessageList.js +5 -5
  79. package/dist/ui/components/PendingMessages.js +1 -1
  80. package/dist/ui/components/PendingToolCalls.d.ts +11 -0
  81. package/dist/ui/components/PendingToolCalls.js +35 -0
  82. package/dist/ui/components/SessionListScreen.js +37 -17
  83. package/dist/ui/components/ToolResultPreview.d.ts +1 -1
  84. package/dist/ui/components/ToolResultPreview.js +119 -155
  85. package/dist/ui/components/UsagePanel.d.ts +2 -0
  86. package/dist/ui/components/UsagePanel.js +360 -0
  87. package/dist/ui/pages/ChatScreen.d.ts +5 -0
  88. package/dist/ui/pages/ChatScreen.js +164 -85
  89. package/dist/ui/pages/ConfigScreen.js +23 -19
  90. package/dist/ui/pages/HeadlessModeScreen.js +2 -4
  91. package/dist/ui/pages/SubAgentConfigScreen.js +17 -17
  92. package/dist/ui/pages/SystemPromptConfigScreen.js +7 -6
  93. package/dist/utils/chatExporter.d.ts +9 -0
  94. package/dist/utils/chatExporter.js +126 -0
  95. package/dist/utils/commandExecutor.d.ts +3 -3
  96. package/dist/utils/commandExecutor.js +4 -4
  97. package/dist/utils/commands/export.d.ts +2 -0
  98. package/dist/utils/commands/export.js +12 -0
  99. package/dist/utils/commands/home.d.ts +2 -0
  100. package/dist/utils/commands/home.js +12 -0
  101. package/dist/utils/commands/init.js +3 -3
  102. package/dist/utils/commands/review.d.ts +2 -0
  103. package/dist/utils/commands/review.js +81 -0
  104. package/dist/utils/commands/role.d.ts +2 -0
  105. package/dist/utils/commands/role.js +37 -0
  106. package/dist/utils/commands/usage.d.ts +2 -0
  107. package/dist/utils/commands/usage.js +12 -0
  108. package/dist/utils/contextCompressor.js +99 -367
  109. package/dist/utils/fileDialog.d.ts +9 -0
  110. package/dist/utils/fileDialog.js +74 -0
  111. package/dist/utils/incrementalSnapshot.d.ts +7 -0
  112. package/dist/utils/incrementalSnapshot.js +35 -0
  113. package/dist/utils/mcpToolsManager.js +12 -12
  114. package/dist/utils/messageFormatter.js +89 -6
  115. package/dist/utils/proxyUtils.d.ts +15 -0
  116. package/dist/utils/proxyUtils.js +50 -0
  117. package/dist/utils/retryUtils.d.ts +27 -0
  118. package/dist/utils/retryUtils.js +114 -2
  119. package/dist/utils/sessionConverter.js +11 -0
  120. package/dist/utils/sessionManager.d.ts +7 -5
  121. package/dist/utils/sessionManager.js +60 -82
  122. package/dist/utils/terminal.js +4 -3
  123. package/dist/utils/toolDisplayConfig.d.ts +16 -0
  124. package/dist/utils/toolDisplayConfig.js +42 -0
  125. package/dist/utils/usageLogger.d.ts +11 -0
  126. package/dist/utils/usageLogger.js +99 -0
  127. package/package.json +3 -7
@@ -8,6 +8,7 @@ import PendingMessages from '../components/PendingMessages.js';
8
8
  import MCPInfoScreen from '../components/MCPInfoScreen.js';
9
9
  import MCPInfoPanel from '../components/MCPInfoPanel.js';
10
10
  import SessionListPanel from '../components/SessionListPanel.js';
11
+ import UsagePanel from '../components/UsagePanel.js';
11
12
  import MarkdownRenderer from '../components/MarkdownRenderer.js';
12
13
  import ToolConfirmation from '../components/ToolConfirmation.js';
13
14
  import DiffViewer from '../components/DiffViewer.js';
@@ -38,6 +39,11 @@ import '../../utils/commands/yolo.js';
38
39
  import '../../utils/commands/init.js';
39
40
  import '../../utils/commands/ide.js';
40
41
  import '../../utils/commands/compact.js';
42
+ import '../../utils/commands/home.js';
43
+ import '../../utils/commands/review.js';
44
+ import '../../utils/commands/role.js';
45
+ import '../../utils/commands/usage.js';
46
+ import '../../utils/commands/export.js';
41
47
  export default function ChatScreen({ skipWelcome }) {
42
48
  const [messages, setMessages] = useState([]);
43
49
  const [isSaving] = useState(false);
@@ -62,7 +68,9 @@ export default function ChatScreen({ skipWelcome }) {
62
68
  const [compressionError, setCompressionError] = useState(null);
63
69
  const [showSessionPanel, setShowSessionPanel] = useState(false);
64
70
  const [showMcpPanel, setShowMcpPanel] = useState(false);
71
+ const [showUsagePanel, setShowUsagePanel] = useState(false);
65
72
  const [shouldIncludeSystemInfo, setShouldIncludeSystemInfo] = useState(true); // Include on first message
73
+ const [restoreInputContent, setRestoreInputContent] = useState(null);
66
74
  const { columns: terminalWidth, rows: terminalHeight } = useTerminalSize();
67
75
  const { stdout } = useStdout();
68
76
  const workingDirectory = process.cwd();
@@ -86,6 +94,17 @@ export default function ChatScreen({ skipWelcome }) {
86
94
  // Ignore localStorage errors
87
95
  }
88
96
  }, [yoloMode]);
97
+ // Clear restore input content after it's been used
98
+ useEffect(() => {
99
+ if (restoreInputContent !== null) {
100
+ // Clear after a short delay to ensure ChatInput has processed it
101
+ const timer = setTimeout(() => {
102
+ setRestoreInputContent(null);
103
+ }, 100);
104
+ return () => clearTimeout(timer);
105
+ }
106
+ return undefined;
107
+ }, [restoreInputContent]);
89
108
  // Auto-resume last session when skipWelcome is true
90
109
  useEffect(() => {
91
110
  if (!skipWelcome)
@@ -161,6 +180,7 @@ export default function ChatScreen({ skipWelcome }) {
161
180
  setShowSessionPanel,
162
181
  setShowMcpInfo,
163
182
  setShowMcpPanel,
183
+ setShowUsagePanel,
164
184
  setMcpPanelKey,
165
185
  setYoloMode,
166
186
  setContextUsage: streamingState.setContextUsage,
@@ -224,6 +244,12 @@ export default function ChatScreen({ skipWelcome }) {
224
244
  }
225
245
  return;
226
246
  }
247
+ if (showUsagePanel) {
248
+ if (key.escape) {
249
+ setShowUsagePanel(false);
250
+ }
251
+ return;
252
+ }
227
253
  if (showMcpInfo) {
228
254
  if (key.escape) {
229
255
  setShowMcpInfo(false);
@@ -235,6 +261,8 @@ export default function ChatScreen({ skipWelcome }) {
235
261
  streamingState.abortController) {
236
262
  // Abort the controller
237
263
  streamingState.abortController.abort();
264
+ // Clear retry status immediately when user cancels
265
+ streamingState.setRetryStatus(null);
238
266
  // Remove all pending tool call messages (those with toolPending: true)
239
267
  setMessages(prev => prev.filter(msg => !msg.toolPending));
240
268
  // Add discontinued message
@@ -253,7 +281,7 @@ export default function ChatScreen({ skipWelcome }) {
253
281
  streamingState.setStreamTokenCount(0);
254
282
  }
255
283
  });
256
- const handleHistorySelect = async (selectedIndex, _message) => {
284
+ const handleHistorySelect = async (selectedIndex, message) => {
257
285
  // Count total files that will be rolled back (from selectedIndex onwards)
258
286
  let totalFileCount = 0;
259
287
  for (const [index, count] of snapshotState.snapshotFileCount.entries()) {
@@ -272,39 +300,95 @@ export default function ChatScreen({ skipWelcome }) {
272
300
  messageIndex: selectedIndex,
273
301
  fileCount: filePaths.length, // Use actual unique file count
274
302
  filePaths,
303
+ message, // Save message for restore after rollback
275
304
  });
276
305
  }
277
306
  else {
278
307
  // No files to rollback, just rollback conversation
279
- performRollback(selectedIndex, false);
308
+ // Note: message is already in input buffer via useHistoryNavigation
309
+ await performRollback(selectedIndex, false);
280
310
  }
281
311
  };
282
312
  const performRollback = async (selectedIndex, rollbackFiles) => {
313
+ const currentSession = sessionManager.getCurrentSession();
283
314
  // Rollback workspace to checkpoint if requested
284
- if (rollbackFiles) {
285
- const currentSession = sessionManager.getCurrentSession();
286
- if (currentSession) {
287
- // Use rollbackToMessageIndex to rollback all snapshots >= selectedIndex
288
- await incrementalSnapshotManager.rollbackToMessageIndex(currentSession.id, selectedIndex);
315
+ if (rollbackFiles && currentSession) {
316
+ // Use rollbackToMessageIndex to rollback all snapshots >= selectedIndex
317
+ await incrementalSnapshotManager.rollbackToMessageIndex(currentSession.id, selectedIndex);
318
+ }
319
+ // For session file: find the correct truncation point based on session messages
320
+ // We need to truncate to the same user message in the session file
321
+ if (currentSession) {
322
+ // Count how many user messages we're deleting (from selectedIndex onwards in UI)
323
+ const uiUserMessagesToDelete = messages
324
+ .slice(selectedIndex)
325
+ .filter(msg => msg.role === 'user').length;
326
+ // Find the corresponding user message in session to delete
327
+ // We start from the end and count backwards
328
+ let sessionUserMessageCount = 0;
329
+ let sessionTruncateIndex = currentSession.messages.length;
330
+ for (let i = currentSession.messages.length - 1; i >= 0; i--) {
331
+ const msg = currentSession.messages[i];
332
+ if (msg && msg.role === 'user') {
333
+ sessionUserMessageCount++;
334
+ if (sessionUserMessageCount === uiUserMessagesToDelete) {
335
+ // We want to delete from this user message onwards
336
+ sessionTruncateIndex = i;
337
+ break;
338
+ }
339
+ }
340
+ }
341
+ // Special case: rolling back to index 0 means deleting the entire session
342
+ if (sessionTruncateIndex === 0 && currentSession) {
343
+ // Delete all snapshots for this session
344
+ await incrementalSnapshotManager.clearAllSnapshots(currentSession.id);
345
+ // Delete the session file
346
+ await sessionManager.deleteSession(currentSession.id);
347
+ // Clear current session
348
+ sessionManager.clearCurrentSession();
349
+ // Clear all messages
350
+ setMessages([]);
351
+ // Clear saved messages
352
+ clearSavedMessages();
353
+ // Clear snapshot state
354
+ snapshotState.setSnapshotFileCount(new Map());
355
+ // Clear pending rollback dialog
356
+ snapshotState.setPendingRollback(null);
357
+ // Trigger remount
358
+ setRemountKey(prev => prev + 1);
359
+ return;
360
+ }
361
+ // Delete snapshot files >= selectedIndex (regardless of whether files were rolled back)
362
+ await incrementalSnapshotManager.deleteSnapshotsFromIndex(currentSession.id, selectedIndex);
363
+ // Reload snapshot file counts from disk after deletion
364
+ const snapshots = await incrementalSnapshotManager.listSnapshots(currentSession.id);
365
+ const counts = new Map();
366
+ for (const snapshot of snapshots) {
367
+ counts.set(snapshot.messageIndex, snapshot.fileCount);
289
368
  }
369
+ snapshotState.setSnapshotFileCount(counts);
370
+ // Truncate session messages
371
+ await sessionManager.truncateMessages(sessionTruncateIndex);
290
372
  }
291
- // Truncate messages array to remove the selected user message and everything after it
373
+ // Truncate UI messages array to remove the selected user message and everything after it
292
374
  setMessages(prev => prev.slice(0, selectedIndex));
293
- // Truncate session messages to match the UI state
294
- await sessionManager.truncateMessages(selectedIndex);
295
375
  clearSavedMessages();
296
376
  setRemountKey(prev => prev + 1);
297
377
  // Clear pending rollback dialog
298
378
  snapshotState.setPendingRollback(null);
299
379
  };
300
- const handleRollbackConfirm = (rollbackFiles) => {
380
+ const handleRollbackConfirm = async (rollbackFiles) => {
301
381
  if (rollbackFiles === null) {
302
382
  // User cancelled - just close the dialog without doing anything
303
383
  snapshotState.setPendingRollback(null);
304
384
  return;
305
385
  }
306
386
  if (snapshotState.pendingRollback) {
307
- performRollback(snapshotState.pendingRollback.messageIndex, rollbackFiles);
387
+ // Restore message to input before rollback
388
+ if (snapshotState.pendingRollback.message) {
389
+ setRestoreInputContent(snapshotState.pendingRollback.message);
390
+ }
391
+ await performRollback(snapshotState.pendingRollback.messageIndex, rollbackFiles);
308
392
  }
309
393
  };
310
394
  const handleSessionPanelSelect = async (sessionId) => {
@@ -351,6 +435,8 @@ export default function ChatScreen({ skipWelcome }) {
351
435
  await processMessage(message, images);
352
436
  };
353
437
  const processMessage = async (message, images, useBasicModel, hideUserMessage) => {
438
+ // Clear any previous retry status when starting a new request
439
+ streamingState.setRetryStatus(null);
354
440
  // Parse and validate file references
355
441
  const { cleanContent, validFiles } = await parseAndValidateFileReferences(message);
356
442
  // Separate image files from regular files
@@ -418,15 +504,18 @@ export default function ChatScreen({ skipWelcome }) {
418
504
  }
419
505
  catch (error) {
420
506
  if (controller.signal.aborted) {
421
- return;
507
+ // Don't return here - let finally block execute
508
+ // Just skip error display for aborted requests
509
+ }
510
+ else {
511
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
512
+ const finalMessage = {
513
+ role: 'assistant',
514
+ content: `Error: ${errorMessage}`,
515
+ streaming: false,
516
+ };
517
+ setMessages(prev => [...prev, finalMessage]);
422
518
  }
423
- const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
424
- const finalMessage = {
425
- role: 'assistant',
426
- content: `Error: ${errorMessage}`,
427
- streaming: false,
428
- };
429
- setMessages(prev => [...prev, finalMessage]);
430
519
  }
431
520
  finally {
432
521
  // End streaming
@@ -440,6 +529,8 @@ export default function ChatScreen({ skipWelcome }) {
440
529
  const processPendingMessages = async () => {
441
530
  if (pendingMessages.length === 0)
442
531
  return;
532
+ // Clear any previous retry status when starting a new request
533
+ streamingState.setRetryStatus(null);
443
534
  // Get current pending messages and clear them immediately
444
535
  const messagesToProcess = [...pendingMessages];
445
536
  setPendingMessages([]);
@@ -500,15 +591,18 @@ export default function ChatScreen({ skipWelcome }) {
500
591
  }
501
592
  catch (error) {
502
593
  if (controller.signal.aborted) {
503
- return;
594
+ // Don't return here - let finally block execute
595
+ // Just skip error display for aborted requests
596
+ }
597
+ else {
598
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
599
+ const finalMessage = {
600
+ role: 'assistant',
601
+ content: `Error: ${errorMessage}`,
602
+ streaming: false,
603
+ };
604
+ setMessages(prev => [...prev, finalMessage]);
504
605
  }
505
- const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
506
- const finalMessage = {
507
- role: 'assistant',
508
- content: `Error: ${errorMessage}`,
509
- streaming: false,
510
- };
511
- setMessages(prev => [...prev, finalMessage]);
512
606
  }
513
607
  finally {
514
608
  // End streaming
@@ -553,10 +647,11 @@ export default function ChatScreen({ skipWelcome }) {
553
647
  workingDirectory)))),
554
648
  ...messages
555
649
  .filter(m => !m.streaming)
556
- .map((message, index) => {
650
+ .map((message, index, filteredMessages) => {
557
651
  // Determine tool message type and color
558
652
  let toolStatusColor = 'cyan';
559
653
  let isToolMessage = false;
654
+ const isLastMessage = index === filteredMessages.length - 1;
560
655
  if (message.role === 'assistant') {
561
656
  if (message.content.startsWith('⚡')) {
562
657
  isToolMessage = true;
@@ -574,7 +669,7 @@ export default function ChatScreen({ skipWelcome }) {
574
669
  toolStatusColor = 'blue';
575
670
  }
576
671
  }
577
- return (React.createElement(Box, { key: `msg-${index}`, marginBottom: isToolMessage ? 0 : 1, paddingX: 1, flexDirection: "column", width: terminalWidth },
672
+ return (React.createElement(Box, { key: `msg-${index}`, marginTop: index > 0 ? 1 : 0, marginBottom: isLastMessage ? 1 : 0, paddingX: 1, flexDirection: "column", width: terminalWidth },
578
673
  React.createElement(Box, null,
579
674
  React.createElement(Text, { color: message.role === 'user'
580
675
  ? 'green'
@@ -585,18 +680,18 @@ export default function ChatScreen({ skipWelcome }) {
585
680
  : message.role === 'command'
586
681
  ? '⌘'
587
682
  : '❆'),
588
- React.createElement(Box, { marginLeft: 1, marginBottom: 1, flexDirection: "column" }, message.role === 'command' ? (React.createElement(Text, { color: "gray", dimColor: true },
589
- "\u2514\u2500 ",
590
- message.commandName)) : message.showTodoTree ? (React.createElement(TodoTree, { todos: currentTodos })) : (React.createElement(React.Fragment, null,
591
- React.createElement(MarkdownRenderer, { content: message.content || ' ', color: message.role === 'user'
683
+ React.createElement(Box, { marginLeft: 1, flexDirection: "column" }, message.role === 'command' ? (React.createElement(React.Fragment, null,
684
+ React.createElement(Text, { color: "gray", dimColor: true },
685
+ "\u2514\u2500 ",
686
+ message.commandName),
687
+ message.content && (React.createElement(Text, { color: "white" }, message.content)))) : message.showTodoTree ? (React.createElement(TodoTree, { todos: currentTodos })) : (React.createElement(React.Fragment, null,
688
+ message.role === 'user' || isToolMessage ? (React.createElement(Text, { color: message.role === 'user'
592
689
  ? 'gray'
593
- : isToolMessage
594
- ? message.content.startsWith('')
595
- ? 'yellow'
596
- : message.content.startsWith('')
597
- ? 'green'
598
- : 'red'
599
- : undefined }),
690
+ : message.content.startsWith('⚡')
691
+ ? 'yellow'
692
+ : message.content.startsWith('')
693
+ ? 'green'
694
+ : 'red' }, message.content || ' ')) : (React.createElement(MarkdownRenderer, { content: message.content || ' ' })),
600
695
  message.toolDisplay &&
601
696
  message.toolDisplay.args.length > 0 && (React.createElement(Box, { flexDirection: "column" }, message.toolDisplay.args.map((arg, argIndex) => (React.createElement(Text, { key: argIndex, color: "gray", dimColor: true },
602
697
  arg.isLast ? '└─' : '├─',
@@ -606,8 +701,7 @@ export default function ChatScreen({ skipWelcome }) {
606
701
  ' ',
607
702
  arg.value))))),
608
703
  message.toolCall &&
609
- (message.toolCall.name === 'filesystem-create' ||
610
- message.toolCall.name === 'filesystem-write') &&
704
+ message.toolCall.name === 'filesystem-create' &&
611
705
  message.toolCall.arguments.content && (React.createElement(Box, { marginTop: 1 },
612
706
  React.createElement(DiffViewer, { newContent: message.toolCall.arguments.content, filename: message.toolCall.arguments.path }))),
613
707
  message.toolCall &&
@@ -626,47 +720,27 @@ export default function ChatScreen({ skipWelcome }) {
626
720
  .completeOldContent, completeNewContent: message.toolCall.arguments
627
721
  .completeNewContent, startLineNumber: message.toolCall.arguments.contextStartLine }))),
628
722
  message.toolCall &&
629
- message.toolCall.name === 'terminal-execute' &&
630
- message.toolCall.arguments.command && (React.createElement(Box, { marginTop: 1, flexDirection: "column" },
631
- React.createElement(Text, { color: "gray", dimColor: true },
632
- "\u2514\u2500 Command:",
633
- ' ',
634
- React.createElement(Text, { color: "white" }, message.toolCall.arguments.command)),
635
- React.createElement(Text, { color: "gray", dimColor: true },
636
- "\u2514\u2500 Exit Code:",
637
- ' ',
638
- React.createElement(Text, { color: message.toolCall.arguments.exitCode === 0
639
- ? 'green'
640
- : 'red' }, message.toolCall.arguments.exitCode)),
641
- message.toolCall.arguments.stdout &&
642
- message.toolCall.arguments.stdout.trim()
643
- .length > 0 && (React.createElement(Box, { flexDirection: "column", marginTop: 1 },
644
- React.createElement(Text, { color: "green", dimColor: true }, "\u2514\u2500 stdout:"),
645
- React.createElement(Box, { paddingLeft: 2 },
646
- React.createElement(Text, { color: "white" }, message.toolCall.arguments.stdout
647
- .trim()
648
- .split('\n')
649
- .slice(0, 20)
650
- .join('\n')),
651
- message.toolCall.arguments.stdout
652
- .trim()
653
- .split('\n').length > 20 && (React.createElement(Text, { color: "gray", dimColor: true }, "... (output truncated)"))))),
654
- message.toolCall.arguments.stderr &&
655
- message.toolCall.arguments.stderr.trim()
656
- .length > 0 && (React.createElement(Box, { flexDirection: "column", marginTop: 1 },
657
- React.createElement(Text, { color: "red", dimColor: true }, "\u2514\u2500 stderr:"),
658
- React.createElement(Box, { paddingLeft: 2 },
659
- React.createElement(Text, { color: "red" }, message.toolCall.arguments.stderr
660
- .trim()
661
- .split('\n')
662
- .slice(0, 10)
663
- .join('\n')),
664
- message.toolCall.arguments.stderr
665
- .trim()
666
- .split('\n').length > 10 && (React.createElement(Text, { color: "gray", dimColor: true }, "... (output truncated)"))))))),
723
+ (message.toolCall.name === 'filesystem-edit' ||
724
+ message.toolCall.name ===
725
+ 'filesystem-edit_search') &&
726
+ message.toolCall.arguments.isBatch &&
727
+ message.toolCall.arguments.batchResults &&
728
+ Array.isArray(message.toolCall.arguments.batchResults) && (React.createElement(Box, { marginTop: 1, flexDirection: "column" }, message.toolCall.arguments.batchResults.map((fileResult, index) => {
729
+ if (fileResult.success &&
730
+ fileResult.oldContent &&
731
+ fileResult.newContent) {
732
+ return (React.createElement(Box, { key: index, flexDirection: "column", marginBottom: 1 },
733
+ React.createElement(Text, { bold: true, color: "cyan" }, `File ${index + 1}: ${fileResult.path}`),
734
+ React.createElement(DiffViewer, { oldContent: fileResult.oldContent, newContent: fileResult.newContent, filename: fileResult.path, completeOldContent: fileResult.completeOldContent, completeNewContent: fileResult.completeNewContent, startLineNumber: fileResult.contextStartLine })));
735
+ }
736
+ return null;
737
+ }))),
667
738
  message.content.startsWith('✓') &&
668
739
  message.toolResult &&
669
- !message.toolCall && (React.createElement(ToolResultPreview, { toolName: message.content
740
+ // 只在没有 diff 数据时显示预览(有 diff 的工具会用 DiffViewer 显示)
741
+ !(message.toolCall &&
742
+ (message.toolCall.arguments?.oldContent ||
743
+ message.toolCall.arguments?.batchResults)) && (React.createElement(ToolResultPreview, { toolName: message.content
670
744
  .replace('✓ ', '')
671
745
  .split('\n')[0] || '', result: message.toolResult, maxLines: 5 })),
672
746
  message.role === 'user' && message.systemInfo && (React.createElement(Box, { marginTop: 1, flexDirection: "column" },
@@ -747,13 +821,18 @@ export default function ChatScreen({ skipWelcome }) {
747
821
  React.createElement(MCPInfoPanel, null),
748
822
  React.createElement(Box, { marginTop: 1 },
749
823
  React.createElement(Text, { color: "gray", dimColor: true }, "Press ESC to close")))),
824
+ showUsagePanel && (React.createElement(Box, { paddingX: 1, flexDirection: "column", width: terminalWidth },
825
+ React.createElement(UsagePanel, null),
826
+ React.createElement(Box, { marginTop: 1 },
827
+ React.createElement(Text, { color: "gray", dimColor: true }, "Press ESC to close")))),
750
828
  snapshotState.pendingRollback && (React.createElement(FileRollbackConfirmation, { fileCount: snapshotState.pendingRollback.fileCount, filePaths: snapshotState.pendingRollback.filePaths || [], onConfirm: handleRollbackConfirm })),
751
829
  !pendingToolConfirmation &&
752
830
  !isCompressing &&
753
831
  !showSessionPanel &&
754
832
  !showMcpPanel &&
833
+ !showUsagePanel &&
755
834
  !snapshotState.pendingRollback && (React.createElement(React.Fragment, null,
756
- React.createElement(ChatInput, { onSubmit: handleMessageSubmit, onCommand: handleCommandExecution, placeholder: "Ask me anything about coding...", disabled: !!pendingToolConfirmation, chatHistory: messages, onHistorySelect: handleHistorySelect, yoloMode: yoloMode, contextUsage: streamingState.contextUsage
835
+ React.createElement(ChatInput, { onSubmit: handleMessageSubmit, onCommand: handleCommandExecution, placeholder: "Ask me anything about coding...", disabled: !!pendingToolConfirmation, isProcessing: streamingState.isStreaming || isSaving, chatHistory: messages, onHistorySelect: handleHistorySelect, yoloMode: yoloMode, contextUsage: streamingState.contextUsage
757
836
  ? {
758
837
  inputTokens: streamingState.contextUsage.prompt_tokens,
759
838
  maxContextTokens: getOpenAiConfig().maxContextTokens || 4000,
@@ -761,8 +840,8 @@ export default function ChatScreen({ skipWelcome }) {
761
840
  cacheReadTokens: streamingState.contextUsage.cache_read_input_tokens,
762
841
  cachedTokens: streamingState.contextUsage.cached_tokens,
763
842
  }
764
- : undefined }),
765
- vscodeState.vscodeConnectionStatus !== 'disconnected' && (React.createElement(Box, { marginTop: 1 },
843
+ : undefined, initialContent: restoreInputContent }),
844
+ vscodeState.vscodeConnectionStatus !== 'disconnected' && (React.createElement(Box, { marginTop: 1, paddingX: 1 },
766
845
  React.createElement(Text, { color: vscodeState.vscodeConnectionStatus === 'connecting'
767
846
  ? 'yellow'
768
847
  : vscodeState.vscodeConnectionStatus === 'connected'
@@ -294,25 +294,6 @@ export default function ConfigScreen({ onBack, onSave, inlineMode = false, }) {
294
294
  if (isFocusEventInput(rawInput)) {
295
295
  return;
296
296
  }
297
- // Handle profile shortcuts (highest priority - works even in Select mode)
298
- if (currentField === 'profile' && (input === 'n' || input === 'N')) {
299
- // Handle profile creation (works in both normal and editing mode)
300
- setProfileMode('creating');
301
- setNewProfileName('');
302
- setIsEditing(false); // Exit Select editing mode
303
- return;
304
- }
305
- if (currentField === 'profile' && (input === 'd' || input === 'D')) {
306
- // Handle profile deletion (works in both normal and editing mode)
307
- if (activeProfile === 'default') {
308
- setErrors(['Cannot delete the default profile']);
309
- setIsEditing(false);
310
- return;
311
- }
312
- setProfileMode('deleting');
313
- setIsEditing(false); // Exit Select editing mode
314
- return;
315
- }
316
297
  // Handle profile creation mode
317
298
  if (profileMode === 'creating') {
318
299
  if (key.return) {
@@ -336,6 +317,29 @@ export default function ConfigScreen({ onBack, onSave, inlineMode = false, }) {
336
317
  }
337
318
  return;
338
319
  }
320
+ // Handle profile shortcuts (only when in normal profile mode)
321
+ if (profileMode === 'normal' &&
322
+ currentField === 'profile' &&
323
+ (input === 'n' || input === 'N')) {
324
+ // Handle profile creation (works in both normal and editing mode)
325
+ setProfileMode('creating');
326
+ setNewProfileName('');
327
+ setIsEditing(false); // Exit Select editing mode
328
+ return;
329
+ }
330
+ if (profileMode === 'normal' &&
331
+ currentField === 'profile' &&
332
+ (input === 'd' || input === 'D')) {
333
+ // Handle profile deletion (works in both normal and editing mode)
334
+ if (activeProfile === 'default') {
335
+ setErrors(['Cannot delete the default profile']);
336
+ setIsEditing(false);
337
+ return;
338
+ }
339
+ setProfileMode('deleting');
340
+ setIsEditing(false); // Exit Select editing mode
341
+ return;
342
+ }
339
343
  // Handle loading state
340
344
  if (loading) {
341
345
  if (key.escape) {
@@ -6,6 +6,7 @@ import { handleConversationWithTools } from '../../hooks/useConversation.js';
6
6
  import { useStreamingState } from '../../hooks/useStreamingState.js';
7
7
  import { useToolConfirmation } from '../../hooks/useToolConfirmation.js';
8
8
  import { useVSCodeState } from '../../hooks/useVSCodeState.js';
9
+ import { useSessionSave } from '../../hooks/useSessionSave.js';
9
10
  import { parseAndValidateFileReferences, createMessageWithFileInstructions, getSystemInfo, } from '../../utils/fileUtils.js';
10
11
  // Console-based markdown renderer functions
11
12
  function renderConsoleMarkdown(content) {
@@ -202,6 +203,7 @@ export default function HeadlessModeScreen({ prompt, onComplete }) {
202
203
  // Use custom hooks
203
204
  const streamingState = useStreamingState();
204
205
  const vscodeState = useVSCodeState();
206
+ const { saveMessage } = useSessionSave();
205
207
  // Use tool confirmation hook
206
208
  const { requestToolConfirmation, isToolAutoApproved, addMultipleToAlwaysApproved, } = useToolConfirmation();
207
209
  // Listen for message changes to display AI responses and tool calls
@@ -335,10 +337,6 @@ export default function HeadlessModeScreen({ prompt, onComplete }) {
335
337
  console.log(`\x1b[36m└─ Assistant Response\x1b[0m`);
336
338
  // Create message for AI
337
339
  const messageForAI = createMessageWithFileInstructions(cleanContent, regularFiles, systemInfo, vscodeState.vscodeConnected ? vscodeState.editorContext : undefined);
338
- // Custom save message function for headless mode
339
- const saveMessage = async () => {
340
- // In headless mode, we don't need to save messages
341
- };
342
340
  // Start conversation with tool support
343
341
  await handleConversationWithTools({
344
342
  userContent: messageForAI,
@@ -8,30 +8,30 @@ const toolCategories = [
8
8
  {
9
9
  name: 'Filesystem Tools',
10
10
  tools: [
11
- 'filesystem_read',
12
- 'filesystem_create',
13
- 'filesystem_edit',
14
- 'filesystem_edit_search',
15
- 'filesystem_delete',
16
- 'filesystem_list',
11
+ 'filesystem-read',
12
+ 'filesystem-create',
13
+ 'filesystem-edit',
14
+ 'filesystem-edit_search',
15
+ 'filesystem-delete',
16
+ 'filesystem-list',
17
17
  ],
18
18
  },
19
19
  {
20
20
  name: 'ACE Code Search Tools',
21
21
  tools: [
22
- 'ace_search_symbols',
23
- 'ace_find_definition',
24
- 'ace_find_references',
25
- 'ace_semantic_search',
26
- 'ace_text_search',
27
- 'ace_file_outline',
28
- 'ace_index_stats',
29
- 'ace_clear_cache',
22
+ 'ace-search_symbols',
23
+ 'ace-find_definition',
24
+ 'ace-find_references',
25
+ 'ace-semantic_search',
26
+ 'ace-text_search',
27
+ 'ace-file_outline',
28
+ 'ace-index_stats',
29
+ 'ace-clear_cache',
30
30
  ],
31
31
  },
32
32
  {
33
33
  name: 'Terminal Tools',
34
- tools: ['terminal_execute'],
34
+ tools: ['terminal-execute'],
35
35
  },
36
36
  {
37
37
  name: 'TODO Management Tools',
@@ -45,11 +45,11 @@ const toolCategories = [
45
45
  },
46
46
  {
47
47
  name: 'Web Search Tools',
48
- tools: ['websearch_search', 'websearch_fetch'],
48
+ tools: ['websearch-search', 'websearch-fetch'],
49
49
  },
50
50
  {
51
51
  name: 'IDE Diagnostics Tools',
52
- tools: ['ide_get_diagnostics'],
52
+ tools: ['ide-get_diagnostics'],
53
53
  },
54
54
  ];
55
55
  export default function SubAgentConfigScreen({ onBack, onSave, inlineMode = false, agentId, }) {
@@ -4,7 +4,7 @@ import { spawn } from 'child_process';
4
4
  import { writeFileSync, readFileSync, existsSync, mkdirSync } from 'fs';
5
5
  import { join } from 'path';
6
6
  import { homedir, platform } from 'os';
7
- import { SYSTEM_PROMPT } from '../../api/systemPrompt.js';
7
+ import { getSystemPrompt } from '../../api/systemPrompt.js';
8
8
  const CONFIG_DIR = join(homedir(), '.snow');
9
9
  const SYSTEM_PROMPT_FILE = join(CONFIG_DIR, 'system-prompt.txt');
10
10
  function getSystemEditor() {
@@ -24,14 +24,14 @@ export default function SystemPromptConfigScreen({ onBack }) {
24
24
  const openEditor = async () => {
25
25
  ensureConfigDirectory();
26
26
  // 读取系统提示词文件,如果不存在则使用默认系统提示词
27
- let currentPrompt = SYSTEM_PROMPT;
27
+ let currentPrompt = getSystemPrompt();
28
28
  if (existsSync(SYSTEM_PROMPT_FILE)) {
29
29
  try {
30
30
  currentPrompt = readFileSync(SYSTEM_PROMPT_FILE, 'utf8');
31
31
  }
32
32
  catch {
33
33
  // 读取失败,使用默认
34
- currentPrompt = SYSTEM_PROMPT;
34
+ currentPrompt = getSystemPrompt();
35
35
  }
36
36
  }
37
37
  // 写入临时文件供编辑
@@ -39,7 +39,7 @@ export default function SystemPromptConfigScreen({ onBack }) {
39
39
  const editor = getSystemEditor();
40
40
  exit();
41
41
  const child = spawn(editor, [SYSTEM_PROMPT_FILE], {
42
- stdio: 'inherit'
42
+ stdio: 'inherit',
43
43
  });
44
44
  child.on('close', () => {
45
45
  // 读取编辑后的内容
@@ -47,7 +47,8 @@ export default function SystemPromptConfigScreen({ onBack }) {
47
47
  try {
48
48
  const editedContent = readFileSync(SYSTEM_PROMPT_FILE, 'utf8');
49
49
  const trimmedContent = editedContent.trim();
50
- if (trimmedContent === '' || trimmedContent === SYSTEM_PROMPT.trim()) {
50
+ if (trimmedContent === '' ||
51
+ trimmedContent === getSystemPrompt().trim()) {
51
52
  // 内容为空或与默认相同,删除文件,使用默认提示词
52
53
  try {
53
54
  const fs = require('fs');
@@ -72,7 +73,7 @@ export default function SystemPromptConfigScreen({ onBack }) {
72
73
  }
73
74
  process.exit(0);
74
75
  });
75
- child.on('error', (error) => {
76
+ child.on('error', error => {
76
77
  console.error('Failed to open editor:', error.message);
77
78
  process.exit(1);
78
79
  });
@@ -0,0 +1,9 @@
1
+ import type { Message } from '../ui/components/MessageList.js';
2
+ /**
3
+ * Format messages to plain text for export
4
+ */
5
+ export declare function formatMessagesAsText(messages: Message[]): string;
6
+ /**
7
+ * Export messages to a file
8
+ */
9
+ export declare function exportMessagesToFile(messages: Message[], filePath: string): Promise<void>;