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.
@@ -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;
@@ -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
@@ -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
- const isFocusNoise = cleanedText.length === 0;
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 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;
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
- 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))));
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
- return isPlaceholder ? (React.createElement(Text, { key: partIndex, color: isImagePlaceholder ? 'magenta' : 'cyan', dimColor: true }, part)) : (React.createElement(Text, { key: partIndex }, part));
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
- return (React.createElement(Text, null,
192
- elements,
193
- !cursorRendered && renderCursor(' ')));
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" }, (() => {
@@ -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
  }