snow-ai 0.3.15 → 0.3.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/systemPrompt.js +45 -1
- package/dist/hooks/useClipboard.js +12 -8
- package/dist/hooks/useCommandHandler.d.ts +0 -1
- package/dist/hooks/useCommandHandler.js +0 -6
- package/dist/hooks/useConversation.d.ts +7 -2
- package/dist/hooks/useConversation.js +13 -8
- package/dist/hooks/useHistoryNavigation.d.ts +10 -1
- package/dist/hooks/useHistoryNavigation.js +4 -5
- package/dist/hooks/useSnapshotState.d.ts +10 -0
- package/dist/ui/components/ChatInput.d.ts +8 -1
- package/dist/ui/components/ChatInput.js +49 -57
- package/dist/ui/components/MessageList.d.ts +0 -5
- package/dist/ui/components/MessageList.js +1 -14
- package/dist/ui/components/PendingMessages.d.ts +8 -1
- package/dist/ui/components/PendingMessages.js +14 -6
- package/dist/ui/pages/ChatScreen.js +31 -41
- package/dist/ui/pages/HeadlessModeScreen.js +2 -11
- package/dist/utils/chatExporter.js +0 -8
- package/dist/utils/commands/home.js +11 -2
- package/dist/utils/fileUtils.d.ts +1 -13
- package/dist/utils/fileUtils.js +1 -57
- package/dist/utils/textBuffer.d.ts +15 -8
- package/dist/utils/textBuffer.js +56 -38
- package/package.json +1 -1
package/dist/api/systemPrompt.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import fs from 'fs';
|
|
5
5
|
import path from 'path';
|
|
6
|
+
import os from 'os';
|
|
6
7
|
/**
|
|
7
8
|
* Get the system prompt, dynamically reading from ROLE.md if it exists
|
|
8
9
|
* This function is called to get the current system prompt with ROLE.md content if available
|
|
@@ -26,6 +27,43 @@ function getSystemPromptWithRole() {
|
|
|
26
27
|
}
|
|
27
28
|
return SYSTEM_PROMPT_TEMPLATE;
|
|
28
29
|
}
|
|
30
|
+
// Get system environment info
|
|
31
|
+
function getSystemEnvironmentInfo() {
|
|
32
|
+
const platform = (() => {
|
|
33
|
+
const platformType = os.platform();
|
|
34
|
+
switch (platformType) {
|
|
35
|
+
case 'win32':
|
|
36
|
+
return 'Windows';
|
|
37
|
+
case 'darwin':
|
|
38
|
+
return 'macOS';
|
|
39
|
+
case 'linux':
|
|
40
|
+
return 'Linux';
|
|
41
|
+
default:
|
|
42
|
+
return platformType;
|
|
43
|
+
}
|
|
44
|
+
})();
|
|
45
|
+
const shell = (() => {
|
|
46
|
+
const shellPath = process.env['SHELL'] || process.env['ComSpec'] || '';
|
|
47
|
+
const shellName = path.basename(shellPath).toLowerCase();
|
|
48
|
+
if (shellName.includes('cmd'))
|
|
49
|
+
return 'cmd.exe';
|
|
50
|
+
if (shellName.includes('powershell') || shellName.includes('pwsh'))
|
|
51
|
+
return 'PowerShell';
|
|
52
|
+
if (shellName.includes('zsh'))
|
|
53
|
+
return 'zsh';
|
|
54
|
+
if (shellName.includes('bash'))
|
|
55
|
+
return 'bash';
|
|
56
|
+
if (shellName.includes('fish'))
|
|
57
|
+
return 'fish';
|
|
58
|
+
if (shellName.includes('sh'))
|
|
59
|
+
return 'sh';
|
|
60
|
+
return shellName || 'shell';
|
|
61
|
+
})();
|
|
62
|
+
const workingDirectory = process.cwd();
|
|
63
|
+
return `Platform: ${platform}
|
|
64
|
+
Shell: ${shell}
|
|
65
|
+
Working Directory: ${workingDirectory}`;
|
|
66
|
+
}
|
|
29
67
|
const SYSTEM_PROMPT_TEMPLATE = `You are Snow AI CLI, an intelligent command-line assistant.
|
|
30
68
|
|
|
31
69
|
## 🎯 Core Principles
|
|
@@ -144,5 +182,11 @@ Guidance and recommendations:
|
|
|
144
182
|
Remember: **ACTION > ANALYSIS**. Write code first, investigate only when blocked.`;
|
|
145
183
|
// Export SYSTEM_PROMPT as a getter function for real-time ROLE.md updates
|
|
146
184
|
export function getSystemPrompt() {
|
|
147
|
-
|
|
185
|
+
const basePrompt = getSystemPromptWithRole();
|
|
186
|
+
const systemEnv = getSystemEnvironmentInfo();
|
|
187
|
+
return `${basePrompt}
|
|
188
|
+
|
|
189
|
+
## 💻 System Environment
|
|
190
|
+
|
|
191
|
+
${systemEnv}`;
|
|
148
192
|
}
|
|
@@ -9,13 +9,15 @@ export function useClipboard(buffer, updateCommandPanelState, updateFilePickerSt
|
|
|
9
9
|
// Windows: Use PowerShell to read image from clipboard
|
|
10
10
|
try {
|
|
11
11
|
const psScript = `Add-Type -AssemblyName System.Windows.Forms; Add-Type -AssemblyName System.Drawing; $clipboard = [System.Windows.Forms.Clipboard]::GetImage(); if ($clipboard -ne $null) { $ms = New-Object System.IO.MemoryStream; $clipboard.Save($ms, [System.Drawing.Imaging.ImageFormat]::Png); $bytes = $ms.ToArray(); $ms.Close(); [Convert]::ToBase64String($bytes) }`;
|
|
12
|
-
const
|
|
12
|
+
const base64Raw = execSync(`powershell -Command "${psScript}"`, {
|
|
13
13
|
encoding: 'utf-8',
|
|
14
14
|
timeout: 5000,
|
|
15
|
-
})
|
|
15
|
+
});
|
|
16
|
+
// 清理所有空白字符(包括换行符)
|
|
17
|
+
const base64 = base64Raw.replace(/\s+/g, '');
|
|
16
18
|
if (base64 && base64.length > 100) {
|
|
17
|
-
|
|
18
|
-
buffer.insertImage(
|
|
19
|
+
// 直接传入 base64 数据,不需要 data URL 前缀
|
|
20
|
+
buffer.insertImage(base64, 'image/png');
|
|
19
21
|
const text = buffer.getFullText();
|
|
20
22
|
const cursorPos = buffer.getCursorPosition();
|
|
21
23
|
updateCommandPanelState(text);
|
|
@@ -51,10 +53,12 @@ end try'`;
|
|
|
51
53
|
timeout: 3000,
|
|
52
54
|
});
|
|
53
55
|
// Read the file as base64
|
|
54
|
-
const
|
|
56
|
+
const base64Raw = execSync(`base64 -i "${tmpFile}"`, {
|
|
55
57
|
encoding: 'utf-8',
|
|
56
58
|
timeout: 2000,
|
|
57
|
-
})
|
|
59
|
+
});
|
|
60
|
+
// 清理所有空白字符(包括换行符)
|
|
61
|
+
const base64 = base64Raw.replace(/\s+/g, '');
|
|
58
62
|
// Clean up temp file
|
|
59
63
|
try {
|
|
60
64
|
execSync(`rm "${tmpFile}"`, { timeout: 1000 });
|
|
@@ -63,8 +67,8 @@ end try'`;
|
|
|
63
67
|
// Ignore cleanup errors
|
|
64
68
|
}
|
|
65
69
|
if (base64 && base64.length > 100) {
|
|
66
|
-
|
|
67
|
-
buffer.insertImage(
|
|
70
|
+
// 直接传入 base64 数据,不需要 data URL 前缀
|
|
71
|
+
buffer.insertImage(base64, 'image/png');
|
|
68
72
|
const text = buffer.getFullText();
|
|
69
73
|
const cursorPos = buffer.getCursorPosition();
|
|
70
74
|
updateCommandPanelState(text);
|
|
@@ -22,7 +22,6 @@ type CommandHandlerOptions = {
|
|
|
22
22
|
setMcpPanelKey: React.Dispatch<React.SetStateAction<number>>;
|
|
23
23
|
setYoloMode: React.Dispatch<React.SetStateAction<boolean>>;
|
|
24
24
|
setContextUsage: React.Dispatch<React.SetStateAction<UsageInfo | null>>;
|
|
25
|
-
setShouldIncludeSystemInfo: React.Dispatch<React.SetStateAction<boolean>>;
|
|
26
25
|
setVscodeConnectionStatus: React.Dispatch<React.SetStateAction<'disconnected' | 'connecting' | 'connected' | 'error'>>;
|
|
27
26
|
processMessage: (message: string, images?: Array<{
|
|
28
27
|
data: string;
|
|
@@ -125,8 +125,6 @@ export function useCommandHandler(options) {
|
|
|
125
125
|
options.clearSavedMessages();
|
|
126
126
|
options.setMessages(compressionResult.uiMessages);
|
|
127
127
|
options.setRemountKey(prev => prev + 1);
|
|
128
|
-
// Reset system info flag to include in next message
|
|
129
|
-
options.setShouldIncludeSystemInfo(true);
|
|
130
128
|
// Update token usage with compression result
|
|
131
129
|
options.setContextUsage(compressionResult.usage);
|
|
132
130
|
}
|
|
@@ -175,8 +173,6 @@ export function useCommandHandler(options) {
|
|
|
175
173
|
options.setRemountKey(prev => prev + 1);
|
|
176
174
|
// Reset context usage (token statistics)
|
|
177
175
|
options.setContextUsage(null);
|
|
178
|
-
// Reset system info flag to include in next message
|
|
179
|
-
options.setShouldIncludeSystemInfo(true);
|
|
180
176
|
// Note: yoloMode is preserved via localStorage (lines 68-76, 104-111)
|
|
181
177
|
// Note: VSCode connection is preserved and managed by vscodeConnection utility
|
|
182
178
|
// Add command execution feedback
|
|
@@ -257,8 +253,6 @@ export function useCommandHandler(options) {
|
|
|
257
253
|
options.setRemountKey(prev => prev + 1);
|
|
258
254
|
// Reset context usage (token statistics)
|
|
259
255
|
options.setContextUsage(null);
|
|
260
|
-
// Reset system info flag to include in next message
|
|
261
|
-
options.setShouldIncludeSystemInfo(true);
|
|
262
256
|
// Add command execution feedback
|
|
263
257
|
const commandMessage = {
|
|
264
258
|
role: 'command',
|
|
@@ -23,7 +23,13 @@ export type ConversationHandlerOptions = {
|
|
|
23
23
|
yoloMode: boolean;
|
|
24
24
|
setContextUsage: React.Dispatch<React.SetStateAction<any>>;
|
|
25
25
|
useBasicModel?: boolean;
|
|
26
|
-
getPendingMessages?: () =>
|
|
26
|
+
getPendingMessages?: () => Array<{
|
|
27
|
+
text: string;
|
|
28
|
+
images?: Array<{
|
|
29
|
+
data: string;
|
|
30
|
+
mimeType: string;
|
|
31
|
+
}>;
|
|
32
|
+
}>;
|
|
27
33
|
clearPendingMessages?: () => void;
|
|
28
34
|
setIsStreaming?: React.Dispatch<React.SetStateAction<boolean>>;
|
|
29
35
|
setIsReasoning?: React.Dispatch<React.SetStateAction<boolean>>;
|
|
@@ -36,7 +42,6 @@ export type ConversationHandlerOptions = {
|
|
|
36
42
|
} | null>>;
|
|
37
43
|
clearSavedMessages?: () => void;
|
|
38
44
|
setRemountKey?: React.Dispatch<React.SetStateAction<number>>;
|
|
39
|
-
setShouldIncludeSystemInfo?: React.Dispatch<React.SetStateAction<boolean>>;
|
|
40
45
|
getCurrentContextPercentage?: () => number;
|
|
41
46
|
};
|
|
42
47
|
/**
|
|
@@ -627,9 +627,6 @@ export async function handleConversationWithTools(options) {
|
|
|
627
627
|
options.setRemountKey(prev => prev + 1);
|
|
628
628
|
}
|
|
629
629
|
options.setContextUsage(compressionResult.usage);
|
|
630
|
-
if (options.setShouldIncludeSystemInfo) {
|
|
631
|
-
options.setShouldIncludeSystemInfo(true);
|
|
632
|
-
}
|
|
633
630
|
// 更新累计的usage为压缩后的usage
|
|
634
631
|
accumulatedUsage = compressionResult.usage;
|
|
635
632
|
// 压缩后需要重新构建conversationMessages
|
|
@@ -796,9 +793,6 @@ export async function handleConversationWithTools(options) {
|
|
|
796
793
|
options.setRemountKey(prev => prev + 1);
|
|
797
794
|
}
|
|
798
795
|
options.setContextUsage(compressionResult.usage);
|
|
799
|
-
if (options.setShouldIncludeSystemInfo) {
|
|
800
|
-
options.setShouldIncludeSystemInfo(true);
|
|
801
|
-
}
|
|
802
796
|
// 更新累计的usage为压缩后的usage
|
|
803
797
|
accumulatedUsage = compressionResult.usage;
|
|
804
798
|
// 压缩后需要重新构建conversationMessages
|
|
@@ -817,22 +811,33 @@ export async function handleConversationWithTools(options) {
|
|
|
817
811
|
// Clear pending messages
|
|
818
812
|
options.clearPendingMessages();
|
|
819
813
|
// Combine multiple pending messages into one
|
|
820
|
-
const combinedMessage = pendingMessages.join('\n\n');
|
|
814
|
+
const combinedMessage = pendingMessages.map(m => m.text).join('\n\n');
|
|
815
|
+
// Collect all images from pending messages
|
|
816
|
+
const allPendingImages = pendingMessages
|
|
817
|
+
.flatMap(m => m.images || [])
|
|
818
|
+
.map(img => ({
|
|
819
|
+
type: 'image',
|
|
820
|
+
data: img.data,
|
|
821
|
+
mimeType: img.mimeType,
|
|
822
|
+
}));
|
|
821
823
|
// Add user message to UI
|
|
822
824
|
const userMessage = {
|
|
823
825
|
role: 'user',
|
|
824
826
|
content: combinedMessage,
|
|
827
|
+
images: allPendingImages.length > 0 ? allPendingImages : undefined,
|
|
825
828
|
};
|
|
826
829
|
setMessages(prev => [...prev, userMessage]);
|
|
827
|
-
// Add user message to conversation history
|
|
830
|
+
// Add user message to conversation history (using images field for image data)
|
|
828
831
|
conversationMessages.push({
|
|
829
832
|
role: 'user',
|
|
830
833
|
content: combinedMessage,
|
|
834
|
+
images: allPendingImages.length > 0 ? allPendingImages : undefined,
|
|
831
835
|
});
|
|
832
836
|
// Save user message
|
|
833
837
|
saveMessage({
|
|
834
838
|
role: 'user',
|
|
835
839
|
content: combinedMessage,
|
|
840
|
+
images: allPendingImages.length > 0 ? allPendingImages : undefined,
|
|
836
841
|
}).catch(error => {
|
|
837
842
|
console.error('Failed to save pending user message:', error);
|
|
838
843
|
});
|
|
@@ -2,8 +2,17 @@ import { TextBuffer } from '../utils/textBuffer.js';
|
|
|
2
2
|
type ChatMessage = {
|
|
3
3
|
role: string;
|
|
4
4
|
content: string;
|
|
5
|
+
images?: Array<{
|
|
6
|
+
type: 'image';
|
|
7
|
+
data: string;
|
|
8
|
+
mimeType: string;
|
|
9
|
+
}>;
|
|
5
10
|
};
|
|
6
|
-
export declare function useHistoryNavigation(buffer: TextBuffer, triggerUpdate: () => void, chatHistory: ChatMessage[], onHistorySelect?: (selectedIndex: number, message: string
|
|
11
|
+
export declare function useHistoryNavigation(buffer: TextBuffer, triggerUpdate: () => void, chatHistory: ChatMessage[], onHistorySelect?: (selectedIndex: number, message: string, images?: Array<{
|
|
12
|
+
type: 'image';
|
|
13
|
+
data: string;
|
|
14
|
+
mimeType: string;
|
|
15
|
+
}>) => void): {
|
|
7
16
|
showHistoryMenu: boolean;
|
|
8
17
|
setShowHistoryMenu: import("react").Dispatch<import("react").SetStateAction<boolean>>;
|
|
9
18
|
historySelectedIndex: number;
|
|
@@ -45,13 +45,12 @@ export function useHistoryNavigation(buffer, triggerUpdate, chatHistory, onHisto
|
|
|
45
45
|
const selectedIndex = parseInt(value, 10);
|
|
46
46
|
const selectedMessage = chatHistory[selectedIndex];
|
|
47
47
|
if (selectedMessage && onHistorySelect) {
|
|
48
|
-
//
|
|
49
|
-
|
|
48
|
+
// Don't modify buffer here - let ChatInput handle everything via initialContent
|
|
49
|
+
// This prevents duplicate image placeholders
|
|
50
50
|
setShowHistoryMenu(false);
|
|
51
|
-
|
|
52
|
-
onHistorySelect(selectedIndex, selectedMessage.content);
|
|
51
|
+
onHistorySelect(selectedIndex, selectedMessage.content, selectedMessage.images);
|
|
53
52
|
}
|
|
54
|
-
}, [chatHistory, onHistorySelect
|
|
53
|
+
}, [chatHistory, onHistorySelect]);
|
|
55
54
|
// Terminal-style history navigation: navigate up (older)
|
|
56
55
|
const navigateHistoryUp = useCallback(() => {
|
|
57
56
|
const history = persistentHistoryRef.current;
|
|
@@ -6,11 +6,21 @@ export declare function useSnapshotState(messagesLength: number): {
|
|
|
6
6
|
fileCount: number;
|
|
7
7
|
filePaths?: string[];
|
|
8
8
|
message?: string;
|
|
9
|
+
images?: Array<{
|
|
10
|
+
type: "image";
|
|
11
|
+
data: string;
|
|
12
|
+
mimeType: string;
|
|
13
|
+
}>;
|
|
9
14
|
} | null;
|
|
10
15
|
setPendingRollback: import("react").Dispatch<import("react").SetStateAction<{
|
|
11
16
|
messageIndex: number;
|
|
12
17
|
fileCount: number;
|
|
13
18
|
filePaths?: string[];
|
|
14
19
|
message?: string;
|
|
20
|
+
images?: Array<{
|
|
21
|
+
type: "image";
|
|
22
|
+
data: string;
|
|
23
|
+
mimeType: string;
|
|
24
|
+
}>;
|
|
15
25
|
} | null>>;
|
|
16
26
|
};
|
|
@@ -32,7 +32,14 @@ type Props = {
|
|
|
32
32
|
cacheReadTokens?: number;
|
|
33
33
|
cachedTokens?: number;
|
|
34
34
|
};
|
|
35
|
-
initialContent?:
|
|
35
|
+
initialContent?: {
|
|
36
|
+
text: string;
|
|
37
|
+
images?: Array<{
|
|
38
|
+
type: 'image';
|
|
39
|
+
data: string;
|
|
40
|
+
mimeType: string;
|
|
41
|
+
}>;
|
|
42
|
+
} | null;
|
|
36
43
|
onContextPercentageChange?: (percentage: number) => void;
|
|
37
44
|
};
|
|
38
45
|
export default function ChatInput({ onSubmit, onCommand, placeholder, disabled, isProcessing, chatHistory, onHistorySelect, yoloMode, contextUsage, initialContent, onContextPercentageChange, }: Props): React.JSX.Element;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React, { useCallback, useEffect, useRef } from 'react';
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
|
-
import { cpSlice
|
|
3
|
+
import { cpSlice } from '../../utils/textUtils.js';
|
|
4
4
|
import CommandPanel from './CommandPanel.js';
|
|
5
5
|
import FileList from './FileList.js';
|
|
6
6
|
import { useInputBuffer } from '../../hooks/useInputBuffer.js';
|
|
@@ -97,7 +97,45 @@ export default function ChatInput({ onSubmit, onCommand, placeholder = 'Type you
|
|
|
97
97
|
// Set initial content when provided (e.g., when rolling back to first message)
|
|
98
98
|
useEffect(() => {
|
|
99
99
|
if (initialContent) {
|
|
100
|
-
|
|
100
|
+
// Always do full restore to avoid duplicate placeholders
|
|
101
|
+
buffer.setText('');
|
|
102
|
+
const text = initialContent.text;
|
|
103
|
+
const images = initialContent.images || [];
|
|
104
|
+
if (images.length === 0) {
|
|
105
|
+
// No images, just set the text
|
|
106
|
+
if (text) {
|
|
107
|
+
buffer.insert(text);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
// Split text by image placeholders and reconstruct with actual images
|
|
112
|
+
// Placeholder format: [image #N]
|
|
113
|
+
const imagePlaceholderPattern = /\[image #\d+\]/g;
|
|
114
|
+
const parts = text.split(imagePlaceholderPattern);
|
|
115
|
+
// Interleave text parts with images
|
|
116
|
+
for (let i = 0; i < parts.length; i++) {
|
|
117
|
+
// Insert text part
|
|
118
|
+
const part = parts[i];
|
|
119
|
+
if (part) {
|
|
120
|
+
buffer.insert(part);
|
|
121
|
+
}
|
|
122
|
+
// Insert image after this text part (if exists)
|
|
123
|
+
if (i < images.length) {
|
|
124
|
+
const img = images[i];
|
|
125
|
+
if (img) {
|
|
126
|
+
// Extract base64 data from data URL if present
|
|
127
|
+
let base64Data = img.data;
|
|
128
|
+
if (base64Data.startsWith('data:')) {
|
|
129
|
+
const base64Index = base64Data.indexOf('base64,');
|
|
130
|
+
if (base64Index !== -1) {
|
|
131
|
+
base64Data = base64Data.substring(base64Index + 7);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
buffer.insertImage(base64Data, img.mimeType);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
101
139
|
triggerUpdate();
|
|
102
140
|
}
|
|
103
141
|
// Only run when initialContent changes
|
|
@@ -143,71 +181,25 @@ export default function ChatInput({ onSubmit, onCommand, placeholder = 'Type you
|
|
|
143
181
|
return React.createElement(Text, null, char);
|
|
144
182
|
}
|
|
145
183
|
}, [hasFocus]);
|
|
146
|
-
// Render content with cursor
|
|
184
|
+
// Render content with cursor (treat all text including placeholders as plain text)
|
|
147
185
|
const renderContent = useCallback(() => {
|
|
148
186
|
if (buffer.text.length > 0) {
|
|
149
|
-
// 使用buffer
|
|
187
|
+
// 使用buffer的内部文本,将占位符当作普通文本处理
|
|
150
188
|
const displayText = buffer.text;
|
|
151
189
|
const cursorPos = buffer.getCursorPosition();
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
if (hasPastePlaceholder || hasImagePlaceholder || isFocusNoise) {
|
|
159
|
-
const atCursor = (() => {
|
|
160
|
-
const charInfo = buffer.getCharAtCursor();
|
|
161
|
-
return charInfo.char === '\n' ? ' ' : charInfo.char;
|
|
162
|
-
})();
|
|
163
|
-
// 分割文本并高亮占位符(粘贴和图片)
|
|
164
|
-
const parts = displayText.split(/(\[Paste \d+ characters #\d+\]|\[image #\d+\])/);
|
|
165
|
-
let processedLength = 0;
|
|
166
|
-
let cursorRendered = false;
|
|
167
|
-
const elements = parts.map((part, partIndex) => {
|
|
168
|
-
const isPastePlaceholder = part.match(/^\[Paste \d+ characters #\d+\]$/);
|
|
169
|
-
const isImagePlaceholder = part.match(/^\[image #\d+\]$/);
|
|
170
|
-
const isPlaceholder = isPastePlaceholder || isImagePlaceholder;
|
|
171
|
-
const partStart = processedLength;
|
|
172
|
-
const partEnd = processedLength + cpLen(part);
|
|
173
|
-
processedLength = partEnd;
|
|
174
|
-
// 检查光标是否在这个部分
|
|
175
|
-
if (cursorPos >= partStart && cursorPos < partEnd) {
|
|
176
|
-
cursorRendered = true;
|
|
177
|
-
const beforeCursorInPart = cpSlice(part, 0, cursorPos - partStart);
|
|
178
|
-
const afterCursorInPart = cpSlice(part, cursorPos - partStart + 1);
|
|
179
|
-
return (React.createElement(React.Fragment, { key: partIndex }, isPlaceholder ? (React.createElement(Text, { color: isImagePlaceholder ? 'magenta' : 'cyan', dimColor: true },
|
|
180
|
-
beforeCursorInPart,
|
|
181
|
-
renderCursor(atCursor),
|
|
182
|
-
afterCursorInPart)) : (React.createElement(React.Fragment, null,
|
|
183
|
-
beforeCursorInPart,
|
|
184
|
-
renderCursor(atCursor),
|
|
185
|
-
afterCursorInPart))));
|
|
186
|
-
}
|
|
187
|
-
else {
|
|
188
|
-
return isPlaceholder ? (React.createElement(Text, { key: partIndex, color: isImagePlaceholder ? 'magenta' : 'cyan', dimColor: true }, part)) : (React.createElement(Text, { key: partIndex }, part));
|
|
189
|
-
}
|
|
190
|
-
});
|
|
191
|
-
return (React.createElement(Text, null,
|
|
192
|
-
elements,
|
|
193
|
-
!cursorRendered && renderCursor(' ')));
|
|
194
|
-
}
|
|
195
|
-
else {
|
|
196
|
-
// 普通文本渲染
|
|
197
|
-
const charInfo = buffer.getCharAtCursor();
|
|
198
|
-
const atCursor = charInfo.char === '\n' ? ' ' : charInfo.char;
|
|
199
|
-
return (React.createElement(Text, null,
|
|
200
|
-
cpSlice(displayText, 0, cursorPos),
|
|
201
|
-
renderCursor(atCursor),
|
|
202
|
-
cpSlice(displayText, cursorPos + 1)));
|
|
203
|
-
}
|
|
190
|
+
const charInfo = buffer.getCharAtCursor();
|
|
191
|
+
const atCursor = charInfo.char === '\n' ? ' ' : charInfo.char;
|
|
192
|
+
return (React.createElement(Text, null,
|
|
193
|
+
cpSlice(displayText, 0, cursorPos),
|
|
194
|
+
renderCursor(atCursor),
|
|
195
|
+
cpSlice(displayText, cursorPos + 1)));
|
|
204
196
|
}
|
|
205
197
|
else {
|
|
206
198
|
return (React.createElement(React.Fragment, null,
|
|
207
199
|
renderCursor(' '),
|
|
208
200
|
React.createElement(Text, { color: disabled ? 'darkGray' : 'gray', dimColor: true }, disabled ? 'Waiting for response...' : placeholder)));
|
|
209
201
|
}
|
|
210
|
-
}, [buffer, disabled, placeholder, renderCursor]);
|
|
202
|
+
}, [buffer, disabled, placeholder, renderCursor, buffer.text]);
|
|
211
203
|
return (React.createElement(Box, { flexDirection: "column", paddingX: 1, width: terminalWidth },
|
|
212
204
|
showHistoryMenu && (React.createElement(Box, { flexDirection: "column", marginBottom: 1, width: terminalWidth - 2 },
|
|
213
205
|
React.createElement(Box, { flexDirection: "column" }, (() => {
|
|
@@ -34,21 +34,8 @@ const MessageList = memo(({ messages, animationFrame, maxMessages = 6 }) => {
|
|
|
34
34
|
React.createElement(Box, { marginLeft: 2 },
|
|
35
35
|
React.createElement(Text, { color: "gray" }, message.content || ' ')))) : (React.createElement(React.Fragment, null,
|
|
36
36
|
message.role === 'user' ? (React.createElement(Text, { color: "gray" }, message.content || ' ')) : (React.createElement(MarkdownRenderer, { content: message.content || ' ' })),
|
|
37
|
-
(message.
|
|
38
|
-
message.files ||
|
|
39
|
-
message.files ||
|
|
37
|
+
(message.files ||
|
|
40
38
|
message.images) && (React.createElement(Box, { flexDirection: "column" },
|
|
41
|
-
message.systemInfo && (React.createElement(React.Fragment, null,
|
|
42
|
-
React.createElement(Text, { color: "gray", dimColor: true },
|
|
43
|
-
"\u2514\u2500 Platform: ",
|
|
44
|
-
message.systemInfo.platform),
|
|
45
|
-
React.createElement(Text, { color: "gray", dimColor: true },
|
|
46
|
-
"\u2514\u2500 Shell: ",
|
|
47
|
-
message.systemInfo.shell),
|
|
48
|
-
React.createElement(Text, { color: "gray", dimColor: true },
|
|
49
|
-
"\u2514\u2500 Working Directory:",
|
|
50
|
-
' ',
|
|
51
|
-
message.systemInfo.workingDirectory))),
|
|
52
39
|
message.files && message.files.length > 0 && (React.createElement(React.Fragment, null, message.files.map((file, fileIndex) => (React.createElement(Text, { key: fileIndex, color: "gray", dimColor: true }, file.isImage
|
|
53
40
|
? `└─ [image #{fileIndex + 1}] ${file.path}`
|
|
54
41
|
: `└─ Read \`${file.path}\`${file.exists
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
interface PendingMessage {
|
|
3
|
+
text: string;
|
|
4
|
+
images?: Array<{
|
|
5
|
+
data: string;
|
|
6
|
+
mimeType: string;
|
|
7
|
+
}>;
|
|
8
|
+
}
|
|
2
9
|
interface Props {
|
|
3
|
-
pendingMessages:
|
|
10
|
+
pendingMessages: PendingMessage[];
|
|
4
11
|
}
|
|
5
12
|
export default function PendingMessages({ pendingMessages }: Props): React.JSX.Element | null;
|
|
6
13
|
export {};
|
|
@@ -9,11 +9,19 @@ export default function PendingMessages({ pendingMessages }) {
|
|
|
9
9
|
"\u2B11 Pending Messages (",
|
|
10
10
|
pendingMessages.length,
|
|
11
11
|
")"),
|
|
12
|
-
pendingMessages.map((message, index) => (React.createElement(Box, { key: index, marginLeft: 1, marginY: 0 },
|
|
13
|
-
React.createElement(
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
React.createElement(
|
|
12
|
+
pendingMessages.map((message, index) => (React.createElement(Box, { key: index, marginLeft: 1, marginY: 0, flexDirection: "column" },
|
|
13
|
+
React.createElement(Box, null,
|
|
14
|
+
React.createElement(Text, { color: "blue", bold: true },
|
|
15
|
+
index + 1,
|
|
16
|
+
"."),
|
|
17
|
+
React.createElement(Box, { marginLeft: 1 },
|
|
18
|
+
React.createElement(Text, { color: "gray" }, message.text.length > 60 ? `${message.text.substring(0, 60)}...` : message.text))),
|
|
19
|
+
message.images && message.images.length > 0 && (React.createElement(Box, { marginLeft: 3 },
|
|
20
|
+
React.createElement(Text, { color: "gray", dimColor: true },
|
|
21
|
+
"\u2514\u2500 ",
|
|
22
|
+
message.images.length,
|
|
23
|
+
" image",
|
|
24
|
+
message.images.length > 1 ? 's' : '',
|
|
25
|
+
" attached")))))),
|
|
18
26
|
React.createElement(Text, { color: "yellow", dimColor: true }, "Will be sent after tool execution completes")));
|
|
19
27
|
}
|
|
@@ -26,7 +26,7 @@ import { useSnapshotState } from '../../hooks/useSnapshotState.js';
|
|
|
26
26
|
import { useStreamingState } from '../../hooks/useStreamingState.js';
|
|
27
27
|
import { useCommandHandler } from '../../hooks/useCommandHandler.js';
|
|
28
28
|
import { useTerminalSize } from '../../hooks/useTerminalSize.js';
|
|
29
|
-
import { parseAndValidateFileReferences, createMessageWithFileInstructions,
|
|
29
|
+
import { parseAndValidateFileReferences, createMessageWithFileInstructions, } from '../../utils/fileUtils.js';
|
|
30
30
|
import { executeCommand } from '../../utils/commandExecutor.js';
|
|
31
31
|
import { convertSessionMessagesToUI } from '../../utils/sessionConverter.js';
|
|
32
32
|
import { incrementalSnapshotManager } from '../../utils/incrementalSnapshot.js';
|
|
@@ -77,7 +77,6 @@ export default function ChatScreen({ skipWelcome }) {
|
|
|
77
77
|
const [showSessionPanel, setShowSessionPanel] = useState(false);
|
|
78
78
|
const [showMcpPanel, setShowMcpPanel] = useState(false);
|
|
79
79
|
const [showUsagePanel, setShowUsagePanel] = useState(false);
|
|
80
|
-
const [shouldIncludeSystemInfo, setShouldIncludeSystemInfo] = useState(true); // Include on first message
|
|
81
80
|
const [restoreInputContent, setRestoreInputContent] = useState(null);
|
|
82
81
|
const { columns: terminalWidth, rows: terminalHeight } = useTerminalSize();
|
|
83
82
|
const { stdout } = useStdout();
|
|
@@ -192,7 +191,6 @@ export default function ChatScreen({ skipWelcome }) {
|
|
|
192
191
|
setMcpPanelKey,
|
|
193
192
|
setYoloMode,
|
|
194
193
|
setContextUsage: streamingState.setContextUsage,
|
|
195
|
-
setShouldIncludeSystemInfo,
|
|
196
194
|
setVscodeConnectionStatus: vscodeState.setVscodeConnectionStatus,
|
|
197
195
|
processMessage: (message, images, useBasicModel, hideUserMessage) => processMessageRef.current?.(message, images, useBasicModel, hideUserMessage) || Promise.resolve(),
|
|
198
196
|
});
|
|
@@ -279,7 +277,7 @@ export default function ChatScreen({ skipWelcome }) {
|
|
|
279
277
|
// Note: session cleanup will be handled in processMessage/processPendingMessages finally block
|
|
280
278
|
}
|
|
281
279
|
});
|
|
282
|
-
const handleHistorySelect = async (selectedIndex, message) => {
|
|
280
|
+
const handleHistorySelect = async (selectedIndex, message, images) => {
|
|
283
281
|
// Count total files that will be rolled back (from selectedIndex onwards)
|
|
284
282
|
let totalFileCount = 0;
|
|
285
283
|
for (const [index, count] of snapshotState.snapshotFileCount.entries()) {
|
|
@@ -299,11 +297,16 @@ export default function ChatScreen({ skipWelcome }) {
|
|
|
299
297
|
fileCount: filePaths.length, // Use actual unique file count
|
|
300
298
|
filePaths,
|
|
301
299
|
message, // Save message for restore after rollback
|
|
300
|
+
images, // Save images for restore after rollback
|
|
302
301
|
});
|
|
303
302
|
}
|
|
304
303
|
else {
|
|
305
304
|
// No files to rollback, just rollback conversation
|
|
306
|
-
//
|
|
305
|
+
// Restore message to input buffer (with or without images)
|
|
306
|
+
setRestoreInputContent({
|
|
307
|
+
text: message,
|
|
308
|
+
images: images,
|
|
309
|
+
});
|
|
307
310
|
await performRollback(selectedIndex, false);
|
|
308
311
|
}
|
|
309
312
|
};
|
|
@@ -406,9 +409,12 @@ export default function ChatScreen({ skipWelcome }) {
|
|
|
406
409
|
return;
|
|
407
410
|
}
|
|
408
411
|
if (snapshotState.pendingRollback) {
|
|
409
|
-
// Restore message to input before rollback
|
|
412
|
+
// Restore message and images to input before rollback
|
|
410
413
|
if (snapshotState.pendingRollback.message) {
|
|
411
|
-
setRestoreInputContent(
|
|
414
|
+
setRestoreInputContent({
|
|
415
|
+
text: snapshotState.pendingRollback.message,
|
|
416
|
+
images: snapshotState.pendingRollback.images,
|
|
417
|
+
});
|
|
412
418
|
}
|
|
413
419
|
await performRollback(snapshotState.pendingRollback.messageIndex, rollbackFiles);
|
|
414
420
|
}
|
|
@@ -441,7 +447,7 @@ export default function ChatScreen({ skipWelcome }) {
|
|
|
441
447
|
const handleMessageSubmit = async (message, images) => {
|
|
442
448
|
// If streaming, add to pending messages instead of sending immediately
|
|
443
449
|
if (streamingState.isStreaming) {
|
|
444
|
-
setPendingMessages(prev => [...prev, message]);
|
|
450
|
+
setPendingMessages(prev => [...prev, { text: message, images }]);
|
|
445
451
|
return;
|
|
446
452
|
}
|
|
447
453
|
// Create checkpoint (lightweight, only tracks modifications)
|
|
@@ -476,7 +482,6 @@ export default function ChatScreen({ skipWelcome }) {
|
|
|
476
482
|
setMessages(compressionResult.uiMessages);
|
|
477
483
|
setRemountKey(prev => prev + 1);
|
|
478
484
|
streamingState.setContextUsage(compressionResult.usage);
|
|
479
|
-
setShouldIncludeSystemInfo(true);
|
|
480
485
|
}
|
|
481
486
|
else {
|
|
482
487
|
throw new Error('Compression failed');
|
|
@@ -518,8 +523,6 @@ export default function ChatScreen({ skipWelcome }) {
|
|
|
518
523
|
mimeType: f.mimeType,
|
|
519
524
|
})),
|
|
520
525
|
];
|
|
521
|
-
// Get system information only if needed
|
|
522
|
-
const systemInfo = shouldIncludeSystemInfo ? getSystemInfo() : undefined;
|
|
523
526
|
// Only add user message to UI if not hidden
|
|
524
527
|
if (!hideUserMessage) {
|
|
525
528
|
const userMessage = {
|
|
@@ -527,21 +530,16 @@ export default function ChatScreen({ skipWelcome }) {
|
|
|
527
530
|
content: cleanContent,
|
|
528
531
|
files: validFiles.length > 0 ? validFiles : undefined,
|
|
529
532
|
images: imageContents.length > 0 ? imageContents : undefined,
|
|
530
|
-
systemInfo,
|
|
531
533
|
};
|
|
532
534
|
setMessages(prev => [...prev, userMessage]);
|
|
533
|
-
// After including system info once, don't include it again
|
|
534
|
-
if (shouldIncludeSystemInfo) {
|
|
535
|
-
setShouldIncludeSystemInfo(false);
|
|
536
|
-
}
|
|
537
535
|
}
|
|
538
536
|
streamingState.setIsStreaming(true);
|
|
539
537
|
// Create new abort controller for this request
|
|
540
538
|
const controller = new AbortController();
|
|
541
539
|
streamingState.setAbortController(controller);
|
|
542
540
|
try {
|
|
543
|
-
// Create message for AI with file read instructions
|
|
544
|
-
const messageForAI = createMessageWithFileInstructions(cleanContent, regularFiles,
|
|
541
|
+
// Create message for AI with file read instructions and editor context
|
|
542
|
+
const messageForAI = createMessageWithFileInstructions(cleanContent, regularFiles, vscodeState.vscodeConnected ? vscodeState.editorContext : undefined);
|
|
545
543
|
// Start conversation with tool support
|
|
546
544
|
await handleConversationWithTools({
|
|
547
545
|
userContent: messageForAI,
|
|
@@ -565,7 +563,6 @@ export default function ChatScreen({ skipWelcome }) {
|
|
|
565
563
|
setRetryStatus: streamingState.setRetryStatus,
|
|
566
564
|
clearSavedMessages,
|
|
567
565
|
setRemountKey,
|
|
568
|
-
setShouldIncludeSystemInfo,
|
|
569
566
|
getCurrentContextPercentage: () => currentContextPercentageRef.current,
|
|
570
567
|
});
|
|
571
568
|
}
|
|
@@ -674,22 +671,27 @@ export default function ChatScreen({ skipWelcome }) {
|
|
|
674
671
|
const messagesToProcess = [...pendingMessages];
|
|
675
672
|
setPendingMessages([]);
|
|
676
673
|
// Combine multiple pending messages into one
|
|
677
|
-
const combinedMessage = messagesToProcess.join('\n\n');
|
|
674
|
+
const combinedMessage = messagesToProcess.map(m => m.text).join('\n\n');
|
|
678
675
|
// Parse and validate file references (same as processMessage)
|
|
679
676
|
const { cleanContent, validFiles } = await parseAndValidateFileReferences(combinedMessage);
|
|
680
677
|
// Separate image files from regular files
|
|
681
678
|
const imageFiles = validFiles.filter(f => f.isImage && f.imageData && f.mimeType);
|
|
682
679
|
const regularFiles = validFiles.filter(f => !f.isImage);
|
|
683
|
-
//
|
|
684
|
-
const
|
|
685
|
-
|
|
680
|
+
// Collect all images from pending messages
|
|
681
|
+
const allImages = messagesToProcess
|
|
682
|
+
.flatMap(m => m.images || [])
|
|
683
|
+
.concat(imageFiles.map(f => ({
|
|
684
|
+
data: f.imageData,
|
|
685
|
+
mimeType: f.mimeType,
|
|
686
|
+
})));
|
|
687
|
+
// Convert to image content format
|
|
688
|
+
const imageContents = allImages.length > 0
|
|
689
|
+
? allImages.map(img => ({
|
|
686
690
|
type: 'image',
|
|
687
|
-
data:
|
|
688
|
-
mimeType:
|
|
691
|
+
data: img.data,
|
|
692
|
+
mimeType: img.mimeType,
|
|
689
693
|
}))
|
|
690
694
|
: undefined;
|
|
691
|
-
// Get system information (not needed for pending messages - they are follow-ups)
|
|
692
|
-
const systemInfo = undefined;
|
|
693
695
|
// Add user message to chat with file references and images
|
|
694
696
|
const userMessage = {
|
|
695
697
|
role: 'user',
|
|
@@ -704,8 +706,8 @@ export default function ChatScreen({ skipWelcome }) {
|
|
|
704
706
|
const controller = new AbortController();
|
|
705
707
|
streamingState.setAbortController(controller);
|
|
706
708
|
try {
|
|
707
|
-
// Create message for AI with file read instructions
|
|
708
|
-
const messageForAI = createMessageWithFileInstructions(cleanContent, regularFiles,
|
|
709
|
+
// Create message for AI with file read instructions and editor context
|
|
710
|
+
const messageForAI = createMessageWithFileInstructions(cleanContent, regularFiles, vscodeState.vscodeConnected ? vscodeState.editorContext : undefined);
|
|
709
711
|
// Use the same conversation handler
|
|
710
712
|
await handleConversationWithTools({
|
|
711
713
|
userContent: messageForAI,
|
|
@@ -729,7 +731,6 @@ export default function ChatScreen({ skipWelcome }) {
|
|
|
729
731
|
clearSavedMessages,
|
|
730
732
|
setRemountKey,
|
|
731
733
|
getCurrentContextPercentage: () => currentContextPercentageRef.current,
|
|
732
|
-
setShouldIncludeSystemInfo,
|
|
733
734
|
});
|
|
734
735
|
}
|
|
735
736
|
catch (error) {
|
|
@@ -962,17 +963,6 @@ export default function ChatScreen({ skipWelcome }) {
|
|
|
962
963
|
.replace('✓ ', '')
|
|
963
964
|
.replace(/.*⚇✓\s*/, '')
|
|
964
965
|
.split('\n')[0] || '', result: message.toolResult, maxLines: 5 })),
|
|
965
|
-
message.role === 'user' && message.systemInfo && (React.createElement(Box, { marginTop: 1, flexDirection: "column" },
|
|
966
|
-
React.createElement(Text, { color: "gray", dimColor: true },
|
|
967
|
-
"\u2514\u2500 Platform: ",
|
|
968
|
-
message.systemInfo.platform),
|
|
969
|
-
React.createElement(Text, { color: "gray", dimColor: true },
|
|
970
|
-
"\u2514\u2500 Shell: ",
|
|
971
|
-
message.systemInfo.shell),
|
|
972
|
-
React.createElement(Text, { color: "gray", dimColor: true },
|
|
973
|
-
"\u2514\u2500 Working Directory:",
|
|
974
|
-
' ',
|
|
975
|
-
message.systemInfo.workingDirectory))),
|
|
976
966
|
message.files && message.files.length > 0 && (React.createElement(Box, { flexDirection: "column" }, message.files.map((file, fileIndex) => (React.createElement(Text, { key: fileIndex, color: "gray", dimColor: true },
|
|
977
967
|
"\u2514\u2500 ",
|
|
978
968
|
file.path,
|
|
@@ -7,7 +7,7 @@ import { useStreamingState } from '../../hooks/useStreamingState.js';
|
|
|
7
7
|
import { useToolConfirmation } from '../../hooks/useToolConfirmation.js';
|
|
8
8
|
import { useVSCodeState } from '../../hooks/useVSCodeState.js';
|
|
9
9
|
import { useSessionSave } from '../../hooks/useSessionSave.js';
|
|
10
|
-
import { parseAndValidateFileReferences, createMessageWithFileInstructions,
|
|
10
|
+
import { parseAndValidateFileReferences, createMessageWithFileInstructions, } from '../../utils/fileUtils.js';
|
|
11
11
|
// Console-based markdown renderer functions
|
|
12
12
|
function renderConsoleMarkdown(content) {
|
|
13
13
|
const blocks = parseConsoleMarkdown(content);
|
|
@@ -297,14 +297,11 @@ export default function HeadlessModeScreen({ prompt, onComplete }) {
|
|
|
297
297
|
// Parse and validate file references
|
|
298
298
|
const { cleanContent, validFiles } = await parseAndValidateFileReferences(prompt);
|
|
299
299
|
const regularFiles = validFiles.filter(f => !f.isImage);
|
|
300
|
-
// Get system information
|
|
301
|
-
const systemInfo = getSystemInfo();
|
|
302
300
|
// Add user message to UI
|
|
303
301
|
const userMessage = {
|
|
304
302
|
role: 'user',
|
|
305
303
|
content: cleanContent,
|
|
306
304
|
files: validFiles.length > 0 ? validFiles : undefined,
|
|
307
|
-
systemInfo,
|
|
308
305
|
};
|
|
309
306
|
setMessages([userMessage]);
|
|
310
307
|
streamingState.setIsStreaming(true);
|
|
@@ -330,15 +327,9 @@ export default function HeadlessModeScreen({ prompt, onComplete }) {
|
|
|
330
327
|
: '\x1b[31m (not found)\x1b[90m'}\x1b[0m`);
|
|
331
328
|
});
|
|
332
329
|
}
|
|
333
|
-
if (systemInfo) {
|
|
334
|
-
console.log(`\x1b[36m├─ System Context\x1b[0m`);
|
|
335
|
-
console.log(`\x1b[90m│ └─ Platform: \x1b[33m${systemInfo.platform}\x1b[0m`);
|
|
336
|
-
console.log(`\x1b[90m│ └─ Shell: \x1b[33m${systemInfo.shell}\x1b[0m`);
|
|
337
|
-
console.log(`\x1b[90m│ └─ Working Directory: \x1b[33m${systemInfo.workingDirectory}\x1b[0m`);
|
|
338
|
-
}
|
|
339
330
|
console.log(`\x1b[36m└─ Assistant Response\x1b[0m`);
|
|
340
331
|
// Create message for AI
|
|
341
|
-
const messageForAI = createMessageWithFileInstructions(cleanContent, regularFiles,
|
|
332
|
+
const messageForAI = createMessageWithFileInstructions(cleanContent, regularFiles, vscodeState.vscodeConnected ? vscodeState.editorContext : undefined);
|
|
342
333
|
// Start conversation with tool support
|
|
343
334
|
await handleConversationWithTools({
|
|
344
335
|
userContent: messageForAI,
|
|
@@ -92,14 +92,6 @@ export function formatMessagesAsText(messages) {
|
|
|
92
92
|
lines.push('');
|
|
93
93
|
lines.push(`[${message.images.length} image(s) attached]`);
|
|
94
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
95
|
// Add files information if present
|
|
104
96
|
if (message.files && message.files.length > 0) {
|
|
105
97
|
lines.push('');
|
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
import { registerCommand } from '../commandExecutor.js';
|
|
2
|
+
import { resetAnthropicClient } from '../../api/anthropic.js';
|
|
3
|
+
import { resetGeminiClient } from '../../api/gemini.js';
|
|
4
|
+
import { resetOpenAIClient as resetChatClient } from '../../api/chat.js';
|
|
5
|
+
import { resetOpenAIClient as resetResponseClient } from '../../api/responses.js';
|
|
2
6
|
// Home command handler - returns to welcome screen
|
|
3
7
|
registerCommand('home', {
|
|
4
8
|
execute: () => {
|
|
9
|
+
// Clear all API configuration caches
|
|
10
|
+
resetAnthropicClient();
|
|
11
|
+
resetGeminiClient();
|
|
12
|
+
resetChatClient();
|
|
13
|
+
resetResponseClient();
|
|
5
14
|
return {
|
|
6
15
|
success: true,
|
|
7
16
|
action: 'home',
|
|
8
|
-
message: 'Returning to welcome screen'
|
|
17
|
+
message: 'Returning to welcome screen',
|
|
9
18
|
};
|
|
10
|
-
}
|
|
19
|
+
},
|
|
11
20
|
});
|
|
12
21
|
export default {};
|
|
@@ -29,11 +29,7 @@ export declare function parseAndValidateFileReferences(content: string): Promise
|
|
|
29
29
|
/**
|
|
30
30
|
* Create message with file read instructions for AI
|
|
31
31
|
*/
|
|
32
|
-
export declare function createMessageWithFileInstructions(content: string, files: SelectedFile[],
|
|
33
|
-
platform: string;
|
|
34
|
-
shell: string;
|
|
35
|
-
workingDirectory: string;
|
|
36
|
-
}, editorContext?: {
|
|
32
|
+
export declare function createMessageWithFileInstructions(content: string, files: SelectedFile[], editorContext?: {
|
|
37
33
|
activeFile?: string;
|
|
38
34
|
selectedText?: string;
|
|
39
35
|
cursorPosition?: {
|
|
@@ -42,11 +38,3 @@ export declare function createMessageWithFileInstructions(content: string, files
|
|
|
42
38
|
};
|
|
43
39
|
workspaceFolder?: string;
|
|
44
40
|
}): string;
|
|
45
|
-
/**
|
|
46
|
-
* Get system information (OS, shell, working directory)
|
|
47
|
-
*/
|
|
48
|
-
export declare function getSystemInfo(): {
|
|
49
|
-
platform: string;
|
|
50
|
-
shell: string;
|
|
51
|
-
workingDirectory: string;
|
|
52
|
-
};
|
package/dist/utils/fileUtils.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import os from 'os';
|
|
4
3
|
/**
|
|
5
4
|
* Get line count for a file
|
|
6
5
|
*/
|
|
@@ -154,17 +153,8 @@ export async function parseAndValidateFileReferences(content) {
|
|
|
154
153
|
/**
|
|
155
154
|
* Create message with file read instructions for AI
|
|
156
155
|
*/
|
|
157
|
-
export function createMessageWithFileInstructions(content, files,
|
|
156
|
+
export function createMessageWithFileInstructions(content, files, editorContext) {
|
|
158
157
|
const parts = [content];
|
|
159
|
-
// Add system info if provided
|
|
160
|
-
if (systemInfo) {
|
|
161
|
-
const systemInfoLines = [
|
|
162
|
-
`└─ Platform: ${systemInfo.platform}`,
|
|
163
|
-
`└─ Shell: ${systemInfo.shell}`,
|
|
164
|
-
`└─ Working Directory: ${systemInfo.workingDirectory}`
|
|
165
|
-
];
|
|
166
|
-
parts.push(systemInfoLines.join('\n'));
|
|
167
|
-
}
|
|
168
158
|
// Add editor context if provided (from VSCode connection)
|
|
169
159
|
if (editorContext) {
|
|
170
160
|
const editorLines = [];
|
|
@@ -193,49 +183,3 @@ export function createMessageWithFileInstructions(content, files, systemInfo, ed
|
|
|
193
183
|
}
|
|
194
184
|
return parts.join('\n');
|
|
195
185
|
}
|
|
196
|
-
/**
|
|
197
|
-
* Get system information (OS, shell, working directory)
|
|
198
|
-
*/
|
|
199
|
-
export function getSystemInfo() {
|
|
200
|
-
// Get OS platform
|
|
201
|
-
const platform = (() => {
|
|
202
|
-
const platformType = os.platform();
|
|
203
|
-
switch (platformType) {
|
|
204
|
-
case 'win32':
|
|
205
|
-
return 'Windows';
|
|
206
|
-
case 'darwin':
|
|
207
|
-
return 'macOS';
|
|
208
|
-
case 'linux':
|
|
209
|
-
return 'Linux';
|
|
210
|
-
default:
|
|
211
|
-
return platformType;
|
|
212
|
-
}
|
|
213
|
-
})();
|
|
214
|
-
// Get shell type
|
|
215
|
-
const shell = (() => {
|
|
216
|
-
const shellPath = process.env['SHELL'] || process.env['ComSpec'] || '';
|
|
217
|
-
const shellName = path.basename(shellPath).toLowerCase();
|
|
218
|
-
if (shellName.includes('cmd'))
|
|
219
|
-
return 'cmd.exe';
|
|
220
|
-
if (shellName.includes('powershell'))
|
|
221
|
-
return 'PowerShell';
|
|
222
|
-
if (shellName.includes('pwsh'))
|
|
223
|
-
return 'PowerShell';
|
|
224
|
-
if (shellName.includes('zsh'))
|
|
225
|
-
return 'zsh';
|
|
226
|
-
if (shellName.includes('bash'))
|
|
227
|
-
return 'bash';
|
|
228
|
-
if (shellName.includes('fish'))
|
|
229
|
-
return 'fish';
|
|
230
|
-
if (shellName.includes('sh'))
|
|
231
|
-
return 'sh';
|
|
232
|
-
return shellName || 'shell';
|
|
233
|
-
})();
|
|
234
|
-
// Get working directory
|
|
235
|
-
const workingDirectory = process.cwd();
|
|
236
|
-
return {
|
|
237
|
-
platform,
|
|
238
|
-
shell,
|
|
239
|
-
workingDirectory
|
|
240
|
-
};
|
|
241
|
-
}
|
|
@@ -2,13 +2,21 @@ export interface Viewport {
|
|
|
2
2
|
width: number;
|
|
3
3
|
height: number;
|
|
4
4
|
}
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* 统一的占位符类型,用于大文本粘贴和图片
|
|
7
|
+
*/
|
|
8
|
+
export interface Placeholder {
|
|
6
9
|
id: string;
|
|
7
10
|
content: string;
|
|
11
|
+
type: 'text' | 'image';
|
|
8
12
|
charCount: number;
|
|
9
13
|
index: number;
|
|
10
14
|
placeholder: string;
|
|
15
|
+
mimeType?: string;
|
|
11
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* 图片数据类型(向后兼容)
|
|
19
|
+
*/
|
|
12
20
|
export interface ImageData {
|
|
13
21
|
id: string;
|
|
14
22
|
data: string;
|
|
@@ -20,10 +28,9 @@ export declare class TextBuffer {
|
|
|
20
28
|
private content;
|
|
21
29
|
private cursorIndex;
|
|
22
30
|
private viewport;
|
|
23
|
-
private
|
|
24
|
-
private
|
|
25
|
-
private
|
|
26
|
-
private imageCounter;
|
|
31
|
+
private placeholderStorage;
|
|
32
|
+
private textPlaceholderCounter;
|
|
33
|
+
private imagePlaceholderCounter;
|
|
27
34
|
private pasteAccumulator;
|
|
28
35
|
private pasteTimer;
|
|
29
36
|
private pastePlaceholderPosition;
|
|
@@ -40,7 +47,7 @@ export declare class TextBuffer {
|
|
|
40
47
|
destroy(): void;
|
|
41
48
|
get text(): string;
|
|
42
49
|
/**
|
|
43
|
-
*
|
|
50
|
+
* 获取完整文本,包括替换占位符为原始内容(仅文本类型)
|
|
44
51
|
*/
|
|
45
52
|
getFullText(): string;
|
|
46
53
|
get visualCursor(): [number, number];
|
|
@@ -79,11 +86,11 @@ export declare class TextBuffer {
|
|
|
79
86
|
private moveCursorToVisualRow;
|
|
80
87
|
private recomputeVisualCursorOnly;
|
|
81
88
|
/**
|
|
82
|
-
*
|
|
89
|
+
* 插入图片数据(使用统一的占位符系统)
|
|
83
90
|
*/
|
|
84
91
|
insertImage(base64Data: string, mimeType: string): void;
|
|
85
92
|
/**
|
|
86
|
-
*
|
|
93
|
+
* 获取所有图片数据(还原为 data URL 格式)
|
|
87
94
|
*/
|
|
88
95
|
getImages(): ImageData[];
|
|
89
96
|
/**
|
package/dist/utils/textBuffer.js
CHANGED
|
@@ -35,30 +35,24 @@ export class TextBuffer {
|
|
|
35
35
|
writable: true,
|
|
36
36
|
value: void 0
|
|
37
37
|
});
|
|
38
|
-
Object.defineProperty(this, "
|
|
38
|
+
Object.defineProperty(this, "placeholderStorage", {
|
|
39
39
|
enumerable: true,
|
|
40
40
|
configurable: true,
|
|
41
41
|
writable: true,
|
|
42
42
|
value: new Map()
|
|
43
|
-
});
|
|
44
|
-
Object.defineProperty(this, "
|
|
43
|
+
}); // 统一的占位符存储
|
|
44
|
+
Object.defineProperty(this, "textPlaceholderCounter", {
|
|
45
45
|
enumerable: true,
|
|
46
46
|
configurable: true,
|
|
47
47
|
writable: true,
|
|
48
48
|
value: 0
|
|
49
|
-
});
|
|
50
|
-
Object.defineProperty(this, "
|
|
51
|
-
enumerable: true,
|
|
52
|
-
configurable: true,
|
|
53
|
-
writable: true,
|
|
54
|
-
value: new Map()
|
|
55
|
-
});
|
|
56
|
-
Object.defineProperty(this, "imageCounter", {
|
|
49
|
+
}); // 文本占位符计数器
|
|
50
|
+
Object.defineProperty(this, "imagePlaceholderCounter", {
|
|
57
51
|
enumerable: true,
|
|
58
52
|
configurable: true,
|
|
59
53
|
writable: true,
|
|
60
54
|
value: 0
|
|
61
|
-
});
|
|
55
|
+
}); // 图片占位符计数器
|
|
62
56
|
Object.defineProperty(this, "pasteAccumulator", {
|
|
63
57
|
enumerable: true,
|
|
64
58
|
configurable: true,
|
|
@@ -126,20 +120,20 @@ export class TextBuffer {
|
|
|
126
120
|
clearTimeout(this.pasteTimer);
|
|
127
121
|
this.pasteTimer = null;
|
|
128
122
|
}
|
|
129
|
-
this.
|
|
130
|
-
this.imageStorage.clear();
|
|
123
|
+
this.placeholderStorage.clear();
|
|
131
124
|
this.onUpdateCallback = undefined;
|
|
132
125
|
}
|
|
133
126
|
get text() {
|
|
134
127
|
return this.content;
|
|
135
128
|
}
|
|
136
129
|
/**
|
|
137
|
-
*
|
|
130
|
+
* 获取完整文本,包括替换占位符为原始内容(仅文本类型)
|
|
138
131
|
*/
|
|
139
132
|
getFullText() {
|
|
140
133
|
let fullText = this.content;
|
|
141
|
-
for (const placeholder of this.
|
|
142
|
-
|
|
134
|
+
for (const placeholder of this.placeholderStorage.values()) {
|
|
135
|
+
// 只替换文本类型的占位符
|
|
136
|
+
if (placeholder.type === 'text' && placeholder.placeholder) {
|
|
143
137
|
fullText = fullText
|
|
144
138
|
.split(placeholder.placeholder)
|
|
145
139
|
.join(placeholder.content);
|
|
@@ -170,10 +164,9 @@ export class TextBuffer {
|
|
|
170
164
|
this.content = sanitized;
|
|
171
165
|
this.clampCursorIndex();
|
|
172
166
|
if (sanitized === '') {
|
|
173
|
-
this.
|
|
174
|
-
this.
|
|
175
|
-
this.
|
|
176
|
-
this.imageCounter = 0;
|
|
167
|
+
this.placeholderStorage.clear();
|
|
168
|
+
this.textPlaceholderCounter = 0;
|
|
169
|
+
this.imagePlaceholderCounter = 0;
|
|
177
170
|
this.pasteAccumulator = '';
|
|
178
171
|
if (this.pasteTimer) {
|
|
179
172
|
clearTimeout(this.pasteTimer);
|
|
@@ -250,14 +243,15 @@ export class TextBuffer {
|
|
|
250
243
|
this.content = this.content.replace(tempPlaceholderPattern, '');
|
|
251
244
|
// 只有当累积的字符数超过300时才创建占位符
|
|
252
245
|
if (totalChars > 300) {
|
|
253
|
-
this.
|
|
254
|
-
const pasteId = `paste_${Date.now()}_${this.
|
|
255
|
-
const placeholderText = `[Paste ${totalChars} characters #${this.
|
|
256
|
-
this.
|
|
246
|
+
this.textPlaceholderCounter++;
|
|
247
|
+
const pasteId = `paste_${Date.now()}_${this.textPlaceholderCounter}`;
|
|
248
|
+
const placeholderText = `[Paste ${totalChars} characters #${this.textPlaceholderCounter}]`;
|
|
249
|
+
this.placeholderStorage.set(pasteId, {
|
|
257
250
|
id: pasteId,
|
|
251
|
+
type: 'text',
|
|
258
252
|
content: this.pasteAccumulator,
|
|
259
253
|
charCount: totalChars,
|
|
260
|
-
index: this.
|
|
254
|
+
index: this.textPlaceholderCounter,
|
|
261
255
|
placeholder: placeholderText,
|
|
262
256
|
});
|
|
263
257
|
// 在记录的位置插入占位符
|
|
@@ -507,33 +501,57 @@ export class TextBuffer {
|
|
|
507
501
|
this.preferredVisualCol = this.visualCursorPos[1];
|
|
508
502
|
}
|
|
509
503
|
/**
|
|
510
|
-
*
|
|
504
|
+
* 插入图片数据(使用统一的占位符系统)
|
|
511
505
|
*/
|
|
512
506
|
insertImage(base64Data, mimeType) {
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
const
|
|
516
|
-
this.
|
|
507
|
+
// 清理 base64 数据:移除所有空白字符(包括换行符)
|
|
508
|
+
// PowerShell/macOS 的 base64 编码可能包含换行符
|
|
509
|
+
const cleanedBase64 = base64Data.replace(/\s+/g, '');
|
|
510
|
+
this.imagePlaceholderCounter++;
|
|
511
|
+
const imageId = `image_${Date.now()}_${this.imagePlaceholderCounter}`;
|
|
512
|
+
const placeholderText = `[image #${this.imagePlaceholderCounter}]`;
|
|
513
|
+
this.placeholderStorage.set(imageId, {
|
|
517
514
|
id: imageId,
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
515
|
+
type: 'image',
|
|
516
|
+
content: cleanedBase64,
|
|
517
|
+
charCount: cleanedBase64.length,
|
|
518
|
+
index: this.imagePlaceholderCounter,
|
|
521
519
|
placeholder: placeholderText,
|
|
520
|
+
mimeType: mimeType,
|
|
522
521
|
});
|
|
523
522
|
this.insertPlainText(placeholderText);
|
|
524
523
|
this.scheduleUpdate();
|
|
525
524
|
}
|
|
526
525
|
/**
|
|
527
|
-
*
|
|
526
|
+
* 获取所有图片数据(还原为 data URL 格式)
|
|
528
527
|
*/
|
|
529
528
|
getImages() {
|
|
530
|
-
return Array.from(this.
|
|
529
|
+
return Array.from(this.placeholderStorage.values())
|
|
530
|
+
.filter((p) => p.type === 'image')
|
|
531
|
+
.map((p) => {
|
|
532
|
+
const mimeType = p.mimeType || 'image/png';
|
|
533
|
+
// 还原为 data URL 格式
|
|
534
|
+
const dataUrl = `data:${mimeType};base64,${p.content}`;
|
|
535
|
+
return {
|
|
536
|
+
id: p.id,
|
|
537
|
+
data: dataUrl,
|
|
538
|
+
mimeType: mimeType,
|
|
539
|
+
index: p.index,
|
|
540
|
+
placeholder: p.placeholder,
|
|
541
|
+
};
|
|
542
|
+
})
|
|
543
|
+
.sort((a, b) => a.index - b.index);
|
|
531
544
|
}
|
|
532
545
|
/**
|
|
533
546
|
* 清除所有图片
|
|
534
547
|
*/
|
|
535
548
|
clearImages() {
|
|
536
|
-
|
|
537
|
-
this.
|
|
549
|
+
// 只清除图片类型的占位符
|
|
550
|
+
for (const [id, placeholder] of this.placeholderStorage.entries()) {
|
|
551
|
+
if (placeholder.type === 'image') {
|
|
552
|
+
this.placeholderStorage.delete(id);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
this.imagePlaceholderCounter = 0;
|
|
538
556
|
}
|
|
539
557
|
}
|