snow-ai 0.3.14 → 0.3.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +95 -20
- 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/ui/pages/SubAgentConfigScreen.js +44 -3
- package/dist/ui/pages/SubAgentListScreen.js +17 -2
- 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/mcpToolsManager.js +6 -2
- 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;
|
|
@@ -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
|
|
@@ -154,7 +192,9 @@ export default function ChatInput({ onSubmit, onCommand, placeholder = 'Type you
|
|
|
154
192
|
const hasImagePlaceholder = displayText.includes('[image #');
|
|
155
193
|
const focusTokenPattern = /(\x1b)?\[[IO]/g;
|
|
156
194
|
const cleanedText = displayText.replace(focusTokenPattern, '').trim();
|
|
157
|
-
|
|
195
|
+
// 检查是否只有换行符或焦点标记
|
|
196
|
+
const hasOnlyNewlines = /^[\n\s]*$/.test(displayText.replace(focusTokenPattern, ''));
|
|
197
|
+
const isFocusNoise = cleanedText.length === 0 && !hasOnlyNewlines;
|
|
158
198
|
if (hasPastePlaceholder || hasImagePlaceholder || isFocusNoise) {
|
|
159
199
|
const atCursor = (() => {
|
|
160
200
|
const charInfo = buffer.getCharAtCursor();
|
|
@@ -162,35 +202,70 @@ export default function ChatInput({ onSubmit, onCommand, placeholder = 'Type you
|
|
|
162
202
|
})();
|
|
163
203
|
// 分割文本并高亮占位符(粘贴和图片)
|
|
164
204
|
const parts = displayText.split(/(\[Paste \d+ characters #\d+\]|\[image #\d+\])/);
|
|
205
|
+
// 先构建带位置信息的数据结构
|
|
206
|
+
const partsWithPosition = [];
|
|
165
207
|
let processedLength = 0;
|
|
166
|
-
let
|
|
167
|
-
|
|
168
|
-
const isPastePlaceholder = part.match(/^\[Paste \d+ characters #\d+\]$/);
|
|
169
|
-
const isImagePlaceholder = part.match(/^\[image #\d+\]$/);
|
|
170
|
-
const isPlaceholder = isPastePlaceholder || isImagePlaceholder;
|
|
208
|
+
for (let i = 0; i < parts.length; i++) {
|
|
209
|
+
const part = parts[i] || '';
|
|
210
|
+
const isPastePlaceholder = !!part.match(/^\[Paste \d+ characters #\d+\]$/);
|
|
211
|
+
const isImagePlaceholder = !!part.match(/^\[image #\d+\]$/);
|
|
171
212
|
const partStart = processedLength;
|
|
172
213
|
const partEnd = processedLength + cpLen(part);
|
|
173
214
|
processedLength = partEnd;
|
|
215
|
+
if (part.length > 0) {
|
|
216
|
+
partsWithPosition.push({
|
|
217
|
+
part,
|
|
218
|
+
partStart,
|
|
219
|
+
partEnd,
|
|
220
|
+
isPastePlaceholder,
|
|
221
|
+
isImagePlaceholder,
|
|
222
|
+
originalIndex: i,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
let cursorRendered = false;
|
|
227
|
+
const elements = [];
|
|
228
|
+
let elementKey = 0;
|
|
229
|
+
for (const item of partsWithPosition) {
|
|
230
|
+
const { part, partStart, partEnd, isPastePlaceholder, isImagePlaceholder, originalIndex } = item;
|
|
231
|
+
const isPlaceholder = isPastePlaceholder || isImagePlaceholder;
|
|
174
232
|
// 检查光标是否在这个部分
|
|
175
233
|
if (cursorPos >= partStart && cursorPos < partEnd) {
|
|
176
234
|
cursorRendered = true;
|
|
177
235
|
const beforeCursorInPart = cpSlice(part, 0, cursorPos - partStart);
|
|
178
236
|
const afterCursorInPart = cpSlice(part, cursorPos - partStart + 1);
|
|
179
|
-
|
|
180
|
-
beforeCursorInPart
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
237
|
+
if (isPlaceholder) {
|
|
238
|
+
if (beforeCursorInPart) {
|
|
239
|
+
elements.push(React.createElement(Text, { key: `${originalIndex}-before`, color: isImagePlaceholder ? 'magenta' : 'cyan', dimColor: true }, beforeCursorInPart));
|
|
240
|
+
}
|
|
241
|
+
elements.push(React.createElement(React.Fragment, { key: `cursor-${elementKey++}` }, renderCursor(atCursor)));
|
|
242
|
+
if (afterCursorInPart) {
|
|
243
|
+
elements.push(React.createElement(Text, { key: `${originalIndex}-after`, color: isImagePlaceholder ? 'magenta' : 'cyan', dimColor: true }, afterCursorInPart));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
if (beforeCursorInPart) {
|
|
248
|
+
elements.push(React.createElement(Text, { key: `${originalIndex}-before` }, beforeCursorInPart));
|
|
249
|
+
}
|
|
250
|
+
elements.push(React.createElement(React.Fragment, { key: `cursor-${elementKey++}` }, renderCursor(atCursor)));
|
|
251
|
+
if (afterCursorInPart) {
|
|
252
|
+
elements.push(React.createElement(Text, { key: `${originalIndex}-after` }, afterCursorInPart));
|
|
253
|
+
}
|
|
254
|
+
}
|
|
186
255
|
}
|
|
187
256
|
else {
|
|
188
|
-
|
|
257
|
+
if (isPlaceholder) {
|
|
258
|
+
elements.push(React.createElement(Text, { key: originalIndex, color: isImagePlaceholder ? 'magenta' : 'cyan', dimColor: true }, part));
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
elements.push(React.createElement(Text, { key: originalIndex }, part));
|
|
262
|
+
}
|
|
189
263
|
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
elements,
|
|
193
|
-
|
|
264
|
+
}
|
|
265
|
+
if (!cursorRendered) {
|
|
266
|
+
elements.push(React.createElement(React.Fragment, { key: `cursor-final` }, renderCursor(' ')));
|
|
267
|
+
}
|
|
268
|
+
return React.createElement(React.Fragment, null, elements);
|
|
194
269
|
}
|
|
195
270
|
else {
|
|
196
271
|
// 普通文本渲染
|
|
@@ -207,7 +282,7 @@ export default function ChatInput({ onSubmit, onCommand, placeholder = 'Type you
|
|
|
207
282
|
renderCursor(' '),
|
|
208
283
|
React.createElement(Text, { color: disabled ? 'darkGray' : 'gray', dimColor: true }, disabled ? 'Waiting for response...' : placeholder)));
|
|
209
284
|
}
|
|
210
|
-
}, [buffer, disabled, placeholder, renderCursor]);
|
|
285
|
+
}, [buffer, disabled, placeholder, renderCursor, buffer.text]);
|
|
211
286
|
return (React.createElement(Box, { flexDirection: "column", paddingX: 1, width: terminalWidth },
|
|
212
287
|
showHistoryMenu && (React.createElement(Box, { flexDirection: "column", marginBottom: 1, width: terminalWidth - 2 },
|
|
213
288
|
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
|
}
|