snow-ai 0.3.7 → 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 (53) hide show
  1. package/dist/agents/compactAgent.js +7 -3
  2. package/dist/agents/summaryAgent.d.ts +57 -0
  3. package/dist/agents/summaryAgent.js +259 -0
  4. package/dist/api/anthropic.d.ts +1 -0
  5. package/dist/api/anthropic.js +20 -13
  6. package/dist/api/chat.d.ts +1 -0
  7. package/dist/api/chat.js +23 -12
  8. package/dist/api/gemini.d.ts +1 -0
  9. package/dist/api/gemini.js +14 -8
  10. package/dist/api/responses.d.ts +1 -0
  11. package/dist/api/responses.js +23 -15
  12. package/dist/app.js +15 -2
  13. package/dist/hooks/useCommandHandler.js +58 -0
  14. package/dist/hooks/useCommandPanel.d.ts +2 -1
  15. package/dist/hooks/useCommandPanel.js +6 -1
  16. package/dist/hooks/useConversation.js +44 -24
  17. package/dist/hooks/useSnapshotState.d.ts +2 -0
  18. package/dist/mcp/filesystem.d.ts +131 -46
  19. package/dist/mcp/filesystem.js +188 -35
  20. package/dist/mcp/types/filesystem.types.d.ts +91 -0
  21. package/dist/mcp/utils/filesystem/batch-operations.utils.d.ts +39 -0
  22. package/dist/mcp/utils/filesystem/batch-operations.utils.js +182 -0
  23. package/dist/ui/components/ChatInput.d.ts +2 -1
  24. package/dist/ui/components/ChatInput.js +3 -3
  25. package/dist/ui/components/CommandPanel.d.ts +2 -1
  26. package/dist/ui/components/CommandPanel.js +18 -3
  27. package/dist/ui/components/MarkdownRenderer.js +10 -1
  28. package/dist/ui/components/MessageList.js +1 -1
  29. package/dist/ui/components/PendingMessages.js +1 -1
  30. package/dist/ui/components/PendingToolCalls.d.ts +11 -0
  31. package/dist/ui/components/PendingToolCalls.js +35 -0
  32. package/dist/ui/components/ToolResultPreview.d.ts +1 -1
  33. package/dist/ui/components/ToolResultPreview.js +116 -152
  34. package/dist/ui/pages/ChatScreen.d.ts +1 -0
  35. package/dist/ui/pages/ChatScreen.js +99 -60
  36. package/dist/utils/chatExporter.d.ts +9 -0
  37. package/dist/utils/chatExporter.js +126 -0
  38. package/dist/utils/commandExecutor.d.ts +1 -1
  39. package/dist/utils/commands/export.d.ts +2 -0
  40. package/dist/utils/commands/export.js +12 -0
  41. package/dist/utils/commands/init.js +3 -3
  42. package/dist/utils/fileDialog.d.ts +9 -0
  43. package/dist/utils/fileDialog.js +74 -0
  44. package/dist/utils/fileUtils.js +3 -3
  45. package/dist/utils/incrementalSnapshot.d.ts +7 -0
  46. package/dist/utils/incrementalSnapshot.js +35 -0
  47. package/dist/utils/messageFormatter.js +89 -6
  48. package/dist/utils/sessionConverter.js +11 -0
  49. package/dist/utils/sessionManager.d.ts +5 -0
  50. package/dist/utils/sessionManager.js +45 -0
  51. package/dist/utils/toolDisplayConfig.d.ts +16 -0
  52. package/dist/utils/toolDisplayConfig.js +42 -0
  53. package/package.json +1 -1
@@ -43,6 +43,7 @@ import '../../utils/commands/home.js';
43
43
  import '../../utils/commands/review.js';
44
44
  import '../../utils/commands/role.js';
45
45
  import '../../utils/commands/usage.js';
