snow-ai 0.3.22 → 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.
Files changed (35) hide show
  1. package/dist/api/gemini.d.ts +5 -1
  2. package/dist/api/gemini.js +30 -5
  3. package/dist/api/responses.js +18 -3
  4. package/dist/hooks/useConversation.d.ts +0 -5
  5. package/dist/hooks/useConversation.js +109 -56
  6. package/dist/hooks/useFilePicker.d.ts +1 -1
  7. package/dist/hooks/useFilePicker.js +13 -7
  8. package/dist/hooks/useHistoryNavigation.js +14 -7
  9. package/dist/hooks/useInputBuffer.d.ts +1 -1
  10. package/dist/hooks/useInputBuffer.js +22 -6
  11. package/dist/hooks/useStreamingState.js +2 -2
  12. package/dist/hooks/useVSCodeState.js +23 -6
  13. package/dist/mcp/filesystem.js +1 -1
  14. package/dist/ui/components/ChatInput.js +17 -11
  15. package/dist/ui/components/MessageList.d.ts +0 -1
  16. package/dist/ui/components/MessageList.js +1 -2
  17. package/dist/ui/components/SessionListPanel.js +12 -8
  18. package/dist/ui/components/SessionListScreen.js +2 -1
  19. package/dist/ui/components/ToolConfirmation.d.ts +1 -1
  20. package/dist/ui/components/ToolConfirmation.js +63 -22
  21. package/dist/ui/components/ToolResultPreview.js +33 -6
  22. package/dist/ui/pages/ChatScreen.js +21 -17
  23. package/dist/ui/pages/ConfigScreen.js +167 -16
  24. package/dist/ui/pages/HeadlessModeScreen.js +0 -1
  25. package/dist/ui/pages/ProxyConfigScreen.d.ts +1 -1
  26. package/dist/ui/pages/ProxyConfigScreen.js +6 -6
  27. package/dist/ui/pages/SensitiveCommandConfigScreen.d.ts +7 -0
  28. package/dist/ui/pages/SensitiveCommandConfigScreen.js +262 -0
  29. package/dist/ui/pages/SubAgentConfigScreen.js +1 -1
  30. package/dist/ui/pages/WelcomeScreen.js +14 -3
  31. package/dist/utils/apiConfig.d.ts +10 -0
  32. package/dist/utils/sensitiveCommandManager.d.ts +53 -0
  33. package/dist/utils/sensitiveCommandManager.js +308 -0
  34. package/dist/utils/sessionConverter.js +16 -11
  35. package/package.json +4 -2
@@ -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,15 +183,20 @@ 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
201
  }, [contextUsage, onContextPercentageChange]);
197
202
  // Render cursor based on focus state
@@ -223,7 +228,7 @@ export default function ChatInput({ onSubmit, onCommand, placeholder = 'Type you
223
228
  renderCursor(' '),
224
229
  React.createElement(Text, { color: disabled ? 'darkGray' : 'gray', dimColor: true }, disabled ? 'Waiting for response...' : placeholder)));
225
230
  }
