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.
@@ -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
- return getSystemPromptWithRole();
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 base64 = execSync(`powershell -Command "${psScript}"`, {
12
+ const base64Raw = execSync(`powershell -Command "${psScript}"`, {
13
13
  encoding: 'utf-8',
14
14
  timeout: 5000,
15
- }).trim();
15
+ });
16
+ // 清理所有空白字符(包括换行符)
17
+ const base64 = base64Raw.replace(/\s+/g, '');
16
18
  if (base64 && base64.length > 100) {
17
- const dataUrl = `data:image/png;base64,${base64}`;
18
- buffer.insertImage(dataUrl, 'image/png');
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 base64 = execSync(`base64 -i "${tmpFile}"`, {
56
+ const base64Raw = execSync(`base64 -i "${tmpFile}"`, {
55
57
  encoding: 'utf-8',
56
58
  timeout: 2000,
57
- }).trim();
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
- const dataUrl = `data:image/png;base64,${base64}`;
67
- buffer.insertImage(dataUrl, 'image/png');
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?: () => string[];
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) => void): {
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
- // Put the message content in the input buffer
49
- buffer.setText(selectedMessage.content);
48
+ // Don't modify buffer here - let ChatInput handle everything via initialContent
49
+ // This prevents duplicate image placeholders
50
50
  setShowHistoryMenu(false);
51
- triggerUpdate();
52
- onHistorySelect(selectedIndex, selectedMessage.content);
51
+ onHistorySelect(selectedIndex, selectedMessage.content, selectedMessage.images);
53
52
  }
54
- }, [chatHistory, onHistorySelect, buffer]);
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?: string | null;
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, cpLen } from '../../utils/textUtils.js';
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
- buffer.setText(initialContent);
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 and paste placeholders
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的内部文本而不是getFullText(),这样可以显示占位符
187
+ // 使用buffer的内部文本,将占位符当作普通文本处理
150
188
  const displayText = buffer.text;
151
189
  const cursorPos = buffer.getCursorPosition();
152
- const hasPastePlaceholder = displayText.includes('[Paste ') &&
153
- displayText.includes(' characters #');
154
- const hasImagePlaceholder = displayText.includes('[image #');
155
- const focusTokenPattern = /(\x1b)?\[[IO]/g;
156
- const cleanedText = displayText.replace(focusTokenPattern, '').trim();
157
- const isFocusNoise = cleanedText.length === 0;
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" }, (() => {
@@ -13,11 +13,6 @@ export interface Message {
13
13
  data: string;
14
14
  mimeType: string;
15
15
  }>;
16
- systemInfo?: {
17
- platform: string;
18
- shell: string;
19
- workingDirectory: string;
20
- };
21
16
  toolCall?: {
22
17
  name: string;
23
18
  arguments: any;
@@ -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.systemInfo ||
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: string[];
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(Text, { color: "blue", bold: true },
14
- index + 1,
15
- "."),
16
- React.createElement(Box, { marginLeft: 1 },
17
- React.createElement(Text, { color: "gray" }, message.length > 60 ? `${message.substring(0, 60)}...` : message))))),
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, getSystemInfo, } from '../../utils/fileUtils.js';
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
- // Note: message is already in input buffer via useHistoryNavigation
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(snapshotState.pendingRollback.message);
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, system info, and editor context
544
- const messageForAI = createMessageWithFileInstructions(cleanContent, regularFiles, systemInfo, vscodeState.vscodeConnected ? vscodeState.editorContext : undefined);
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
- // Convert image files to image content format
684
- const imageContents = imageFiles.length > 0
685
- ? imageFiles.map(f => ({
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: f.imageData,
688
- mimeType: f.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, and editor context
708
- const messageForAI = createMessageWithFileInstructions(cleanContent, regularFiles, systemInfo, vscodeState.vscodeConnected ? vscodeState.editorContext : undefined);
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, getSystemInfo, } from '../../utils/fileUtils.js';
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, systemInfo, vscodeState.vscodeConnected ? vscodeState.editorContext : undefined);
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[], systemInfo?: {
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
- };
@@ -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, systemInfo, editorContext) {
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
- export interface PastePlaceholder {
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 pasteStorage;
24
- private pasteCounter;
25
- private imageStorage;
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
  /**
@@ -35,30 +35,24 @@ export class TextBuffer {
35
35
  writable: true,
36
36
  value: void 0
37
37
  });
38
- Object.defineProperty(this, "pasteStorage", {
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, "pasteCounter", {
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, "imageStorage", {
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.pasteStorage.clear();
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.pasteStorage.values()) {
142
- if (placeholder.placeholder) {
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.pasteStorage.clear();
174
- this.pasteCounter = 0;
175
- this.imageStorage.clear();
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.pasteCounter++;
254
- const pasteId = `paste_${Date.now()}_${this.pasteCounter}`;
255
- const placeholderText = `[Paste ${totalChars} characters #${this.pasteCounter}]`;
256
- this.pasteStorage.set(pasteId, {
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.pasteCounter,
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
- this.imageCounter++;
514
- const imageId = `image_${Date.now()}_${this.imageCounter}`;
515
- const placeholderText = `[image #${this.imageCounter}]`;
516
- this.imageStorage.set(imageId, {
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
- data: base64Data,
519
- mimeType: mimeType,
520
- index: this.imageCounter,
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.imageStorage.values()).sort((a, b) => a.index - b.index);
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
- this.imageStorage.clear();
537
- this.imageCounter = 0;
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "snow-ai",
3
- "version": "0.3.15",
3
+ "version": "0.3.17",
4
4
  "description": "Intelligent Command Line Assistant powered by AI",
5
5
  "license": "MIT",
6
6
  "bin": {