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.
- package/dist/agents/compactAgent.js +7 -3
- package/dist/agents/summaryAgent.d.ts +57 -0
- package/dist/agents/summaryAgent.js +259 -0
- package/dist/api/anthropic.d.ts +1 -0
- package/dist/api/anthropic.js +20 -13
- package/dist/api/chat.d.ts +1 -0
- package/dist/api/chat.js +23 -12
- package/dist/api/gemini.d.ts +1 -0
- package/dist/api/gemini.js +14 -8
- package/dist/api/responses.d.ts +1 -0
- package/dist/api/responses.js +23 -15
- package/dist/app.js +15 -2
- package/dist/hooks/useCommandHandler.js +58 -0
- package/dist/hooks/useCommandPanel.d.ts +2 -1
- package/dist/hooks/useCommandPanel.js +6 -1
- package/dist/hooks/useConversation.js +44 -24
- package/dist/hooks/useSnapshotState.d.ts +2 -0
- package/dist/mcp/filesystem.d.ts +131 -46
- package/dist/mcp/filesystem.js +188 -35
- package/dist/mcp/types/filesystem.types.d.ts +91 -0
- package/dist/mcp/utils/filesystem/batch-operations.utils.d.ts +39 -0
- package/dist/mcp/utils/filesystem/batch-operations.utils.js +182 -0
- package/dist/ui/components/ChatInput.d.ts +2 -1
- package/dist/ui/components/ChatInput.js +3 -3
- package/dist/ui/components/CommandPanel.d.ts +2 -1
- package/dist/ui/components/CommandPanel.js +18 -3
- package/dist/ui/components/MarkdownRenderer.js +10 -1
- package/dist/ui/components/MessageList.js +1 -1
- package/dist/ui/components/PendingMessages.js +1 -1
- package/dist/ui/components/PendingToolCalls.d.ts +11 -0
- package/dist/ui/components/PendingToolCalls.js +35 -0
- package/dist/ui/components/ToolResultPreview.d.ts +1 -1
- package/dist/ui/components/ToolResultPreview.js +116 -152
- package/dist/ui/pages/ChatScreen.d.ts +1 -0
- package/dist/ui/pages/ChatScreen.js +99 -60
- package/dist/utils/chatExporter.d.ts +9 -0
- package/dist/utils/chatExporter.js +126 -0
- package/dist/utils/commandExecutor.d.ts +1 -1
- package/dist/utils/commands/export.d.ts +2 -0
- package/dist/utils/commands/export.js +12 -0
- package/dist/utils/commands/init.js +3 -3
- package/dist/utils/fileDialog.d.ts +9 -0
- package/dist/utils/fileDialog.js +74 -0
- package/dist/utils/fileUtils.js +3 -3
- package/dist/utils/incrementalSnapshot.d.ts +7 -0
- package/dist/utils/incrementalSnapshot.js +35 -0
- package/dist/utils/messageFormatter.js +89 -6
- package/dist/utils/sessionConverter.js +11 -0
- package/dist/utils/sessionManager.d.ts +5 -0
- package/dist/utils/sessionManager.js +45 -0
- package/dist/utils/toolDisplayConfig.d.ts +16 -0
- package/dist/utils/toolDisplayConfig.js +42 -0
- 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
|
-
|
|
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
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
-
|
|
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}`,
|
|
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,
|
|
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
|
-
|
|
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 === '
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
React.createElement(
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
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
|
-
|
|
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,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-
|
|
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
|
+
}
|
package/dist/utils/fileUtils.js
CHANGED
|
@@ -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
|
-
|
|
181
|
-
|
|
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
|
*/
|