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.
- package/dist/api/gemini.d.ts +5 -1
- package/dist/api/gemini.js +30 -5
- package/dist/api/responses.js +18 -3
- package/dist/hooks/useConversation.d.ts +0 -5
- package/dist/hooks/useConversation.js +109 -56
- package/dist/hooks/useFilePicker.d.ts +1 -1
- package/dist/hooks/useFilePicker.js +13 -7
- package/dist/hooks/useHistoryNavigation.js +14 -7
- package/dist/hooks/useInputBuffer.d.ts +1 -1
- package/dist/hooks/useInputBuffer.js +22 -6
- package/dist/hooks/useStreamingState.js +2 -2
- package/dist/hooks/useVSCodeState.js +23 -6
- package/dist/mcp/filesystem.js +1 -1
- package/dist/ui/components/ChatInput.js +17 -11
- package/dist/ui/components/MessageList.d.ts +0 -1
- package/dist/ui/components/MessageList.js +1 -2
- package/dist/ui/components/SessionListPanel.js +12 -8
- package/dist/ui/components/SessionListScreen.js +2 -1
- package/dist/ui/components/ToolConfirmation.d.ts +1 -1
- package/dist/ui/components/ToolConfirmation.js +63 -22
- package/dist/ui/components/ToolResultPreview.js +33 -6
- package/dist/ui/pages/ChatScreen.js +21 -17
- package/dist/ui/pages/ConfigScreen.js +167 -16
- package/dist/ui/pages/HeadlessModeScreen.js +0 -1
- package/dist/ui/pages/ProxyConfigScreen.d.ts +1 -1
- package/dist/ui/pages/ProxyConfigScreen.js +6 -6
- package/dist/ui/pages/SensitiveCommandConfigScreen.d.ts +7 -0
- package/dist/ui/pages/SensitiveCommandConfigScreen.js +262 -0
- package/dist/ui/pages/SubAgentConfigScreen.js +1 -1
- package/dist/ui/pages/WelcomeScreen.js +14 -3
- package/dist/utils/apiConfig.d.ts +10 -0
- package/dist/utils/sensitiveCommandManager.d.ts +53 -0
- package/dist/utils/sensitiveCommandManager.js +308 -0
- package/dist/utils/sessionConverter.js +16 -11
- package/package.json +4 -2
package/dist/mcp/filesystem.js
CHANGED
|
@@ -1033,7 +1033,7 @@ export const mcpTools = [
|
|
|
1033
1033
|
},
|
|
1034
1034
|
{
|
|
1035
1035
|
name: 'filesystem-create',
|
|
1036
|
-
description: '
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
}),
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
166
|
+
' ',
|
|
167
|
+
"\u2022 ",
|
|
165
168
|
timeStr)));
|
|
166
169
|
}),
|
|
167
170
|
hasMore && (React.createElement(Text, { color: "gray", dimColor: true },
|
|
168
|
-
|
|
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
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
{
|
|
85
|
-
|
|
86
|
-
|
|
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 &&
|
|
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
|
|
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
|
-
//
|
|
365
|
-
//
|
|
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
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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)))) :
|
|
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('⚡')
|