46
+ import '../../utils/commands/export.js';
46
47
  export default function ChatScreen({ skipWelcome }) {
47
48
  const [messages, setMessages] = useState([]);
48
49
  const [isSaving] = useState(false);
@@ -260,6 +261,8 @@ export default function ChatScreen({ skipWelcome }) {
260
261
  streamingState.abortController) {
261
262
  // Abort the controller
262
263
  streamingState.abortController.abort();
264
+ // Clear retry status immediately when user cancels
265
+ streamingState.setRetryStatus(null);
263
266
  // Remove all pending tool call messages (those with toolPending: true)
264
267
  setMessages(prev => prev.filter(msg => !msg.toolPending));
265
268
  // Add discontinued message
@@ -279,10 +282,6 @@ export default function ChatScreen({ skipWelcome }) {
279
282
  }
280
283
  });
281
284
  const handleHistorySelect = async (selectedIndex, message) => {
282
- // If rolling back to the first message (index 0), save the message content to restore in input
283
- if (selectedIndex === 0) {
284
- setRestoreInputContent(message);
285
- }
286
285
  // Count total files that will be rolled back (from selectedIndex onwards)
287
286
  let totalFileCount = 0;
288
287
  for (const [index, count] of snapshotState.snapshotFileCount.entries()) {
@@ -301,39 +300,95 @@ export default function ChatScreen({ skipWelcome }) {
301
300
  messageIndex: selectedIndex,
302
301
  fileCount: filePaths.length, // Use actual unique file count
303
302
  filePaths,
303
+ message, // Save message for restore after rollback
304
304
  });
305
305
  }
306
306
  else {
307
307
  // No files to rollback, just rollback conversation
308
- performRollback(selectedIndex, false);
308
+ // Note: message is already in input buffer via useHistoryNavigation
309
+ await performRollback(selectedIndex, false);
309
310
  }
310
311
  };
311
312
  const performRollback = async (selectedIndex, rollbackFiles) => {
313
+ const currentSession = sessionManager.getCurrentSession();
312
314
  // Rollback workspace to checkpoint if requested
313
- if (rollbackFiles) {
314
- const currentSession = sessionManager.getCurrentSession();
315
- if (currentSession) {
316
- // Use rollbackToMessageIndex to rollback all snapshots >= selectedIndex
317
- 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;
318
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);
368
+ }
369
+ snapshotState.setSnapshotFileCount(counts);
370
+ // Truncate session messages
371
+ await sessionManager.truncateMessages(sessionTruncateIndex);
319
372
  }
320
- // 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
321
374
  setMessages(prev => prev.slice(0, selectedIndex));
322
- // Truncate session messages to match the UI state
323
- await sessionManager.truncateMessages(selectedIndex);
324
375
  clearSavedMessages();
325
376
  setRemountKey(prev => prev + 1);
326
377
  // Clear pending rollback dialog
327
378
  snapshotState.setPendingRollback(null);
328
379
  };
329
- const handleRollbackConfirm = (rollbackFiles) => {
380
+ const handleRollbackConfirm = async (rollbackFiles) => {
330
381
  if (rollbackFiles === null) {
331
382
  // User cancelled - just close the dialog without doing anything
332
383
  snapshotState.setPendingRollback(null);
333
384
  return;
334
385
  }
335
386
  if (snapshotState.pendingRollback) {
336
- 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);
337
392
  }
338
393
  };