226
- }, [buffer, disabled, placeholder, renderCursor, buffer.text]);
231
+ }, [buffer, disabled, placeholder, renderCursor]); // 移除 buffer.text 避免循环依赖,buffer 变化时会自然触发重渲染
227
232
  return (React.createElement(Box, { flexDirection: "column", paddingX: 1, width: terminalWidth },
228
233
  showHistoryMenu && (React.createElement(Box, { flexDirection: "column", marginBottom: 1, width: terminalWidth - 2 },
229
234
  React.createElement(Box, { flexDirection: "column" }, (() => {
@@ -248,8 +253,9 @@ export default function ChatInput({ onSubmit, onCommand, placeholder = 'Type you
248
253
  " more above...")) : (React.createElement(Text, null, " "))),
249
254
  visibleMessages.map((message, displayIndex) => {
250
255
  const actualIndex = startIndex + displayIndex;
251
- // Remove all newlines and extra spaces from label to ensure single line
256
+ // Ensure single line by removing all newlines and control characters
252
257
  const singleLineLabel = message.label
258
+ .replace(/[\r\n\t\v\f\u0000-\u001F\u007F-\u009F]+/g, ' ')
253
259
  .replace(/\s+/g, ' ')
254
260
  .trim();
255
261
  // Calculate available width for the message
@@ -261,7 +267,7 @@ export default function ChatInput({ onSubmit, onCommand, placeholder = 'Type you
261
267
  return (React.createElement(Box, { key: message.value, height: 1 },
262
268
  React.createElement(Text, { color: actualIndex === historySelectedIndex
263
269
  ? 'green'
264
- : 'white', bold: true },
270
+ : 'white', bold: true, wrap: "truncate" },
265
271
  actualIndex === historySelectedIndex ? '❯ ' : ' ',
266
272
  truncatedLabel)));
267
273
  }),
@@ -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
@@ -1,6 +1,6 @@
1
1
  import React, { useState, useEffect, useCallback } from 'react';
2
2
  import { Box, Text, useInput } from 'ink';
3
- import { sessionManager } from '../../utils/sessionManager.js';
3
+ import { sessionManager, } from '../../utils/sessionManager.js';
4
4
  export default function SessionListPanel({ onSelectSession, onClose }) {
5
5
  const [sessions, setSessions] = useState([]);
6
6
  const [loading, setLoading] = useState(true);
@@ -140,32 +140,36 @@ export default function SessionListPanel({ onSelectSession, onClose }) {
140
140
  sessions.length,
141
141
  ")",
142
142
  currentSession && ` • ${currentSession.messageCount} msgs`,
143
- markedSessions.size > 0 && React.createElement(Text, { color: "yellow" },
143
+ markedSessions.size > 0 && (React.createElement(Text, { color: "yellow" },
144
144
  " \u2022 ",
145
145
  markedSessions.size,
146
- " marked")),
146
+ " marked"))),
147
147
  React.createElement(Text, { color: "gray", dimColor: true }, "\u2191\u2193 navigate \u2022 Space mark \u2022 D delete \u2022 Enter select \u2022 ESC close")),
148
148
  hasPrevious && (React.createElement(Text, { color: "gray", dimColor: true },
149
- " \u2191 ",
149
+ ' ',
150
+ "\u2191 ",
150
151
  scrollOffset,
151
152
  " more above")),
152
153
  visibleSessions.map((session, index) => {
153
154
  const actualIndex = scrollOffset + index;
154
155
  const isSelected = actualIndex === selectedIndex;
155
156
  const isMarked = markedSessions.has(session.id);
156
- const title = session.title || 'Untitled';
157
+ // Remove newlines and other whitespace characters from title
158
+ const cleanTitle = (session.title || 'Untitled').replace(/[\r\n\t]+/g, ' ');
157
159
  const timeStr = formatDate(session.updatedAt);
158
- const truncatedLabel = title.length > 50 ? title.slice(0, 47) + '...' : title;
160
+ const truncatedLabel = cleanTitle.length > 50 ? cleanTitle.slice(0, 47) + '...' : cleanTitle;
159
161
  return (React.createElement(Box, { key: session.id },
160
162
  React.createElement(Text, { color: isMarked ? 'green' : 'gray' }, isMarked ? '✔ ' : ' '),
161
163
  React.createElement(Text, { color: isSelected ? 'green' : 'gray' }, isSelected ? '❯ ' : ' '),
162
164
  React.createElement(Text, { color: isSelected ? 'cyan' : isMarked ? 'green' : 'white' }, truncatedLabel),
163
165
  React.createElement(Text, { color: "gray", dimColor: true },
164
- " \u2022 ",
166
+ ' ',
167
+ "\u2022 ",
165
168
  timeStr)));
166
169
  }),
167
170
  hasMore && (React.createElement(Text, { color: "gray", dimColor: true },
168
- " \u2193 ",
171
+ ' ',
172
+ "\u2193 ",
169
173
  sessions.length - scrollOffset - VISIBLE_ITEMS,
170
174
  " more below"))));
171
175
  }
@@ -58,7 +58,8 @@ export default function SessionListScreen({ onBack, onSelectSession }) {
58
58
  const maxLabelWidth = Math.max(30, terminalWidth - reservedSpace);
59
59
  return sessions.map(session => {
60
60
  const timeString = formatDate(session.updatedAt);
61
- const title = session.title || 'Untitled';
61
+ // Remove newlines and other whitespace characters from title
62
+ const title = (session.title || 'Untitled').replace(/[\r\n\t]+/g, ' ');
62
63
  // Format: "Title • 5 msgs • 2h ago"
63
64
  const messageInfo = `${session.messageCount} msg${session.messageCount !== 1 ? 's' : ''}`;
64
65
  const fullLabel = `${title} • ${messageInfo} • ${timeString}`;
@@ -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 {};
@@ -1,6 +1,7 @@
1
1
  import React, { useState, useMemo } from 'react';
2
2
  import { Box, Text } from 'ink';
3
3
  import SelectInput from 'ink-select-input';
4
+ import { isSensitiveCommand } from '../../utils/sensitiveCommandManager.js';
4
5
  // Helper function to format argument values with truncation
5
6
  function formatArgumentValue(value, maxLength = 100) {
6
7
  if (value === null || value === undefined) {
@@ -35,11 +36,28 @@ function formatArgumentsAsTree(args, toolName) {
35
36
  return keys.map((key, index) => ({
36
37
  key,
37
38
  value: formatArgumentValue(args[key]),
38
- isLast: index === keys.length - 1
39
+ isLast: index === keys.length - 1,
39
40
  }));
40
41
  }
41
- export default function ToolConfirmation({ toolName, toolArguments, allTools, onConfirm }) {
42
+ export default function ToolConfirmation({ toolName, toolArguments, allTools, onConfirm, }) {
42
43
  const [hasSelected, setHasSelected] = useState(false);
44
+ // Check if this is a sensitive command (for terminal-execute)
45
+ const sensitiveCommandCheck = useMemo(() => {
46
+ if (toolName !== 'terminal-execute' || !toolArguments) {
47
+ return { isSensitive: false };
48
+ }
49
+ try {
50
+ const parsed = JSON.parse(toolArguments);
51
+ const command = parsed.command;
52
+ if (command && typeof command === 'string') {
53
+ return isSensitiveCommand(command);
54
+ }
55
+ }
56
+ catch {
57
+ // Ignore parse errors
58
+ }
59
+ return { isSensitive: false };
60
+ }, [toolName, toolArguments]);
43
61
  // Parse and format tool arguments for display (single tool)
44
62
  const formattedArgs = useMemo(() => {
45
63
  if (!toolArguments)
@@ -61,31 +79,38 @@ export default function ToolConfirmation({ toolName, toolArguments, allTools, on
61
79
  const parsed = JSON.parse(tool.function.arguments);
62
80
  return {
63
81
  name: tool.function.name,
64
- args: formatArgumentsAsTree(parsed, tool.function.name)
82
+ args: formatArgumentsAsTree(parsed, tool.function.name),
65
83
  };
66
84
  }
67
85
  catch {
68
86
  return {
69
87
  name: tool.function.name,
70
- args: []
88
+ args: [],
71
89
  };
72
90
  }
73
91
  });
74
92
  }, [allTools]);
75
- const items = [
76
- {
77
- label: 'Approve (once)',
78
- value: 'approve'
79
- },
80
- {
81
- label: 'Always approve this tool',
82
- value: 'approve_always'
83
- },
84
- {
85
- label: 'Reject (end session)',
86
- value: 'reject'
93
+ // Conditionally show "Always approve" based on sensitive command check
94
+ const items = useMemo(() => {
95
+ const baseItems = [
96
+ {
97
+ label: 'Approve (once)',
98
+ value: 'approve',
99
+ },
100
+ ];
101
+ // Only show "Always approve" if NOT a sensitive command
102
+ if (!sensitiveCommandCheck.isSensitive) {
103
+ baseItems.push({
104
+ label: 'Always approve this tool',
105
+ value: 'approve_always',
106
+ });
87
107
  }
88
- ];
108
+ baseItems.push({
109
+ label: 'Reject (end session)',
110
+ value: 'reject',
111
+ });
112
+ return baseItems;
113
+ }, [sensitiveCommandCheck.isSensitive]);
89
114
  const handleSelect = (item) => {
90
115
  if (!hasSelected) {
91
116
  setHasSelected(true);
@@ -98,8 +123,21 @@ export default function ToolConfirmation({ toolName, toolArguments, allTools, on
98
123
  !formattedAllTools && (React.createElement(React.Fragment, null,
99
124
  React.createElement(Box, { marginBottom: 1 },
100
125
  React.createElement(Text, null,
101
- "Tool: ",
126
+ "Tool:",
127
+ ' ',
102
128
  React.createElement(Text, { bold: true, color: "cyan" }, toolName))),
129
+ sensitiveCommandCheck.isSensitive && (React.createElement(Box, { flexDirection: "column", marginBottom: 1 },
130
+ React.createElement(Box, { marginBottom: 1 },
131
+ React.createElement(Text, { bold: true, color: "red" }, "SENSITIVE COMMAND DETECTED")),
132
+ React.createElement(Box, { flexDirection: "column", gap: 0 },
133
+ React.createElement(Box, null,
134
+ React.createElement(Text, { dimColor: true }, "Pattern: "),
135
+ React.createElement(Text, { color: "magenta", bold: true }, sensitiveCommandCheck.matchedCommand?.pattern)),
136
+ React.createElement(Box, { marginTop: 0 },
137
+ React.createElement(Text, { dimColor: true }, "Reason: "),
138
+ React.createElement(Text, { color: "white" }, sensitiveCommandCheck.matchedCommand?.description))),
139
+ React.createElement(Box, { marginTop: 1, paddingX: 1, paddingY: 0 },
140
+ React.createElement(Text, { color: "yellow", italic: true }, "This command requires confirmation even in YOLO/Always-Approved mode")))),
103
141
  formattedArgs && formattedArgs.length > 0 && (React.createElement(Box, { flexDirection: "column", marginBottom: 1 },
104
142
  React.createElement(Text, { dimColor: true }, "Arguments:"),
105
143
  formattedArgs.map((arg, index) => (React.createElement(Box, { key: index, flexDirection: "column" },
@@ -107,12 +145,14 @@ export default function ToolConfirmation({ toolName, toolArguments, allTools, on
107
145
  arg.isLast ? '└─' : '├─',
108
146
  " ",
109
147
  arg.key,
110
- ": ",
148
+ ":",
149
+ ' ',
111
150
  React.createElement(Text, { color: "white" }, arg.value))))))))),
112
151
  formattedAllTools && (React.createElement(Box, { flexDirection: "column", marginBottom: 1 },
113
152
  React.createElement(Box, { marginBottom: 1 },
114
153
  React.createElement(Text, null,
115
- "Tools: ",
154
+ "Tools:",
155
+ ' ',
116
156
  React.createElement(Text, { bold: true, color: "cyan" },
117
157
  formattedAllTools.length,
118
158
  " tools in parallel"))),
@@ -125,11 +165,12 @@ export default function ToolConfirmation({ toolName, toolArguments, allTools, on
125
165
  arg.isLast ? '└─' : '├─',
126
166
  " ",
127
167
  arg.key,
128
- ": ",
168
+ ":",
169
+ ' ',
129
170
  React.createElement(Text, { color: "white" }, arg.value))))))))))),
130
171
  React.createElement(Box, { marginBottom: 1 },
131
172
  React.createElement(Text, { dimColor: true }, "Select action:")),
132
- !hasSelected && (React.createElement(SelectInput, { items: items, onSelect: handleSelect })),
173
+ !hasSelected && React.createElement(SelectInput, { items: items, onSelect: handleSelect }),
133
174
  hasSelected && (React.createElement(Box, null,
134
175
  React.createElement(Text, { color: "green" }, "Confirmed")))));
135
176
  }
@@ -1,5 +1,6 @@
1
1
  import React from 'react';
2
2
  import { Box, Text } from 'ink';
3
+ import TodoTree from './TodoTree.js';
3
4
  /**
4
5
  * Display a compact preview of tool execution results
5
6
  * Shows a tree-like structure with limited content
@@ -36,6 +37,9 @@ export default function ToolResultPreview({ toolName, result, maxLines = 5, }) {
36
37
  else if (toolName.startsWith('ace-')) {
37
38
  return renderACEPreview(toolName, data, maxLines);
38
39
  }
40
+ else if (toolName.startsWith('todo-')) {
41
+ return renderTodoPreview(toolName, data, maxLines);
42
+ }
39
43
  else {
40
44
  // Generic preview for unknown tools
41
45
  return renderGenericPreview(data, maxLines);
@@ -56,7 +60,7 @@ function renderSubAgentPreview(data, _maxLines) {
56
60
  React.createElement(Text, { color: "gray", dimColor: true },
57
61
  "\u2514\u2500 Sub-agent completed (",
58
62
  lines.length,
59
- " ",
63
+ ' ',
60
64
  lines.length === 1 ? 'line' : 'lines',
61
65
  " output)")));
62
66
  }
@@ -141,7 +145,7 @@ function renderACEPreview(toolName, data, maxLines) {
141
145
  React.createElement(Text, { color: "gray", dimColor: true },
142
146
  "\u2514\u2500 Found ",
143
147
  symbols.length,
144
- " ",
148
+ ' ',
145
149
  symbols.length === 1 ? 'symbol' : 'symbols')));
146
150
  }
147
151
  // Handle ace-find-references results
@@ -156,7 +160,7 @@ function renderACEPreview(toolName, data, maxLines) {
156
160
  React.createElement(Text, { color: "gray", dimColor: true },
157
161
  "\u2514\u2500 Found ",
158
162
  references.length,
159
- " ",
163
+ ' ',
160
164
  references.length === 1 ? 'reference' : 'references')));
161
165
  }
162
166
  // Handle ace-find-definition result
@@ -188,7 +192,7 @@ function renderACEPreview(toolName, data, maxLines) {
188
192
  React.createElement(Text, { color: "gray", dimColor: true },
189
193
  "\u2514\u2500 Found ",
190
194
  symbols.length,
191
- " ",
195
+ ' ',
192
196
  symbols.length === 1 ? 'symbol' : 'symbols',
193
197
  " in file")));
194
198
  }
@@ -204,12 +208,12 @@ function renderACEPreview(toolName, data, maxLines) {
204
208
  React.createElement(Text, { color: "gray", dimColor: true },
205
209
  "\u251C\u2500 ",
206
210
  data.symbols?.length || 0,
207
- " ",
211
+ ' ',
208
212
  (data.symbols?.length || 0) === 1 ? 'symbol' : 'symbols'),
209
213
  React.createElement(Text, { color: "gray", dimColor: true },
210
214
  "\u2514\u2500 ",
211
215
  data.references?.length || 0,
212
- " ",
216
+ ' ',
213
217
  (data.references?.length || 0) === 1 ? 'reference' : 'references')));
214
218
  }
215
219
  // Generic ACE tool preview
@@ -278,3 +282,26 @@ function renderGenericPreview(data, maxLines) {
278
282
  valueStr));
279
283
  })));
280
284
  }
285
+ function renderTodoPreview(_toolName, data, _maxLines) {
286
+ // Handle todo-create, todo-get, todo-update, todo-add, todo-delete
287
+ // Debug: Check if data is actually the stringified result that needs parsing again
288
+ // Some tools might return the result wrapped in content[0].text
289
+ let todoData = data;
290
+ // If data has content array (MCP format), extract the text
291
+ if (data.content && Array.isArray(data.content) && data.content[0]?.text) {
292
+ try {
293
+ todoData = JSON.parse(data.content[0].text);
294
+ }
295
+ catch (e) {
296
+ // If parsing fails, just use original data
297
+ }
298
+ }
299
+ if (!todoData.todos) {
300
+ return (React.createElement(Box, { marginLeft: 2 },
301
+ React.createElement(Text, { color: "gray", dimColor: true },
302
+ "\u2514\u2500 ",
303
+ todoData.message || 'No TODO list')));
304
+ }
305
+ // Use the TodoTree component to display the TODO list
306
+ return React.createElement(TodoTree, { todos: todoData.todos });
307
+ }
@@ -13,7 +13,6 @@ import MarkdownRenderer from '../components/MarkdownRenderer.js';
13
13
  import ToolConfirmation from '../components/ToolConfirmation.js';
14
14
  import DiffViewer from '../components/DiffViewer.js';
15
15
  import ToolResultPreview from '../components/ToolResultPreview.js';
16
- import TodoTree from '../components/TodoTree.js';
17
16
  import FileRollbackConfirmation from '../components/FileRollbackConfirmation.js';
18
17
  import ShimmerText from '../components/ShimmerText.js';
19
18
  import { getOpenAiConfig } from '../../utils/apiConfig.js';
@@ -50,7 +49,6 @@ import '../../utils/commands/todoPicker.js';
50
49
  export default function ChatScreen({ skipWelcome }) {
51
50
  const [messages, setMessages] = useState([]);
52
51
  const [isSaving] = useState(false);
53
- const [currentTodos, setCurrentTodos] = useState([]);
54
52
  const [pendingMessages, setPendingMessages] = useState([]);
55
53
  const pendingMessagesRef = useRef([]);
56
54
  const hasAttemptedAutoVscodeConnect = useRef(false);
@@ -157,7 +155,7 @@ export default function ChatScreen({ skipWelcome }) {
157
155
  return () => {
158
156
  clearTimeout(handler);
159
157
  };
160
- }, [terminalWidth, stdout]);
158
+ }, [terminalWidth]); // stdout 对象可能在每次渲染时变化,移除以避免循环
161
159
  // Reload messages from session when remountKey changes (to restore sub-agent messages)
162
160
  useEffect(() => {
163
161
  if (remountKey === 0)
@@ -361,18 +359,26 @@ export default function ChatScreen({ skipWelcome }) {
361
359
  return;
362
360
  }
363
361
  }
364
- // Find the corresponding user message in session to delete
365
- // We start from the end and count backwards
366
- let sessionUserMessageCount = 0;
362
+ // Special case: if rolling back to index 0 (first message), always delete entire session
363
+ // This handles the case where user interrupts the first conversation
367
364
  let sessionTruncateIndex = currentSession.messages.length;
368
- for (let i = currentSession.messages.length - 1; i >= 0; i--) {
369
- const msg = currentSession.messages[i];
370
- if (msg && msg.role === 'user') {
371
- sessionUserMessageCount++;
372
- if (sessionUserMessageCount === uiUserMessagesToDelete) {
373
- // We want to delete from this user message onwards
374
- sessionTruncateIndex = i;
375
- break;
365
+ if (selectedIndex === 0) {
366
+ // Rolling back to the very first message means deleting entire session
367
+ sessionTruncateIndex = 0;
368
+ }
369
+ else {
370
+ // Find the corresponding user message in session to delete
371
+ // We start from the end and count backwards
372
+ let sessionUserMessageCount = 0;
373
+ for (let i = currentSession.messages.length - 1; i >= 0; i--) {
374
+ const msg = currentSession.messages[i];
375
+ if (msg && msg.role === 'user') {
376
+ sessionUserMessageCount++;
377
+ if (sessionUserMessageCount === uiUserMessagesToDelete) {
378
+ // We want to delete from this user message onwards
379
+ sessionTruncateIndex = i;
380
+ break;
381
+ }
376
382
  }
377
383
  }
378
384
  }
@@ -562,7 +568,6 @@ export default function ChatScreen({ skipWelcome }) {
562
568
  saveMessage,
563
569
  setMessages,
564
570
  setStreamTokenCount: streamingState.setStreamTokenCount,
565
- setCurrentTodos,
566
571
  requestToolConfirmation,
567
572
  isToolAutoApproved,
568
573
  addMultipleToAlwaysApproved,
@@ -746,7 +751,6 @@ export default function ChatScreen({ skipWelcome }) {
746
751
  saveMessage,
747
752
  setMessages,
748
753
  setStreamTokenCount: streamingState.setStreamTokenCount,
749
- setCurrentTodos,
750
754
  requestToolConfirmation,
751
755
  isToolAutoApproved,
752
756
  addMultipleToAlwaysApproved,
@@ -931,7 +935,7 @@ export default function ChatScreen({ skipWelcome }) {
931
935
  React.createElement(Text, { color: "gray", dimColor: true },
932
936
  "\u2514\u2500 ",
933
937
  message.commandName),
934
- message.content && (React.createElement(Text, { color: "white" }, message.content)))) : message.showTodoTree ? (React.createElement(TodoTree, { todos: currentTodos })) : (React.createElement(React.Fragment, null,
938
+ message.content && (React.createElement(Text, { color: "white" }, message.content)))) : (React.createElement(React.Fragment, null,
935
939
  message.role === 'user' || isToolMessage ? (React.createElement(Text, { color: message.role === 'user'
936
940
  ? 'gray'
937
941
  : message.content.startsWith('⚡')