snow-ai 0.3.23 → 0.3.24

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.
@@ -12,11 +12,6 @@ export type ConversationHandlerOptions = {
12
12
  saveMessage: (message: any) => Promise<void>;
13
13
  setMessages: React.Dispatch<React.SetStateAction<Message[]>>;
14
14
  setStreamTokenCount: React.Dispatch<React.SetStateAction<number>>;
15
- setCurrentTodos: React.Dispatch<React.SetStateAction<Array<{
16
- id: string;
17
- content: string;
18
- status: 'pending' | 'completed';
19
- }>>>;
20
15
  requestToolConfirmation: (toolCall: ToolCall, batchToolNames?: string, allTools?: ToolCall[]) => Promise<string>;
21
16
  isToolAutoApproved: (toolName: string) => boolean;
22
17
  addMultipleToAlwaysApproved: (toolNames: string[]) => void;
@@ -20,7 +20,7 @@ import { shouldAutoCompress, performAutoCompression, } from '../utils/autoCompre
20
20
  export async function handleConversationWithTools(options) {
21
21
  const { userContent, imageContents, controller,
22
22
  // messages, // No longer used - we load from session instead to get complete history with tool calls
23
- saveMessage, setMessages, setStreamTokenCount, setCurrentTodos, requestToolConfirmation, isToolAutoApproved, addMultipleToAlwaysApproved, yoloMode, setContextUsage, setIsReasoning, setRetryStatus, } = options;
23
+ saveMessage, setMessages, setStreamTokenCount, requestToolConfirmation, isToolAutoApproved, addMultipleToAlwaysApproved, yoloMode, setContextUsage, setIsReasoning, setRetryStatus, } = options;
24
24
  // Create a wrapper function for adding single tool to always-approved list
25
25
  const addToAlwaysApproved = (toolName) => {
26
26
  addMultipleToAlwaysApproved([toolName]);
@@ -33,10 +33,6 @@ export async function handleConversationWithTools(options) {
33
33
  const todoService = getTodoService();
34
34
  // Get existing TODO list
35
35
  const existingTodoList = await todoService.getTodoList(currentSession.id);
36
- // Update UI state
37
- if (existingTodoList) {
38
- setCurrentTodos(existingTodoList.todos);
39
- }
40
36
  // Collect all MCP tools
41
37
  const mcpTools = await collectAllMCPTools();
42
38
  // Build conversation history with TODO context as pinned user message
@@ -364,8 +360,28 @@ export async function handleConversationWithTools(options) {
364
360
  const autoApprovedTools = [];
365
361
  for (const toolCall of receivedToolCalls) {
366
362
  // Check both global approved list and session-approved list
367
- if (isToolAutoApproved(toolCall.function.name) ||
368
- sessionApprovedTools.has(toolCall.function.name)) {
363
+ const isApproved = isToolAutoApproved(toolCall.function.name) ||
364
+ sessionApprovedTools.has(toolCall.function.name);
365
+ // Check if this is a sensitive command (terminal-execute with sensitive pattern)
366
+ let isSensitiveCommand = false;
367
+ if (toolCall.function.name === 'terminal-execute') {
368
+ try {
369
+ const args = JSON.parse(toolCall.function.arguments);
370
+ const { isSensitiveCommand: checkSensitiveCommand } = await import('../utils/sensitiveCommandManager.js').then(m => ({
371
+ isSensitiveCommand: m.isSensitiveCommand,
372
+ }));
373
+ const sensitiveCheck = checkSensitiveCommand(args.command);
374
+ isSensitiveCommand = sensitiveCheck.isSensitive;
375
+ }
376
+ catch {
377
+ // If parsing fails, treat as normal command
378
+ }
379
+ }
380
+ // If sensitive command, always require confirmation regardless of approval status
381
+ if (isSensitiveCommand) {
382
+ toolsNeedingConfirmation.push(toolCall);
383
+ }
384
+ else if (isApproved) {
369
385
  autoApprovedTools.push(toolCall);
370
386
  }
371
387
  else {
@@ -374,24 +390,79 @@ export async function handleConversationWithTools(options) {
374
390
  }
375
391
  // Request confirmation only once for all tools needing confirmation
376
392
  let approvedTools = [...autoApprovedTools];
377
- // In YOLO mode, auto-approve all tools
393
+ // In YOLO mode, auto-approve all tools EXCEPT sensitive commands
378
394
  if (yoloMode) {
379
- approvedTools.push(...toolsNeedingConfirmation);
395
+ // Filter out sensitive commands from auto-approval
396
+ const nonSensitiveTools = [];
397
+ const sensitiveTools = [];
398
+ for (const toolCall of toolsNeedingConfirmation) {
399
+ if (toolCall.function.name === 'terminal-execute') {
400
+ try {
401
+ const args = JSON.parse(toolCall.function.arguments);
402
+ const { isSensitiveCommand: checkSensitiveCommand } = await import('../utils/sensitiveCommandManager.js').then(m => ({
403
+ isSensitiveCommand: m.isSensitiveCommand,
404
+ }));
405
+ const sensitiveCheck = checkSensitiveCommand(args.command);
406
+ if (sensitiveCheck.isSensitive) {
407
+ sensitiveTools.push(toolCall);
408
+ }
409
+ else {
410
+ nonSensitiveTools.push(toolCall);
411
+ }
412
+ }
413
+ catch {
414
+ nonSensitiveTools.push(toolCall);
415
+ }
416
+ }
417
+ else {
418
+ nonSensitiveTools.push(toolCall);
419
+ }
420
+ }
421
+ approvedTools.push(...nonSensitiveTools);
422
+ // If there are sensitive tools, still need confirmation even in YOLO mode
423
+ if (sensitiveTools.length > 0) {
424
+ const firstTool = sensitiveTools[0];
425
+ const allTools = sensitiveTools.length > 1 ? sensitiveTools : undefined;
426
+ const confirmation = await requestToolConfirmation(firstTool, undefined, allTools);
427
+ if (confirmation === 'reject') {
428
+ setMessages(prev => prev.filter(msg => !msg.toolPending));
429
+ for (const toolCall of sensitiveTools) {
430
+ const rejectionMessage = {
431
+ role: 'tool',
432
+ tool_call_id: toolCall.id,
433
+ content: 'Error: Tool execution rejected by user',
434
+ };
435
+ conversationMessages.push(rejectionMessage);
436
+ saveMessage(rejectionMessage).catch(error => {
437
+ console.error('Failed to save tool rejection message:', error);
438
+ });
439
+ }
440
+ setMessages(prev => [
441
+ ...prev,
442
+ {
443
+ role: 'assistant',
444
+ content: 'Tool call rejected, session ended',
445
+ streaming: false,
446
+ },
447
+ ]);
448
+ if (options.setIsStreaming) {
449
+ options.setIsStreaming(false);
450
+ }
451
+ freeEncoder();
452
+ return { usage: accumulatedUsage };
453
+ }
454
+ // Approved, add sensitive tools to approved list
455
+ approvedTools.push(...sensitiveTools);
456
+ }
380
457
  }
381
458
  else if (toolsNeedingConfirmation.length > 0) {
382
- const firstTool = toolsNeedingConfirmation[0]; // Safe: length > 0 guarantees this exists
383
- // Use regular CLI confirmation
384
- // Pass all tools for proper display in confirmation UI
459
+ const firstTool = toolsNeedingConfirmation[0];
385
460
  const allTools = toolsNeedingConfirmation.length > 1
386
461
  ? toolsNeedingConfirmation
387
462
  : undefined;
388
- // Use first tool for confirmation UI, but apply result to all
389
463
  const confirmation = await requestToolConfirmation(firstTool, undefined, allTools);
390
464
  if (confirmation === 'reject') {
391
- // Remove pending tool messages
392
465
  setMessages(prev => prev.filter(msg => !msg.toolPending));
393
- // User rejected - need to save tool rejection messages to maintain conversation structure
394
- // Add tool rejection responses for ALL tools that were rejected
395
466
  for (const toolCall of toolsNeedingConfirmation) {
396
467
  const rejectionMessage = {
397
468
  role: 'tool',
@@ -403,7 +474,6 @@ export async function handleConversationWithTools(options) {
403
474
  console.error('Failed to save tool rejection message:', error);
404
475
  });
405
476
  }
406
- // User rejected - end conversation
407
477
  setMessages(prev => [
408
478
  ...prev,
409
479
  {
@@ -412,12 +482,11 @@ export async function handleConversationWithTools(options) {
412
482
  streaming: false,
413
483
  },
414
484
  ]);
415
- // End streaming immediately
416
485
  if (options.setIsStreaming) {
417
486
  options.setIsStreaming(false);
418
487
  }
419
488
  freeEncoder();
420
- return { usage: accumulatedUsage }; // Exit the conversation loop
489
+ return { usage: accumulatedUsage };
421
490
  }
422
491
  // If approved_always, add ALL these tools to both global and session-approved sets
423
492
  if (confirmation === 'approve_always') {
@@ -648,18 +717,6 @@ export async function handleConversationWithTools(options) {
648
717
  // 即使压缩失败也继续处理工具结果
649
718
  }
650
719
  }
651
- // Check if there are TODO related tool calls, if yes refresh TODO list
652
- const hasTodoTools = approvedTools.some(t => t.function.name.startsWith('todo-'));
653
- const hasTodoUpdateTools = approvedTools.some(t => t.function.name === 'todo-update');
654
- if (hasTodoTools) {
655
- const session = sessionManager.getCurrentSession();
656
- if (session) {
657
- const updatedTodoList = await todoService.getTodoList(session.id);
658
- if (updatedTodoList) {
659
- setCurrentTodos(updatedTodoList.todos);
660
- }
661
- }
662
- }
663
720
  // Remove only streaming sub-agent content messages (not tool-related messages)
664
721
  // Keep sub-agent tool call and tool result messages for display
665
722
  setMessages(prev => prev.filter(m => m.role !== 'subagent' ||
@@ -763,18 +820,6 @@ export async function handleConversationWithTools(options) {
763
820
  });
764
821
  }
765
822
  }
766
- // After all tool results are processed, show TODO panel if there were todo-update calls
767
- if (hasTodoUpdateTools) {
768
- setMessages(prev => [
769
- ...prev,
770
- {
771
- role: 'assistant',
772
- content: '',
773
- streaming: false,
774
- showTodoTree: true,
775
- },
776
- ]);
777
- }
778
823
  // Check if there are pending user messages to insert
779
824
  if (options.getPendingMessages && options.clearPendingMessages) {
780
825
  const pendingMessages = options.getPendingMessages();
@@ -817,7 +862,9 @@ export async function handleConversationWithTools(options) {
817
862
  // Clear pending messages
818
863
  options.clearPendingMessages();
819
864
  // Combine multiple pending messages into one
820
- const combinedMessage = pendingMessages.map(m => m.text).join('\n\n');
865
+ const combinedMessage = pendingMessages
866
+ .map(m => m.text)
867
+ .join('\n\n');
821
868
  // Collect all images from pending messages
822
869
  const allPendingImages = pendingMessages
823
870
  .flatMap(m => m.images || [])
@@ -11,7 +11,7 @@ export declare function useFilePicker(buffer: TextBuffer, triggerUpdate: () => v
11
11
  setAtSymbolPosition: (_pos: number) => void;
12
12
  filteredFileCount: number;
13
13
  searchMode: "content" | "file";
14
- updateFilePickerState: (text: string, cursorPos: number) => void;
14
+ updateFilePickerState: (_text: string, cursorPos: number) => void;
15
15
  handleFileSelect: (filePath: string) => Promise<void>;
16
16
  handleFilteredCountChange: (count: number) => void;
17
17
  fileListRef: import("react").RefObject<FileListRef>;
@@ -51,15 +51,19 @@ export function useFilePicker(buffer, triggerUpdate) {
51
51
  });
52
52
  const fileListRef = useRef(null);
53
53
  // Update file picker state
54
- const updateFilePickerState = useCallback((text, cursorPos) => {
55
- if (!text.includes('@')) {
54
+ const updateFilePickerState = useCallback((_text, cursorPos) => {
55
+ // Use display text (with placeholders) instead of full text (expanded)
56
+ // to ensure cursor position matches text content
57
+ // Note: _text parameter is ignored, we use buffer.text instead
58
+ const displayText = buffer.text;
59
+ if (!displayText.includes('@')) {
56
60
  if (state.showFilePicker) {
57
61
  dispatch({ type: 'HIDE' });
58
62
  }
59
63
  return;
60
64
  }
61
65
  // Find the last '@' or '@@' symbol before the cursor
62
- const beforeCursor = text.slice(0, cursorPos);
66
+ const beforeCursor = displayText.slice(0, cursorPos);
63
67
  // Look for @@ first (content search), then @ (file search)
64
68
  let searchMode = 'file';
65
69
  let position = -1;
@@ -130,6 +134,7 @@ export function useFilePicker(buffer, triggerUpdate) {
130
134
  }
131
135
  }
132
136
  }, [
137
+ buffer,
133
138
  state.showFilePicker,
134
139
  state.fileQuery,
135
140
  state.atSymbolPosition,
@@ -138,13 +143,14 @@ export function useFilePicker(buffer, triggerUpdate) {
138
143
  // Handle file selection
139
144
  const handleFileSelect = useCallback(async (filePath) => {
140
145
  if (state.atSymbolPosition !== -1) {
141
- const text = buffer.getFullText();
146
+ // Use display text (with placeholders) for position calculations
147
+ const displayText = buffer.text;
142
148
  const cursorPos = buffer.getCursorPosition();
143
149
  // Replace query with selected file path
144
150
  // For content search (@@), the filePath already includes line number
145
151
  // For file search (@), just the file path
146
- const beforeAt = text.slice(0, state.atSymbolPosition);
147
- const afterCursor = text.slice(cursorPos);
152
+ const beforeAt = displayText.slice(0, state.atSymbolPosition);
153
+ const afterCursor = displayText.slice(cursorPos);
148
154
  // Construct the replacement based on search mode
149
155
  const prefix = state.searchMode === 'content' ? '@@' : '@';
150
156
  const newText = beforeAt + prefix + filePath + ' ' + afterCursor;
@@ -156,7 +162,7 @@ export function useFilePicker(buffer, triggerUpdate) {
156
162
  const targetPos = state.atSymbolPosition + insertedLength;
157
163
  // Reset cursor to beginning, then move to correct position
158
164
  for (let i = 0; i < targetPos; i++) {
159
- if (i < buffer.getFullText().length) {
165
+ if (i < buffer.text.length) {
160
166
  buffer.moveRight();
161
167
  }
162
168
  }
@@ -2,5 +2,5 @@ import { TextBuffer, Viewport } from '../utils/textBuffer.js';
2
2
  export declare function useInputBuffer(viewport: Viewport): {
3
3
  buffer: TextBuffer;
4
4
  triggerUpdate: () => void;
5
- forceUpdate: import("react").Dispatch<import("react").SetStateAction<{}>>;
5
+ forceUpdate: () => void;
6
6
  };
@@ -1,21 +1,36 @@
1
1
  import { useState, useCallback, useEffect, useRef } from 'react';
2
2
  import { TextBuffer } from '../utils/textBuffer.js';
3
3
  export function useInputBuffer(viewport) {
4
- const [, forceUpdate] = useState({});
4
+ const [, setForceUpdateState] = useState({});
5
5
  const lastUpdateTime = useRef(0);
6
- // Force re-render when buffer changes
7
- const triggerUpdate = useCallback(() => {
6
+ const bufferRef = useRef(null);
7
+ // Stable forceUpdate function using useRef
8
+ const forceUpdateRef = useRef(() => {
9
+ setForceUpdateState({});
10
+ });
11
+ // Stable triggerUpdate function using useRef
12
+ const triggerUpdateRef = useRef(() => {
8
13
  const now = Date.now();
9
14
  lastUpdateTime.current = now;
10
- forceUpdate({});
11
- }, []); // 空依赖项确保函数稳定
12
- const [buffer] = useState(() => new TextBuffer(viewport, triggerUpdate));
15
+ forceUpdateRef.current();
16
+ });
17
+ // Initialize buffer once
18
+ if (!bufferRef.current) {
19
+ bufferRef.current = new TextBuffer(viewport, triggerUpdateRef.current);
20
+ }
21
+ const buffer = bufferRef.current;
22
+ // Expose stable callback functions
23
+ const forceUpdate = useCallback(() => {
24
+ forceUpdateRef.current();
25
+ }, []);
26
+ const triggerUpdate = useCallback(() => {
27
+ triggerUpdateRef.current();
28
+ }, []);
13
29
  // Update buffer viewport when viewport changes
14
30
  useEffect(() => {
15
31
  buffer.updateViewport(viewport);
16
- // 直接调用 forceUpdate 而不是 triggerUpdate,避免依赖问题
17
- forceUpdate({});
18
- }, [viewport.width, viewport.height]); // 移除 buffer 和 triggerUpdate 避免循环依赖
32
+ forceUpdateRef.current();
33
+ }, [viewport.width, viewport.height, buffer]);
19
34
  // Cleanup buffer on unmount
20
35
  useEffect(() => {
21
36
  return () => {
@@ -45,7 +45,7 @@ export function useStreamingState() {
45
45
  }, [timerStartTime]);
46
46
  // Initialize remaining seconds when retry starts
47
47
  useEffect(() => {
48
- if (!retryStatus || !retryStatus.isRetrying)
48
+ if (!retryStatus?.isRetrying)
49
49
  return;
50
50
  if (retryStatus.remainingSeconds !== undefined)
51
51
  return;
@@ -56,7 +56,7 @@ export function useStreamingState() {
56
56
  remainingSeconds: Math.ceil(prev.nextDelay / 1000),
57
57
  }
58
58
  : null);
59
- }, [retryStatus?.isRetrying, retryStatus?.nextDelay]);
59
+ }, [retryStatus?.isRetrying]); // Only depend on isRetrying flag
60
60
  // Countdown timer for retry delays
61
61
  useEffect(() => {
62
62
  if (!retryStatus || !retryStatus.isRetrying)
@@ -1,26 +1,43 @@
1
- import { useState, useEffect } from 'react';
1
+ import { useState, useEffect, useRef } from 'react';
2
2
  import { vscodeConnection } from '../utils/vscodeConnection.js';
3
3
  export function useVSCodeState() {
4
4
  const [vscodeConnected, setVscodeConnected] = useState(false);
5
5
  const [vscodeConnectionStatus, setVscodeConnectionStatus] = useState('disconnected');
6
6
  const [editorContext, setEditorContext] = useState({});
7
+ // Use ref to track last status without causing re-renders
8
+ const lastStatusRef = useRef('disconnected');
9
+ // Use ref to track last editor context to avoid unnecessary updates
10
+ const lastEditorContextRef = useRef({});
7
11
  // Monitor VSCode connection status and editor context
8
12
  useEffect(() => {
9
13
  const checkConnectionInterval = setInterval(() => {
10
14
  const isConnected = vscodeConnection.isConnected();
11
15
  setVscodeConnected(isConnected);
12
16
  // Update connection status based on actual connection state
13
- if (isConnected && vscodeConnectionStatus !== 'connected') {
17
+ // Use ref to avoid reading from state
18
+ if (isConnected && lastStatusRef.current !== 'connected') {
19
+ lastStatusRef.current = 'connected';
14
20
  setVscodeConnectionStatus('connected');
15
21
  }
16
- else if (!isConnected && vscodeConnectionStatus === 'connected') {
22
+ else if (!isConnected && lastStatusRef.current === 'connected') {
23
+ lastStatusRef.current = 'disconnected';
17
24
  setVscodeConnectionStatus('disconnected');
18
25
  }
19
26
  }, 1000);
20
27
  const unsubscribe = vscodeConnection.onContextUpdate(context => {
21
- setEditorContext(context);
28
+ // Only update state if context has actually changed
29
+ const hasChanged = context.activeFile !== lastEditorContextRef.current.activeFile ||
30
+ context.selectedText !== lastEditorContextRef.current.selectedText ||
31
+ context.cursorPosition?.line !== lastEditorContextRef.current.cursorPosition?.line ||
32
+ context.cursorPosition?.character !== lastEditorContextRef.current.cursorPosition?.character ||
33
+ context.workspaceFolder !== lastEditorContextRef.current.workspaceFolder;
34
+ if (hasChanged) {
35
+ lastEditorContextRef.current = context;
36
+ setEditorContext(context);
37
+ }
22
38
  // When we receive context, it means connection is successful
23
- if (vscodeConnectionStatus !== 'connected') {
39
+ if (lastStatusRef.current !== 'connected') {
40
+ lastStatusRef.current = 'connected';
24
41
  setVscodeConnectionStatus('connected');
25
42
  }
26
43
  });
@@ -28,7 +45,7 @@ export function useVSCodeState() {
28
45
  clearInterval(checkConnectionInterval);
29
46
  unsubscribe();
30
47
  };
31
- }, [vscodeConnectionStatus]);
48
+ }, []); // Remove vscodeConnectionStatus from dependencies
32
49
  // Separate effect for handling connecting timeout
33
50
  useEffect(() => {
34
51
  if (vscodeConnectionStatus !== 'connecting') {
@@ -1033,7 +1033,7 @@ export const mcpTools = [
1033
1033
  },
1034
1034
  {
1035
1035
  name: 'filesystem-create',
1036
- description: 'PREFERRED tool for file creation: Create a new file with specified content. More reliable than terminal commands like echo/cat with redirects. Automatically creates parent directories if needed. Terminal commands can be used as a fallback if needed.',
1036
+ description: 'Preferred tool for creating files: Use specified content to create a new file. Before creating the file, you need to determine if the file already exists; if it does, your creation will fail. You should use editing instead of creation, as this tool is more reliable than terminal commands like echo/cat with redirection. If necessary, automatically create the parent directory. If necessary, terminal commands can be used as a fallback.',
1037
1037
  inputSchema: {
1038
1038
  type: 'object',
1039
1039
  properties: {
@@ -1,4 +1,4 @@
1
- import React, { useCallback, useEffect, useRef } from 'react';
1
+ import React, { useCallback, useEffect, useRef, useMemo } from 'react';
2
2
  import { Box, Text } from 'ink';
3
3
  import { cpSlice } from '../../utils/textUtils.js';
4
4
  import CommandPanel from './CommandPanel.js';
@@ -41,10 +41,10 @@ export default function ChatInput({ onSubmit, onCommand, placeholder = 'Type you
41
41
  // Recalculate viewport dimensions to ensure proper resizing
42
42
  const uiOverhead = 8;
43
43
  const viewportWidth = Math.max(40, terminalWidth - uiOverhead);
44
- const viewport = {
44
+ const viewport = useMemo(() => ({
45
45
  width: viewportWidth,
46
46
  height: 1,
47
- };
47
+ }), [viewportWidth]); // Memoize viewport to prevent unnecessary re-renders
48
48
  // Use input buffer hook
49
49
  const { buffer, triggerUpdate, forceUpdate } = useInputBuffer(viewport);
50
50
  // Use command panel hook
@@ -169,10 +169,10 @@ export default function ChatInput({ onSubmit, onCommand, placeholder = 'Type you
169
169
  useEffect(() => {
170
170
  // Use a small delay to ensure the component tree has updated
171
171
  const timer = setTimeout(() => {
172
- forceUpdate({});
172
+ forceUpdate();
173
173
  }, 10);
174
174
  return () => clearTimeout(timer);
175
- }, [showFilePicker]);
175
+ }, [showFilePicker, forceUpdate]);
176
176
  // Handle terminal width changes with debounce (like gemini-cli)
177
177
  useEffect(() => {
178
178
  // Skip on initial mount
@@ -183,17 +183,22 @@ export default function ChatInput({ onSubmit, onCommand, placeholder = 'Type you
183
183
  prevTerminalWidthRef.current = terminalWidth;
184
184
  // Debounce the re-render to avoid flickering during resize
185
185
  const timer = setTimeout(() => {
186
- forceUpdate({});
186
+ forceUpdate();
187
187
  }, 100);
188
188
  return () => clearTimeout(timer);
189
- }, [terminalWidth]);
189
+ }, [terminalWidth, forceUpdate]);
190
190
  // Notify parent of context percentage changes
191
+ const lastPercentageRef = useRef(0);
191
192
  useEffect(() => {
192
193
  if (contextUsage && onContextPercentageChange) {
193
194
  const percentage = calculateContextPercentage(contextUsage);
194
- onContextPercentageChange(percentage);
195
+ // Only call callback if percentage has actually changed
196
+ if (percentage !== lastPercentageRef.current) {
197
+ lastPercentageRef.current = percentage;
198
+ onContextPercentageChange(percentage);
199
+ }
195
200
  }
196
- }, [contextUsage]); // 移除 onContextPercentageChange 避免循环依赖
201
+ }, [contextUsage, onContextPercentageChange]);
197
202
  // Render cursor based on focus state
198
203
  const renderCursor = useCallback((char) => {
199
204
  if (hasFocus) {
@@ -6,7 +6,6 @@ export interface Message {
6
6
  streaming?: boolean;
7
7
  discontinued?: boolean;
8
8
  commandName?: string;
9
- showTodoTree?: boolean;
10
9
  files?: SelectedFile[];
11
10
  images?: Array<{
12
11
  type: 'image';
@@ -34,8 +34,7 @@ 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.files ||
38
- message.images) && (React.createElement(Box, { flexDirection: "column" },
37
+ (message.files || message.images) && (React.createElement(Box, { flexDirection: "column" },
39
38
  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
40
39
  ? `└─ [image #{fileIndex + 1}] ${file.path}`
41
40
  : `└─ Read \`${file.path}\`${file.exists
@@ -14,5 +14,5 @@ interface Props {
14
14
  allTools?: ToolCall[];
15
15
  onConfirm: (result: ConfirmationResult) => void;
16
16
  }
17
- export default function ToolConfirmation({ toolName, toolArguments, allTools, onConfirm }: Props): React.JSX.Element;
17
+ export default function ToolConfirmation({ toolName, toolArguments, allTools, onConfirm, }: Props): React.JSX.Element;
18
18
  export {};