339
394
  const handleSessionPanelSelect = async (sessionId) => {
@@ -380,6 +435,8 @@ export default function ChatScreen({ skipWelcome }) {
380
435
  await processMessage(message, images);
381
436
  };
382
437
  const processMessage = async (message, images, useBasicModel, hideUserMessage) => {
438
+ // Clear any previous retry status when starting a new request
439
+ streamingState.setRetryStatus(null);
383
440
  // Parse and validate file references
384
441
  const { cleanContent, validFiles } = await parseAndValidateFileReferences(message);
385
442
  // Separate image files from regular files
@@ -472,6 +529,8 @@ export default function ChatScreen({ skipWelcome }) {
472
529
  const processPendingMessages = async () => {
473
530
  if (pendingMessages.length === 0)
474
531
  return;
532
+ // Clear any previous retry status when starting a new request
533
+ streamingState.setRetryStatus(null);
475
534
  // Get current pending messages and clear them immediately
476
535
  const messagesToProcess = [...pendingMessages];
477
536
  setPendingMessages([]);
@@ -588,10 +647,11 @@ export default function ChatScreen({ skipWelcome }) {
588
647
  workingDirectory)))),
589
648
  ...messages
590
649
  .filter(m => !m.streaming)
591
- .map((message, index) => {
650
+ .map((message, index, filteredMessages) => {
592
651
  // Determine tool message type and color
593
652
  let toolStatusColor = 'cyan';
594
653
  let isToolMessage = false;
654
+ const isLastMessage = index === filteredMessages.length - 1;
595
655
  if (message.role === 'assistant') {
596
656
  if (message.content.startsWith('⚡')) {
597
657
  isToolMessage = true;
@@ -609,7 +669,7 @@ export default function ChatScreen({ skipWelcome }) {
609
669
  toolStatusColor = 'blue';
610
670
  }
611
671
  }
612
- 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 },
613
673
  React.createElement(Box, null,
614
674
  React.createElement(Text, { color: message.role === 'user'
615
675
  ? 'green'
@@ -620,7 +680,7 @@ export default function ChatScreen({ skipWelcome }) {
620
680
  : message.role === 'command'
621
681
  ? '⌘'
622
682
  : '❆'),
623
- React.createElement(Box, { marginLeft: 1, marginBottom: 1, flexDirection: "column" }, message.role === 'command' ? (React.createElement(React.Fragment, null,
683
+ React.createElement(Box, { marginLeft: 1, flexDirection: "column" }, message.role === 'command' ? (React.createElement(React.Fragment, null,
624
684
  React.createElement(Text, { color: "gray", dimColor: true },
625
685
  "\u2514\u2500 ",
626
686
  message.commandName),
@@ -641,8 +701,7 @@ export default function ChatScreen({ skipWelcome }) {
641
701
  ' ',
642
702
  arg.value))))),
643
703
  message.toolCall &&
644
- (message.toolCall.name === 'filesystem-create' ||
645
- message.toolCall.name === 'filesystem-write') &&
704
+ message.toolCall.name === 'filesystem-create' &&
646
705
  message.toolCall.arguments.content && (React.createElement(Box, { marginTop: 1 },
647
706
  React.createElement(DiffViewer, { newContent: message.toolCall.arguments.content, filename: message.toolCall.arguments.path }))),
648
707
  message.toolCall &&
@@ -661,47 +720,27 @@ export default function ChatScreen({ skipWelcome }) {
661
720
  .completeOldContent, completeNewContent: message.toolCall.arguments
662
721
  .completeNewContent, startLineNumber: message.toolCall.arguments.contextStartLine }))),
663
722
  message.toolCall &&
664
- message.toolCall.name === 'terminal-execute' &&
665
- message.toolCall.arguments.command && (React.createElement(Box, { marginTop: 1, flexDirection: "column" },
666
- React.createElement(Text, { color: "gray", dimColor: true },
667
- "\u2514\u2500 Command:",
668
- ' ',
669
- React.createElement(Text, { color: "white" }, message.toolCall.arguments.command)),
670
- React.createElement(Text, { color: "gray", dimColor: true },
671
- "\u2514\u2500 Exit Code:",
672
- ' ',
673
- React.createElement(Text, { color: message.toolCall.arguments.exitCode === 0
674
- ? 'green'
675
- : 'red' }, message.toolCall.arguments.exitCode)),
676
- message.toolCall.arguments.stdout &&
677
- message.toolCall.arguments.stdout.trim()
678
- .length > 0 && (React.createElement(Box, { flexDirection: "column", marginTop: 1 },
679
- React.createElement(Text, { color: "green", dimColor: true }, "\u2514\u2500 stdout:"),
680
- React.createElement(Box, { paddingLeft: 2 },
681
- React.createElement(Text, { color: "white" }, message.toolCall.arguments.stdout
682
- .trim()
683
- .split('\n')
684
- .slice(0, 20)
685
- .join('\n')),
686
- message.toolCall.arguments.stdout
687
- .trim()
688
- .split('\n').length > 20 && (React.createElement(Text, { color: "gray", dimColor: true }, "... (output truncated)"))))),
689
- message.toolCall.arguments.stderr &&
690
- message.toolCall.arguments.stderr.trim()
691
- .length > 0 && (React.createElement(Box, { flexDirection: "column", marginTop: 1 },
692
- React.createElement(Text, { color: "red", dimColor: true }, "\u2514\u2500 stderr:"),
693
- React.createElement(Box, { paddingLeft: 2 },
694
- React.createElement(Text, { color: "red" }, message.toolCall.arguments.stderr
695
- .trim()
696
- .split('\n')
697
- .slice(0, 10)
698
- .join('\n')),
699
- message.toolCall.arguments.stderr
700
- .trim()
701
- .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
+ }))),
702
738
  message.content.startsWith('✓') &&
703
739
  message.toolResult &&
704
- !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
705
744
  .replace('✓ ', '')
706
745
  .split('\n')[0] || '', result: message.toolResult, maxLines: 5 })),
707
746
  message.role === 'user' && message.systemInfo && (React.createElement(Box, { marginTop: 1, flexDirection: "column" },
@@ -793,7 +832,7 @@ export default function ChatScreen({ skipWelcome }) {
793
832
  !showMcpPanel &&
794
833
  !showUsagePanel &&
795
834
  !snapshotState.pendingRollback && (React.createElement(React.Fragment, null,
796
- 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
797
836
  ? {
798
837
  inputTokens: streamingState.contextUsage.prompt_tokens,
799
838
  maxContextTokens: getOpenAiConfig().maxContextTokens || 4000,
@@ -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>;
@@ -0,0 +1,126 @@
1
+ import * as fs from 'fs/promises';
2
+ /**
3
+ * Format messages to plain text for export
4
+ */
5
+ export function formatMessagesAsText(messages) {
6
+ const lines = [];
7
+ const timestamp = new Date().toISOString();
8
+ // Add header
9
+ lines.push('=====================================');
10
+ lines.push('Snow AI - Chat Export');
11
+ lines.push(`Exported at: ${new Date(timestamp).toLocaleString()}`);
12
+ lines.push('=====================================');
13
+ lines.push('');
14
+ // Format each message
15
+ for (const message of messages) {
16
+ // Skip command messages
17
+ if (message.role === 'command') {
18
+ continue;
19
+ }
20
+ // Add role header
21
+ let roleLabel = '';
22
+ if (message.role === 'user') {
23
+ roleLabel = 'USER';
24
+ }
25
+ else if (message.role === 'assistant') {
26
+ roleLabel = 'ASSISTANT';
27
+ }
28
+ else if (message.role === 'subagent') {
29
+ roleLabel = 'SUBAGENT';
30
+ }
31
+ else {
32
+ roleLabel = 'UNKNOWN';
33
+ }
34
+ lines.push(`[${roleLabel}]`);
35
+ lines.push('-'.repeat(40));
36
+ // Add content (Message.content is always string based on the type definition)
37
+ lines.push(message.content);
38
+ // Add tool call information if present
39
+ if (message.toolCall) {
40
+ lines.push('');
41
+ lines.push(`[TOOL CALL: ${message.toolCall.name}]`);
42
+ try {
43
+ const argsStr = typeof message.toolCall.arguments === 'string'
44
+ ? message.toolCall.arguments
45
+ : JSON.stringify(message.toolCall.arguments, null, 2);
46
+ lines.push(argsStr);
47
+ }
48
+ catch {
49
+ lines.push(String(message.toolCall.arguments));
50
+ }
51
+ }
52
+ // Add tool display information if present
53
+ if (message.toolDisplay) {
54
+ lines.push('');
55
+ lines.push(`[TOOL: ${message.toolDisplay.toolName}]`);
56
+ for (const arg of message.toolDisplay.args) {
57
+ lines.push(` ${arg.key}: ${arg.value}`);
58
+ }
59
+ }
60
+ // Add tool result if present
61
+ if (message.toolResult) {
62
+ lines.push('');
63
+ lines.push('[TOOL RESULT]');
64
+ try {
65
+ const result = JSON.parse(message.toolResult);
66
+ lines.push(JSON.stringify(result, null, 2));
67
+ }
68
+ catch {
69
+ lines.push(message.toolResult);
70
+ }
71
+ }
72
+ // Add terminal result if present
73
+ if (message.terminalResult) {
74
+ lines.push('');
75
+ if (message.terminalResult.command) {
76
+ lines.push(`[COMMAND: ${message.terminalResult.command}]`);
77
+ }
78
+ if (message.terminalResult.stdout) {
79
+ lines.push('[STDOUT]');
80
+ lines.push(message.terminalResult.stdout);
81
+ }
82
+ if (message.terminalResult.stderr) {
83
+ lines.push('[STDERR]');
84
+ lines.push(message.terminalResult.stderr);
85
+ }
86
+ if (message.terminalResult.exitCode !== undefined) {
87
+ lines.push(`[EXIT CODE: ${message.terminalResult.exitCode}]`);
88
+ }
89
+ }
90
+ // Add images information if present
91
+ if (message.images && message.images.length > 0) {
92
+ lines.push('');
93
+ lines.push(`[${message.images.length} image(s) attached]`);
94
+ }
95
+ // Add system info if present
96
+ if (message.systemInfo) {
97
+ lines.push('');
98
+ lines.push('[SYSTEM INFO]');
99
+ lines.push(`Platform: ${message.systemInfo.platform}`);
100
+ lines.push(`Shell: ${message.systemInfo.shell}`);
101
+ lines.push(`Working Directory: ${message.systemInfo.workingDirectory}`);
102
+ }
103
+ // Add files information if present
104
+ if (message.files && message.files.length > 0) {
105
+ lines.push('');
106
+ lines.push(`[${message.files.length} file(s) referenced]`);
107
+ for (const file of message.files) {
108
+ lines.push(` - ${file.path}`);
109
+ }
110
+ }
111
+ lines.push('');
112
+ lines.push('');
113
+ }
114
+ // Add footer
115
+ lines.push('=====================================');
116
+ lines.push('End of Chat Export');
117
+ lines.push('=====================================');
118
+ return lines.join('\n');
119
+ }
120
+ /**
121
+ * Export messages to a file
122
+ */
123
+ export async function exportMessagesToFile(messages, filePath) {
124
+ const textContent = formatMessagesAsText(messages);
125
+ await fs.writeFile(filePath, textContent, 'utf-8');
126
+ }
@@ -1,7 +1,7 @@
1
1
  export interface CommandResult {
2
2
  success: boolean;
3
3
  message?: string;
4
- action?: 'clear' | 'resume' | 'info' | 'showMcpInfo' | 'toggleYolo' | 'initProject' | 'compact' | 'showSessionPanel' | 'showMcpPanel' | 'showUsagePanel' | 'home' | 'review';
4
+ action?: 'clear' | 'resume' | 'info' | 'showMcpInfo' | 'toggleYolo' | 'initProject' | 'compact' | 'showSessionPanel' | 'showMcpPanel' | 'showUsagePanel' | 'home' | 'review' | 'exportChat';
5
5
  prompt?: string;
6
6
  alreadyConnected?: boolean;
7
7
  }
@@ -0,0 +1,2 @@
1
+ declare const _default: {};
2
+ export default _default;
@@ -0,0 +1,12 @@
1
+ import { registerCommand } from '../commandExecutor.js';
2
+ // Export command handler - exports chat conversation to text file
3
+ registerCommand('export', {
4
+ execute: () => {
5
+ return {
6
+ success: true,
7
+ action: 'exportChat',
8
+ message: 'Exporting conversation...'
9
+ };
10
+ }
11
+ });
12
+ export default {};
@@ -83,11 +83,11 @@ License information (check package.json or LICENSE file)
83
83
  - Be thorough but concise - focus on essential information
84
84
  - If SNOW.md already exists, read it first and UPDATE it rather than replace
85
85
  - Format with proper Markdown syntax
86
- - After generating content, use filesystem-write or filesystem-create to save SNOW.md in the project root
86
+ - After generating content, use filesystem-create to save SNOW.md in the project root
87
87
  - Confirm completion with a brief summary
88
88
 
89
- Begin your analysis now. Use every tool at your disposal to understand this project completely.`
89
+ Begin your analysis now. Use every tool at your disposal to understand this project completely.`,
90
90
  };
91
- }
91
+ },
92
92
  });
93
93
  export default {};
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Cross-platform file save dialog
3
+ * Opens a native file save dialog and returns the selected path
4
+ */
5
+ export declare function showSaveDialog(defaultFilename?: string, title?: string): Promise<string | null>;
6
+ /**
7
+ * Check if native file dialogs are available on this platform
8
+ */
9
+ export declare function isFileDialogSupported(): boolean;
@@ -0,0 +1,74 @@
1
+ import { exec } from 'child_process';
2
+ import { promisify } from 'util';
3
+ import * as path from 'path';
4
+ import * as os from 'os';
5
+ const execAsync = promisify(exec);
6
+ /**
7
+ * Cross-platform file save dialog
8
+ * Opens a native file save dialog and returns the selected path
9
+ */
10
+ export async function showSaveDialog(defaultFilename = 'export.txt', title = 'Save File') {
11
+ const platform = os.platform();
12
+ try {
13
+ if (platform === 'darwin') {
14
+ // macOS - use osascript (AppleScript)
15
+ const defaultPath = path.join(os.homedir(), 'Downloads', defaultFilename);
16
+ const script = `
17
+ set defaultPath to POSIX file "${defaultPath}"
18
+ set saveFile to choose file name with prompt "${title}" default location (POSIX file "${os.homedir()}/Downloads") default name "${defaultFilename}"
19
+ return POSIX path of saveFile
20
+ `;
21
+ const { stdout } = await execAsync(`osascript -e '${script.replace(/'/g, "'\\''")}'`);
22
+ return stdout.trim();
23
+ }
24
+ else if (platform === 'win32') {
25
+ // Windows - use PowerShell
26
+ const script = `
27
+ Add-Type -AssemblyName System.Windows.Forms
28
+ $dialog = New-Object System.Windows.Forms.SaveFileDialog
29
+ $dialog.Title = "${title}"
30
+ $dialog.Filter = "Text files (*.txt)|*.txt|Markdown files (*.md)|*.md|All files (*.*)|*.*"
31
+ $dialog.FileName = "${defaultFilename}"
32
+ $dialog.InitialDirectory = "${path.join(os.homedir(), 'Downloads').replace(/\\/g, '\\\\')}"
33
+ $result = $dialog.ShowDialog()
34
+ if ($result -eq 'OK') {
35
+ Write-Output $dialog.FileName
36
+ }
37
+ `;
38
+ const { stdout } = await execAsync(`powershell -NoProfile -Command "${script.replace(/"/g, '\\"')}"`);
39
+ const result = stdout.trim();
40
+ return result || null;
41
+ }
42
+ else {
43
+ // Linux - use zenity (most common) or kdialog as fallback
44
+ try {
45
+ const defaultPath = path.join(os.homedir(), 'Downloads', defaultFilename);
46
+ const { stdout } = await execAsync(`zenity --file-selection --save --title="${title}" --filename="${defaultPath}" --confirm-overwrite`);
47
+ return stdout.trim();
48
+ }
49
+ catch (error) {
50
+ // Try kdialog as fallback for KDE systems
51
+ try {
52
+ const defaultPath = path.join(os.homedir(), 'Downloads', defaultFilename);
53
+ const { stdout } = await execAsync(`kdialog --getsavefilename "${defaultPath}" "*.*|All Files" --title "${title}"`);
54
+ return stdout.trim();
55
+ }
56
+ catch {
57
+ // If both fail, return null
58
+ return null;
59
+ }
60
+ }
61
+ }
62
+ }
63
+ catch (error) {
64
+ // User cancelled or error occurred
65
+ return null;
66
+ }
67
+ }
68
+ /**
69
+ * Check if native file dialogs are available on this platform
70
+ */
71
+ export function isFileDialogSupported() {
72
+ const platform = os.platform();
73
+ return platform === 'darwin' || platform === 'win32' || platform === 'linux';
74
+ }
@@ -177,9 +177,9 @@ export function createMessageWithFileInstructions(content, files, systemInfo, ed
177
177
  if (editorContext.cursorPosition) {
178
178
  editorLines.push(`└─ Cursor: Line ${editorContext.cursorPosition.line + 1}, Column ${editorContext.cursorPosition.character + 1}`);
179
179
  }
180
- // if (editorContext.selectedText) {
181
- // editorLines.push(`└─ Selected Code:\n\`\`\`\n${editorContext.selectedText}\n\`\`\``);
182
- // }
180
+ if (editorContext.selectedText) {
181
+ editorLines.push(`└─ Selected Code:\n\`\`\`\n${editorContext.selectedText}\n\`\`\``);
182
+ }
183
183
  if (editorLines.length > 0) {
184
184
  parts.push(editorLines.join('\n'));
185
185
  }
@@ -75,6 +75,13 @@ declare class IncrementalSnapshotManager {
75
75
  * @returns Number of files rolled back
76
76
  */
77
77
  rollbackToMessageIndex(sessionId: string, targetMessageIndex: number): Promise<number>;
78
+ /**
79
+ * Delete all snapshots >= targetMessageIndex
80
+ * This is used when user rolls back conversation to clean up snapshot files
81
+ * @param sessionId Session ID
82
+ * @param targetMessageIndex The message index to delete from (inclusive)
83
+ */
84
+ deleteSnapshotsFromIndex(sessionId: string, targetMessageIndex: number): Promise<number>;
78
85
  /**
79
86
  * Clear all snapshots for a session
80
87
  */
@@ -255,6 +255,41 @@ class IncrementalSnapshotManager {
255
255
  return 0;
256
256
  }
257
257
  }
258
+ /**
259
+ * Delete all snapshots >= targetMessageIndex
260
+ * This is used when user rolls back conversation to clean up snapshot files
261
+ * @param sessionId Session ID
262
+ * @param targetMessageIndex The message index to delete from (inclusive)
263
+ */
264
+ async deleteSnapshotsFromIndex(sessionId, targetMessageIndex) {
265
+ await this.ensureSnapshotsDir();
266
+ try {
267
+ const files = await fs.readdir(this.snapshotsDir);
268
+ let deletedCount = 0;
269
+ for (const file of files) {
270
+ if (file.startsWith(sessionId) && file.endsWith('.json')) {
271
+ const snapshotPath = path.join(this.snapshotsDir, file);
272
+ const content = await fs.readFile(snapshotPath, 'utf-8');
273
+ const metadata = JSON.parse(content);
274
+ // Delete snapshots with messageIndex >= targetMessageIndex
275
+ if (metadata.messageIndex >= targetMessageIndex) {
276
+ try {
277
+ await fs.unlink(snapshotPath);
278
+ deletedCount++;
279
+ }
280
+ catch (error) {
281
+ console.error(`Failed to delete snapshot file ${snapshotPath}:`, error);
282
+ }
283
+ }
284
+ }
285
+ }
286
+ return deletedCount;
287
+ }
288
+ catch (error) {
289
+ console.error('Failed to delete snapshots from index:', error);
290
+ return 0;
291
+ }
292
+ }
258
293
  /**
259
294
  * Clear all snapshots for a session
260
295
  */