snow-ai 0.3.13 → 0.3.14
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/responses.d.ts +1 -1
- package/dist/api/responses.js +4 -1
- package/dist/api/systemPrompt.js +7 -0
- package/dist/hooks/useConversation.js +35 -8
- package/dist/hooks/useToolConfirmation.js +22 -11
- package/dist/mcp/subagent.d.ts +1 -0
- package/dist/mcp/subagent.js +2 -2
- package/dist/ui/pages/ChatScreen.js +183 -79
- package/dist/utils/commands/ide.js +6 -10
- package/dist/utils/subAgentExecutor.d.ts +4 -1
- package/dist/utils/subAgentExecutor.js +16 -5
- package/dist/utils/toolExecutor.d.ts +5 -2
- package/dist/utils/toolExecutor.js +4 -3
- package/package.json +1 -1
package/dist/api/responses.d.ts
CHANGED
package/dist/api/responses.js
CHANGED
|
@@ -246,7 +246,10 @@ export async function* createStreamingResponse(options, abortSignal, onRetry) {
|
|
|
246
246
|
tools: convertToolsForResponses(options.tools),
|
|
247
247
|
tool_choice: options.tool_choice,
|
|
248
248
|
parallel_tool_calls: false,
|
|
249
|
-
|
|
249
|
+
// Only add reasoning if not explicitly disabled (null means don't pass it)
|
|
250
|
+
...(options.reasoning !== null && {
|
|
251
|
+
reasoning: options.reasoning || { effort: 'high', summary: 'auto' },
|
|
252
|
+
}),
|
|
250
253
|
store: false,
|
|
251
254
|
stream: true,
|
|
252
255
|
prompt_cache_key: options.prompt_cache_key,
|
package/dist/api/systemPrompt.js
CHANGED
|
@@ -121,6 +121,13 @@ and other shell features. Your capabilities include text processing, data filter
|
|
|
121
121
|
manipulation, workflow automation, and complex command chaining to solve sophisticated
|
|
122
122
|
system administration and data processing challenges.
|
|
123
123
|
|
|
124
|
+
**Sub-Agent:**
|
|
125
|
+
A sub-agent is a separate session isolated from the main session, and a sub-agent may have some of the tools described above to focus on solving a specific problem.
|
|
126
|
+
If you have a sub-agent tool, then you can leave some of the work to the sub-agent to solve.
|
|
127
|
+
For example, if you have a sub-agent of a work plan, you can hand over the work plan to the sub-agent to solve when you receive user requirements.
|
|
128
|
+
This way, the master agent can focus on task fulfillment.
|
|
129
|
+
*If you don't have a sub-agent tool, ignore this feature*
|
|
130
|
+
|
|
124
131
|
## 🔍 Quality Assurance
|
|
125
132
|
|
|
126
133
|
Guidance and recommendations:
|
|
@@ -21,6 +21,10 @@ 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
23
|
saveMessage, setMessages, setStreamTokenCount, setCurrentTodos, requestToolConfirmation, isToolAutoApproved, addMultipleToAlwaysApproved, yoloMode, setContextUsage, setIsReasoning, setRetryStatus, } = options;
|
|
24
|
+
// Create a wrapper function for adding single tool to always-approved list
|
|
25
|
+
const addToAlwaysApproved = (toolName) => {
|
|
26
|
+
addMultipleToAlwaysApproved([toolName]);
|
|
27
|
+
};
|
|
24
28
|
// Step 1: Ensure session exists and get existing TODOs
|
|
25
29
|
let currentSession = sessionManager.getCurrentSession();
|
|
26
30
|
if (!currentSession) {
|
|
@@ -64,13 +68,18 @@ export async function handleConversationWithTools(options) {
|
|
|
64
68
|
images: imageContents,
|
|
65
69
|
});
|
|
66
70
|
// Save user message (directly save API format message)
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
71
|
+
// IMPORTANT: await to ensure message is saved before continuing
|
|
72
|
+
// This prevents loss of user message if conversation is interrupted (ESC)
|
|
73
|
+
try {
|
|
74
|
+
await saveMessage({
|
|
75
|
+
role: 'user',
|
|
76
|
+
content: userContent,
|
|
77
|
+
images: imageContents,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
72
81
|
console.error('Failed to save user message:', error);
|
|
73
|
-
}
|
|
82
|
+
}
|
|
74
83
|
// Initialize token encoder with proper cleanup tracking
|
|
75
84
|
let encoder;
|
|
76
85
|
let encoderFreed = false;
|
|
@@ -142,6 +151,8 @@ export async function handleConversationWithTools(options) {
|
|
|
142
151
|
max_tokens: config.maxTokens || 4096,
|
|
143
152
|
tools: mcpTools.length > 0 ? mcpTools : undefined,
|
|
144
153
|
sessionId: currentSession?.id,
|
|
154
|
+
// Disable thinking for basicModel (e.g., init command)
|
|
155
|
+
disableThinking: options.useBasicModel,
|
|
145
156
|
}, controller.signal, onRetry)
|
|
146
157
|
: config.requestMethod === 'gemini'
|
|
147
158
|
? createStreamingGeminiCompletion({
|
|
@@ -158,6 +169,9 @@ export async function handleConversationWithTools(options) {
|
|
|
158
169
|
tools: mcpTools.length > 0 ? mcpTools : undefined,
|
|
159
170
|
tool_choice: 'auto',
|
|
160
171
|
prompt_cache_key: cacheKey, // Use session ID as cache key
|
|
172
|
+
// Don't pass reasoning for basicModel (small models may not support it)
|
|
173
|
+
// Pass null to explicitly disable reasoning in API call
|
|
174
|
+
reasoning: options.useBasicModel ? null : undefined,
|
|
161
175
|
}, controller.signal, onRetry)
|
|
162
176
|
: createStreamingChatCompletion({
|
|
163
177
|
model,
|
|
@@ -411,6 +425,8 @@ export async function handleConversationWithTools(options) {
|
|
|
411
425
|
approvedTools.push(...toolsNeedingConfirmation);
|
|
412
426
|
}
|
|
413
427
|
// Execute approved tools with sub-agent message callback and terminal output callback
|
|
428
|
+
// Track sub-agent content for token counting
|
|
429
|
+
let subAgentContentAccumulator = '';
|
|
414
430
|
const toolResults = await executeToolCalls(approvedTools, controller.signal, setStreamTokenCount, async (subAgentMessage) => {
|
|
415
431
|
// Handle sub-agent messages - display and save to session
|
|
416
432
|
setMessages(prev => {
|
|
@@ -524,9 +540,20 @@ export async function handleConversationWithTools(options) {
|
|
|
524
540
|
let content = '';
|
|
525
541
|
if (subAgentMessage.message.type === 'content') {
|
|
526
542
|
content = subAgentMessage.message.content;
|
|
543
|
+
// Update token count for sub-agent content
|
|
544
|
+
subAgentContentAccumulator += content;
|
|
545
|
+
try {
|
|
546
|
+
const tokens = encoder.encode(subAgentContentAccumulator);
|
|
547
|
+
setStreamTokenCount(tokens.length);
|
|
548
|
+
}
|
|
549
|
+
catch (e) {
|
|
550
|
+
// Ignore encoding errors
|
|
551
|
+
}
|
|
527
552
|
}
|
|
528
553
|
else if (subAgentMessage.message.type === 'done') {
|
|
529
|
-
// Mark as complete
|
|
554
|
+
// Mark as complete and reset token counter
|
|
555
|
+
subAgentContentAccumulator = '';
|
|
556
|
+
setStreamTokenCount(0);
|
|
530
557
|
if (existingIndex !== -1) {
|
|
531
558
|
const updated = [...prev];
|
|
532
559
|
const existing = updated[existingIndex];
|
|
@@ -574,7 +601,7 @@ export async function handleConversationWithTools(options) {
|
|
|
574
601
|
}
|
|
575
602
|
return prev;
|
|
576
603
|
});
|
|
577
|
-
}, requestToolConfirmation, isToolAutoApproved, yoloMode);
|
|
604
|
+
}, requestToolConfirmation, isToolAutoApproved, yoloMode, addToAlwaysApproved);
|
|
578
605
|
// Check if aborted during tool execution
|
|
579
606
|
if (controller.signal.aborted) {
|
|
580
607
|
freeEncoder();
|
|
@@ -1,15 +1,17 @@
|
|
|
1
|
-
import { useState } from 'react';
|
|
1
|
+
import { useState, useRef, useCallback } from 'react';
|
|
2
2
|
/**
|
|
3
3
|
* Hook for managing tool confirmation state and logic
|
|
4
4
|
*/
|
|
5
5
|
export function useToolConfirmation() {
|
|
6
6
|
const [pendingToolConfirmation, setPendingToolConfirmation] = useState(null);
|
|
7
|
+
// Use ref for always-approved tools to ensure closure functions always see latest state
|
|
8
|
+
const alwaysApprovedToolsRef = useRef(new Set());
|
|
7
9
|
const [alwaysApprovedTools, setAlwaysApprovedTools] = useState(new Set());
|
|
8
10
|
/**
|
|
9
11
|
* Request user confirmation for tool execution
|
|
10
12
|
*/
|
|
11
13
|
const requestToolConfirmation = async (toolCall, batchToolNames, allTools) => {
|
|
12
|
-
return new Promise(
|
|
14
|
+
return new Promise(resolve => {
|
|
13
15
|
setPendingToolConfirmation({
|
|
14
16
|
tool: toolCall,
|
|
15
17
|
batchToolNames,
|
|
@@ -17,34 +19,43 @@ export function useToolConfirmation() {
|
|
|
17
19
|
resolve: (result) => {
|
|
18
20
|
setPendingToolConfirmation(null);
|
|
19
21
|
resolve(result);
|
|
20
|
-
}
|
|
22
|
+
},
|
|
21
23
|
});
|
|
22
24
|
});
|
|
23
25
|
};
|
|
24
26
|
/**
|
|
25
27
|
* Check if a tool is auto-approved
|
|
28
|
+
* Uses ref to ensure it always sees the latest approved tools
|
|
26
29
|
*/
|
|
27
|
-
const isToolAutoApproved = (toolName) => {
|
|
28
|
-
return
|
|
29
|
-
|
|
30
|
+
const isToolAutoApproved = useCallback((toolName) => {
|
|
31
|
+
return (alwaysApprovedToolsRef.current.has(toolName) ||
|
|
32
|
+
toolName.startsWith('todo-') ||
|
|
33
|
+
toolName.startsWith('subagent-'));
|
|
34
|
+
}, []);
|
|
30
35
|
/**
|
|
31
36
|
* Add a tool to the always-approved list
|
|
32
37
|
*/
|
|
33
|
-
const addToAlwaysApproved = (toolName) => {
|
|
38
|
+
const addToAlwaysApproved = useCallback((toolName) => {
|
|
39
|
+
// Update ref immediately (for closure functions)
|
|
40
|
+
alwaysApprovedToolsRef.current.add(toolName);
|
|
41
|
+
// Update state (for UI reactivity)
|
|
34
42
|
setAlwaysApprovedTools(prev => new Set([...prev, toolName]));
|
|
35
|
-
};
|
|
43
|
+
}, []);
|
|
36
44
|
/**
|
|
37
45
|
* Add multiple tools to the always-approved list
|
|
38
46
|
*/
|
|
39
|
-
const addMultipleToAlwaysApproved = (toolNames) => {
|
|
47
|
+
const addMultipleToAlwaysApproved = useCallback((toolNames) => {
|
|
48
|
+
// Update ref immediately (for closure functions)
|
|
49
|
+
toolNames.forEach(name => alwaysApprovedToolsRef.current.add(name));
|
|
50
|
+
// Update state (for UI reactivity)
|
|
40
51
|
setAlwaysApprovedTools(prev => new Set([...prev, ...toolNames]));
|
|
41
|
-
};
|
|
52
|
+
}, []);
|
|
42
53
|
return {
|
|
43
54
|
pendingToolConfirmation,
|
|
44
55
|
alwaysApprovedTools,
|
|
45
56
|
requestToolConfirmation,
|
|
46
57
|
isToolAutoApproved,
|
|
47
58
|
addToAlwaysApproved,
|
|
48
|
-
addMultipleToAlwaysApproved
|
|
59
|
+
addMultipleToAlwaysApproved,
|
|
49
60
|
};
|
|
50
61
|
}
|
package/dist/mcp/subagent.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export interface SubAgentToolExecutionOptions {
|
|
|
8
8
|
requestToolConfirmation?: (toolCall: ToolCall, batchToolNames?: string, allTools?: ToolCall[]) => Promise<string>;
|
|
9
9
|
isToolAutoApproved?: (toolName: string) => boolean;
|
|
10
10
|
yoloMode?: boolean;
|
|
11
|
+
addToAlwaysApproved?: (toolName: string) => void;
|
|
11
12
|
}
|
|
12
13
|
/**
|
|
13
14
|
* Sub-Agent MCP Service
|
package/dist/mcp/subagent.js
CHANGED
|
@@ -9,7 +9,7 @@ export class SubAgentService {
|
|
|
9
9
|
* Execute a sub-agent as a tool
|
|
10
10
|
*/
|
|
11
11
|
async execute(options) {
|
|
12
|
-
const { agentId, prompt, onMessage, abortSignal, requestToolConfirmation, isToolAutoApproved, yoloMode, } = options;
|
|
12
|
+
const { agentId, prompt, onMessage, abortSignal, requestToolConfirmation, isToolAutoApproved, yoloMode, addToAlwaysApproved, } = options;
|
|
13
13
|
// Create a tool confirmation adapter for sub-agent if needed
|
|
14
14
|
const subAgentToolConfirmation = requestToolConfirmation
|
|
15
15
|
? async (toolName, toolArgs) => {
|
|
@@ -25,7 +25,7 @@ export class SubAgentService {
|
|
|
25
25
|
return await requestToolConfirmation(fakeToolCall);
|
|
26
26
|
}
|
|
27
27
|
: undefined;
|
|
28
|
-
const result = await executeSubAgent(agentId, prompt, onMessage, abortSignal, subAgentToolConfirmation, isToolAutoApproved, yoloMode);
|
|
28
|
+
const result = await executeSubAgent(agentId, prompt, onMessage, abortSignal, subAgentToolConfirmation, isToolAutoApproved, yoloMode, addToAlwaysApproved);
|
|
29
29
|
if (!result.success) {
|
|
30
30
|
throw new Error(result.error || 'Sub-agent execution failed');
|
|
31
31
|
}
|
|
@@ -52,6 +52,7 @@ export default function ChatScreen({ skipWelcome }) {
|
|
|
52
52
|
const [pendingMessages, setPendingMessages] = useState([]);
|
|
53
53
|
const pendingMessagesRef = useRef([]);
|
|
54
54
|
const hasAttemptedAutoVscodeConnect = useRef(false);
|
|
55
|
+
const userInterruptedRef = useRef(false); // Track if user manually interrupted via ESC
|
|
55
56
|
const [remountKey, setRemountKey] = useState(0);
|
|
56
57
|
const [showMcpInfo, setShowMcpInfo] = useState(false);
|
|
57
58
|
const [mcpPanelKey, setMcpPanelKey] = useState(0);
|
|
@@ -266,85 +267,16 @@ export default function ChatScreen({ skipWelcome }) {
|
|
|
266
267
|
if (key.escape &&
|
|
267
268
|
streamingState.isStreaming &&
|
|
268
269
|
streamingState.abortController) {
|
|
270
|
+
// Mark that user manually interrupted
|
|
271
|
+
userInterruptedRef.current = true;
|
|
269
272
|
// Abort the controller
|
|
270
273
|
streamingState.abortController.abort();
|
|
271
274
|
// Clear retry status immediately when user cancels
|
|
272
275
|
streamingState.setRetryStatus(null);
|
|
273
276
|
// Remove all pending tool call messages (those with toolPending: true)
|
|
274
277
|
setMessages(prev => prev.filter(msg => !msg.toolPending));
|
|
275
|
-
//
|
|
276
|
-
//
|
|
277
|
-
// 1. User message without AI response (scenario 1)
|
|
278
|
-
// 2. Assistant message with tool_calls but no tool results (scenario 2)
|
|
279
|
-
const session = sessionManager.getCurrentSession();
|
|
280
|
-
if (session && session.messages.length > 0) {
|
|
281
|
-
// Use async cleanup to avoid blocking UI
|
|
282
|
-
(async () => {
|
|
283
|
-
try {
|
|
284
|
-
// Find the last complete conversation round
|
|
285
|
-
const messages = session.messages;
|
|
286
|
-
let truncateIndex = messages.length;
|
|
287
|
-
// Scan from the end to find incomplete round
|
|
288
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
289
|
-
const msg = messages[i];
|
|
290
|
-
if (!msg)
|
|
291
|
-
continue;
|
|
292
|
-
// If last message is user message without assistant response, remove it
|
|
293
|
-
if (msg.role === 'user' && i === messages.length - 1) {
|
|
294
|
-
truncateIndex = i;
|
|
295
|
-
break;
|
|
296
|
-
}
|
|
297
|
-
// If assistant message has tool_calls, verify all tool results exist
|
|
298
|
-
if (msg.role === 'assistant' &&
|
|
299
|
-
msg.tool_calls &&
|
|
300
|
-
msg.tool_calls.length > 0) {
|
|
301
|
-
const toolCallIds = new Set(msg.tool_calls.map(tc => tc.id));
|
|
302
|
-
// Check if all tool results exist after this assistant message
|
|
303
|
-
for (let j = i + 1; j < messages.length; j++) {
|
|
304
|
-
const followMsg = messages[j];
|
|
305
|
-
if (followMsg &&
|
|
306
|
-
followMsg.role === 'tool' &&
|
|
307
|
-
followMsg.tool_call_id) {
|
|
308
|
-
toolCallIds.delete(followMsg.tool_call_id);
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
// If some tool results are missing, remove from this assistant message onwards
|
|
312
|
-
if (toolCallIds.size > 0) {
|
|
313
|
-
truncateIndex = i;
|
|
314
|
-
break;
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
// If we found a complete assistant response without tool calls, we're done
|
|
318
|
-
if (msg.role === 'assistant' && !msg.tool_calls) {
|
|
319
|
-
break;
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
// Truncate session if needed
|
|
323
|
-
if (truncateIndex < messages.length) {
|
|
324
|
-
await sessionManager.truncateMessages(truncateIndex);
|
|
325
|
-
// Also clear from saved messages tracking
|
|
326
|
-
clearSavedMessages();
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
catch (error) {
|
|
330
|
-
console.error('Failed to clean up incomplete conversation:', error);
|
|
331
|
-
}
|
|
332
|
-
})();
|
|
333
|
-
}
|
|
334
|
-
// Add discontinued message
|
|
335
|
-
setMessages(prev => [
|
|
336
|
-
...prev,
|
|
337
|
-
{
|
|
338
|
-
role: 'assistant',
|
|
339
|
-
content: '',
|
|
340
|
-
streaming: false,
|
|
341
|
-
discontinued: true,
|
|
342
|
-
},
|
|
343
|
-
]);
|
|
344
|
-
// Stop streaming state
|
|
345
|
-
streamingState.setIsStreaming(false);
|
|
346
|
-
streamingState.setAbortController(null);
|
|
347
|
-
streamingState.setStreamTokenCount(0);
|
|
278
|
+
// Note: discontinued message will be added in processMessage/processPendingMessages finally block
|
|
279
|
+
// Note: session cleanup will be handled in processMessage/processPendingMessages finally block
|
|
348
280
|
}
|
|
349
281
|
});
|
|
350
282
|
const handleHistorySelect = async (selectedIndex, message) => {
|
|
@@ -389,6 +321,30 @@ export default function ChatScreen({ skipWelcome }) {
|
|
|
389
321
|
const uiUserMessagesToDelete = messages
|
|
390
322
|
.slice(selectedIndex)
|
|
391
323
|
.filter(msg => msg.role === 'user').length;
|
|
324
|
+
// Check if the selected message is a user message that might not be in session
|
|
325
|
+
// (e.g., interrupted before AI response)
|
|
326
|
+
const selectedMessage = messages[selectedIndex];
|
|
327
|
+
const isUncommittedUserMessage = selectedMessage?.role === 'user' &&
|
|
328
|
+
uiUserMessagesToDelete === 1 &&
|
|
329
|
+
// Check if this is the last or second-to-last message (before discontinued)
|
|
330
|
+
(selectedIndex === messages.length - 1 ||
|
|
331
|
+
(selectedIndex === messages.length - 2 &&
|
|
332
|
+
messages[messages.length - 1]?.discontinued));
|
|
333
|
+
// If this is an uncommitted user message, just truncate UI and skip session modification
|
|
334
|
+
if (isUncommittedUserMessage) {
|
|
335
|
+
// Check if session ends with a complete assistant response
|
|
336
|
+
const lastSessionMsg = currentSession.messages[currentSession.messages.length - 1];
|
|
337
|
+
const sessionEndsWithAssistant = lastSessionMsg?.role === 'assistant' && !lastSessionMsg?.tool_calls;
|
|
338
|
+
if (sessionEndsWithAssistant) {
|
|
339
|
+
// Session is complete, this user message wasn't saved
|
|
340
|
+
// Just truncate UI, don't modify session
|
|
341
|
+
setMessages(prev => prev.slice(0, selectedIndex));
|
|
342
|
+
clearSavedMessages();
|
|
343
|
+
setRemountKey(prev => prev + 1);
|
|
344
|
+
snapshotState.setPendingRollback(null);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
392
348
|
// Find the corresponding user message in session to delete
|
|
393
349
|
// We start from the end and count backwards
|
|
394
350
|
let sessionUserMessageCount = 0;
|
|
@@ -629,6 +585,78 @@ export default function ChatScreen({ skipWelcome }) {
|
|
|
629
585
|
}
|
|
630
586
|
}
|
|
631
587
|
finally {
|
|
588
|
+
// Handle user interruption uniformly
|
|
589
|
+
if (userInterruptedRef.current) {
|
|
590
|
+
// Clean up incomplete conversation in session
|
|
591
|
+
const session = sessionManager.getCurrentSession();
|
|
592
|
+
if (session && session.messages.length > 0) {
|
|
593
|
+
(async () => {
|
|
594
|
+
try {
|
|
595
|
+
// Find the last complete conversation round
|
|
596
|
+
const messages = session.messages;
|
|
597
|
+
let truncateIndex = messages.length;
|
|
598
|
+
// Scan from the end to find incomplete round
|
|
599
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
600
|
+
const msg = messages[i];
|
|
601
|
+
if (!msg)
|
|
602
|
+
continue;
|
|
603
|
+
// If last message is user message without assistant response, remove it
|
|
604
|
+
// The user message was saved via await saveMessage() before interruption
|
|
605
|
+
// So it's safe to truncate it from session when incomplete
|
|
606
|
+
if (msg.role === 'user' && i === messages.length - 1) {
|
|
607
|
+
truncateIndex = i;
|
|
608
|
+
break;
|
|
609
|
+
}
|
|
610
|
+
// If assistant message has tool_calls, verify all tool results exist
|
|
611
|
+
if (msg.role === 'assistant' &&
|
|
612
|
+
msg.tool_calls &&
|
|
613
|
+
msg.tool_calls.length > 0) {
|
|
614
|
+
const toolCallIds = new Set(msg.tool_calls.map(tc => tc.id));
|
|
615
|
+
// Check if all tool results exist after this assistant message
|
|
616
|
+
for (let j = i + 1; j < messages.length; j++) {
|
|
617
|
+
const followMsg = messages[j];
|
|
618
|
+
if (followMsg &&
|
|
619
|
+
followMsg.role === 'tool' &&
|
|
620
|
+
followMsg.tool_call_id) {
|
|
621
|
+
toolCallIds.delete(followMsg.tool_call_id);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
// If some tool results are missing, remove from this assistant message onwards
|
|
625
|
+
if (toolCallIds.size > 0) {
|
|
626
|
+
truncateIndex = i;
|
|
627
|
+
break;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
// If we found a complete assistant response without tool calls, we're done
|
|
631
|
+
if (msg.role === 'assistant' && !msg.tool_calls) {
|
|
632
|
+
break;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
// Truncate session if needed
|
|
636
|
+
if (truncateIndex < messages.length) {
|
|
637
|
+
await sessionManager.truncateMessages(truncateIndex);
|
|
638
|
+
// Also clear from saved messages tracking
|
|
639
|
+
clearSavedMessages();
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
catch (error) {
|
|
643
|
+
console.error('Failed to clean up incomplete conversation:', error);
|
|
644
|
+
}
|
|
645
|
+
})();
|
|
646
|
+
}
|
|
647
|
+
// Add discontinued message after all processing is done
|
|
648
|
+
setMessages(prev => [
|
|
649
|
+
...prev,
|
|
650
|
+
{
|
|
651
|
+
role: 'assistant',
|
|
652
|
+
content: '',
|
|
653
|
+
streaming: false,
|
|
654
|
+
discontinued: true,
|
|
655
|
+
},
|
|
656
|
+
]);
|
|
657
|
+
// Reset interruption flag
|
|
658
|
+
userInterruptedRef.current = false;
|
|
659
|
+
}
|
|
632
660
|
// End streaming
|
|
633
661
|
streamingState.setIsStreaming(false);
|
|
634
662
|
streamingState.setAbortController(null);
|
|
@@ -720,6 +748,76 @@ export default function ChatScreen({ skipWelcome }) {
|
|
|
720
748
|
}
|
|
721
749
|
}
|
|
722
750
|
finally {
|
|
751
|
+
// Handle user interruption uniformly
|
|
752
|
+
if (userInterruptedRef.current) {
|
|
753
|
+
// Clean up incomplete conversation in session
|
|
754
|
+
const session = sessionManager.getCurrentSession();
|
|
755
|
+
if (session && session.messages.length > 0) {
|
|
756
|
+
(async () => {
|
|
757
|
+
try {
|
|
758
|
+
// Find the last complete conversation round
|
|
759
|
+
const messages = session.messages;
|
|
760
|
+
let truncateIndex = messages.length;
|
|
761
|
+
// Scan from the end to find incomplete round
|
|
762
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
763
|
+
const msg = messages[i];
|
|
764
|
+
if (!msg)
|
|
765
|
+
continue;
|
|
766
|
+
// If last message is user message without assistant response, remove it
|
|
767
|
+
if (msg.role === 'user' && i === messages.length - 1) {
|
|
768
|
+
truncateIndex = i;
|
|
769
|
+
break;
|
|
770
|
+
}
|
|
771
|
+
// If assistant message has tool_calls, verify all tool results exist
|
|
772
|
+
if (msg.role === 'assistant' &&
|
|
773
|
+
msg.tool_calls &&
|
|
774
|
+
msg.tool_calls.length > 0) {
|
|
775
|
+
const toolCallIds = new Set(msg.tool_calls.map(tc => tc.id));
|
|
776
|
+
// Check if all tool results exist after this assistant message
|
|
777
|
+
for (let j = i + 1; j < messages.length; j++) {
|
|
778
|
+
const followMsg = messages[j];
|
|
779
|
+
if (followMsg &&
|
|
780
|
+
followMsg.role === 'tool' &&
|
|
781
|
+
followMsg.tool_call_id) {
|
|
782
|
+
toolCallIds.delete(followMsg.tool_call_id);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
// If some tool results are missing, remove from this assistant message onwards
|
|
786
|
+
if (toolCallIds.size > 0) {
|
|
787
|
+
truncateIndex = i;
|
|
788
|
+
break;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
// If we found a complete assistant response without tool calls, we're done
|
|
792
|
+
if (msg.role === 'assistant' && !msg.tool_calls) {
|
|
793
|
+
break;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
// Truncate session if needed
|
|
797
|
+
if (truncateIndex < messages.length) {
|
|
798
|
+
await sessionManager.truncateMessages(truncateIndex);
|
|
799
|
+
// Also clear from saved messages tracking
|
|
800
|
+
clearSavedMessages();
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
catch (error) {
|
|
804
|
+
console.error('Failed to clean up incomplete conversation:', error);
|
|
805
|
+
}
|
|
806
|
+
})();
|
|
807
|
+
}
|
|
808
|
+
// Add discontinued message after all processing is done
|
|
809
|
+
setMessages(prev => [
|
|
810
|
+
...prev,
|
|
811
|
+
{
|
|
812
|
+
role: 'assistant',
|
|
813
|
+
content: '',
|
|
814
|
+
streaming: false,
|
|
815
|
+
discontinued: true,
|
|
816
|
+
},
|
|
817
|
+
]);
|
|
818
|
+
// Reset interruption flag
|
|
819
|
+
userInterruptedRef.current = false;
|
|
820
|
+
}
|
|
723
821
|
// End streaming
|
|
724
822
|
streamingState.setIsStreaming(false);
|
|
725
823
|
streamingState.setAbortController(null);
|
|
@@ -767,21 +865,25 @@ export default function ChatScreen({ skipWelcome }) {
|
|
|
767
865
|
let toolStatusColor = 'cyan';
|
|
768
866
|
let isToolMessage = false;
|
|
769
867
|
const isLastMessage = index === filteredMessages.length - 1;
|
|
770
|
-
if (message.role === 'assistant') {
|
|
771
|
-
if (message.content.startsWith('⚡')
|
|
868
|
+
if (message.role === 'assistant' || message.role === 'subagent') {
|
|
869
|
+
if (message.content.startsWith('⚡') ||
|
|
870
|
+
message.content.includes('⚇⚡')) {
|
|
772
871
|
isToolMessage = true;
|
|
773
872
|
toolStatusColor = 'yellowBright';
|
|
774
873
|
}
|
|
775
|
-
else if (message.content.startsWith('✓')
|
|
874
|
+
else if (message.content.startsWith('✓') ||
|
|
875
|
+
message.content.includes('⚇✓')) {
|
|
776
876
|
isToolMessage = true;
|
|
777
877
|
toolStatusColor = 'green';
|
|
778
878
|
}
|
|
779
|
-
else if (message.content.startsWith('✗')
|
|
879
|
+
else if (message.content.startsWith('✗') ||
|
|
880
|
+
message.content.includes('⚇✗')) {
|
|
780
881
|
isToolMessage = true;
|
|
781
882
|
toolStatusColor = 'red';
|
|
782
883
|
}
|
|
783
884
|
else {
|
|
784
|
-
toolStatusColor =
|
|
885
|
+
toolStatusColor =
|
|
886
|
+
message.role === 'subagent' ? 'magenta' : 'blue';
|
|
785
887
|
}
|
|
786
888
|
}
|
|
787
889
|
return (React.createElement(Box, { key: `msg-${index}`, marginTop: index > 0 ? 1 : 0, marginBottom: isLastMessage ? 1 : 0, paddingX: 1, flexDirection: "column", width: terminalWidth },
|
|
@@ -850,13 +952,15 @@ export default function ChatScreen({ skipWelcome }) {
|
|
|
850
952
|
}
|
|
851
953
|
return null;
|
|
852
954
|
}))),
|
|
853
|
-
message.content.startsWith('✓')
|
|
955
|
+
(message.content.startsWith('✓') ||
|
|
956
|
+
message.content.includes('⚇✓')) &&
|
|
854
957
|
message.toolResult &&
|
|
855
958
|
// 只在没有 diff 数据时显示预览(有 diff 的工具会用 DiffViewer 显示)
|
|
856
959
|
!(message.toolCall &&
|
|
857
960
|
(message.toolCall.arguments?.oldContent ||
|
|
858
961
|
message.toolCall.arguments?.batchResults)) && (React.createElement(ToolResultPreview, { toolName: message.content
|
|
859
962
|
.replace('✓ ', '')
|
|
963
|
+
.replace(/.*⚇✓\s*/, '')
|
|
860
964
|
.split('\n')[0] || '', result: message.toolResult, maxLines: 5 })),
|
|
861
965
|
message.role === 'user' && message.systemInfo && (React.createElement(Box, { marginTop: 1, flexDirection: "column" },
|
|
862
966
|
React.createElement(Text, { color: "gray", dimColor: true },
|
|
@@ -3,14 +3,10 @@ import { vscodeConnection } from '../vscodeConnection.js';
|
|
|
3
3
|
// IDE connection command handler
|
|
4
4
|
registerCommand('ide', {
|
|
5
5
|
execute: async () => {
|
|
6
|
-
//
|
|
6
|
+
// If already connected, disconnect first to force reconnection
|
|
7
7
|
if (vscodeConnection.isConnected()) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
action: 'info',
|
|
11
|
-
alreadyConnected: true,
|
|
12
|
-
message: `Already connected to IDE (port ${vscodeConnection.getPort()})`
|
|
13
|
-
};
|
|
8
|
+
vscodeConnection.stop();
|
|
9
|
+
vscodeConnection.resetReconnectAttempts();
|
|
14
10
|
}
|
|
15
11
|
// Try to connect to IDE plugin server
|
|
16
12
|
try {
|
|
@@ -18,7 +14,7 @@ registerCommand('ide', {
|
|
|
18
14
|
return {
|
|
19
15
|
success: true,
|
|
20
16
|
action: 'info',
|
|
21
|
-
message: `Connected to IDE on port ${vscodeConnection.getPort()}\nMake sure your IDE plugin (VSCode/JetBrains) is active and running
|
|
17
|
+
message: `Connected to IDE on port ${vscodeConnection.getPort()}\nMake sure your IDE plugin (VSCode/JetBrains) is active and running.`,
|
|
22
18
|
};
|
|
23
19
|
}
|
|
24
20
|
catch (error) {
|
|
@@ -26,9 +22,9 @@ registerCommand('ide', {
|
|
|
26
22
|
success: false,
|
|
27
23
|
message: error instanceof Error
|
|
28
24
|
? `Failed to connect to IDE: ${error.message}\nMake sure your IDE plugin is installed and active.`
|
|
29
|
-
: 'Failed to connect to IDE. Make sure your IDE plugin is installed and active.'
|
|
25
|
+
: 'Failed to connect to IDE. Make sure your IDE plugin is installed and active.',
|
|
30
26
|
};
|
|
31
27
|
}
|
|
32
|
-
}
|
|
28
|
+
},
|
|
33
29
|
});
|
|
34
30
|
export default {};
|
|
@@ -15,6 +15,9 @@ export interface ToolConfirmationCallback {
|
|
|
15
15
|
export interface ToolApprovalChecker {
|
|
16
16
|
(toolName: string): boolean;
|
|
17
17
|
}
|
|
18
|
+
export interface AddToAlwaysApprovedCallback {
|
|
19
|
+
(toolName: string): void;
|
|
20
|
+
}
|
|
18
21
|
/**
|
|
19
22
|
* Execute a sub-agent as a tool
|
|
20
23
|
* @param agentId - The ID of the sub-agent to execute
|
|
@@ -26,4 +29,4 @@ export interface ToolApprovalChecker {
|
|
|
26
29
|
* @param yoloMode - Whether YOLO mode is enabled (auto-approve all tools)
|
|
27
30
|
* @returns The final result from the sub-agent
|
|
28
31
|
*/
|
|
29
|
-
export declare function executeSubAgent(agentId: string, prompt: string, onMessage?: (message: SubAgentMessage) => void, abortSignal?: AbortSignal, requestToolConfirmation?: ToolConfirmationCallback, isToolAutoApproved?: ToolApprovalChecker, yoloMode?: boolean): Promise<SubAgentResult>;
|
|
32
|
+
export declare function executeSubAgent(agentId: string, prompt: string, onMessage?: (message: SubAgentMessage) => void, abortSignal?: AbortSignal, requestToolConfirmation?: ToolConfirmationCallback, isToolAutoApproved?: ToolApprovalChecker, yoloMode?: boolean, addToAlwaysApproved?: AddToAlwaysApprovedCallback): Promise<SubAgentResult>;
|
|
@@ -17,7 +17,7 @@ import { sessionManager } from './sessionManager.js';
|
|
|
17
17
|
* @param yoloMode - Whether YOLO mode is enabled (auto-approve all tools)
|
|
18
18
|
* @returns The final result from the sub-agent
|
|
19
19
|
*/
|
|
20
|
-
export async function executeSubAgent(agentId, prompt, onMessage, abortSignal, requestToolConfirmation, isToolAutoApproved, yoloMode) {
|
|
20
|
+
export async function executeSubAgent(agentId, prompt, onMessage, abortSignal, requestToolConfirmation, isToolAutoApproved, yoloMode, addToAlwaysApproved) {
|
|
21
21
|
try {
|
|
22
22
|
// Get sub-agent configuration
|
|
23
23
|
const agent = getSubAgent(agentId);
|
|
@@ -60,6 +60,9 @@ export async function executeSubAgent(agentId, prompt, onMessage, abortSignal, r
|
|
|
60
60
|
let finalResponse = '';
|
|
61
61
|
let hasError = false;
|
|
62
62
|
let errorMessage = '';
|
|
63
|
+
// Local session-approved tools for this sub-agent execution
|
|
64
|
+
// This ensures tools approved during execution are immediately recognized
|
|
65
|
+
const sessionApprovedTools = new Set();
|
|
63
66
|
const maxIterations = 10; // Prevent infinite loops
|
|
64
67
|
let iteration = 0;
|
|
65
68
|
while (iteration < maxIterations) {
|
|
@@ -168,8 +171,9 @@ export async function executeSubAgent(agentId, prompt, onMessage, abortSignal, r
|
|
|
168
171
|
if (yoloMode) {
|
|
169
172
|
needsConfirmation = false;
|
|
170
173
|
}
|
|
171
|
-
// Check if tool is in auto-approved list
|
|
172
|
-
else if (
|
|
174
|
+
// Check if tool is in auto-approved list (global or session)
|
|
175
|
+
else if (sessionApprovedTools.has(toolName) ||
|
|
176
|
+
(isToolAutoApproved && isToolAutoApproved(toolName))) {
|
|
173
177
|
needsConfirmation = false;
|
|
174
178
|
}
|
|
175
179
|
if (needsConfirmation && requestToolConfirmation) {
|
|
@@ -179,8 +183,15 @@ export async function executeSubAgent(agentId, prompt, onMessage, abortSignal, r
|
|
|
179
183
|
rejectedToolCalls.push(toolCall);
|
|
180
184
|
continue;
|
|
181
185
|
}
|
|
182
|
-
// If
|
|
183
|
-
|
|
186
|
+
// If approve_always, add to both global and session lists
|
|
187
|
+
if (confirmation === 'approve_always') {
|
|
188
|
+
// Add to local session set (immediate effect)
|
|
189
|
+
sessionApprovedTools.add(toolName);
|
|
190
|
+
// Add to global list (persistent across sub-agent calls)
|
|
191
|
+
if (addToAlwaysApproved) {
|
|
192
|
+
addToAlwaysApproved(toolName);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
184
195
|
}
|
|
185
196
|
approvedToolCalls.push(toolCall);
|
|
186
197
|
}
|
|
@@ -19,11 +19,14 @@ export interface ToolConfirmationCallback {
|
|
|
19
19
|
export interface ToolApprovalChecker {
|
|
20
20
|
(toolName: string): boolean;
|
|
21
21
|
}
|
|
22
|
+
export interface AddToAlwaysApprovedCallback {
|
|
23
|
+
(toolName: string): void;
|
|
24
|
+
}
|
|
22
25
|
/**
|
|
23
26
|
* Execute a single tool call and return the result
|
|
24
27
|
*/
|
|
25
|
-
export declare function executeToolCall(toolCall: ToolCall, abortSignal?: AbortSignal, onTokenUpdate?: (tokenCount: number) => void, onSubAgentMessage?: SubAgentMessageCallback, requestToolConfirmation?: ToolConfirmationCallback, isToolAutoApproved?: ToolApprovalChecker, yoloMode?: boolean): Promise<ToolResult>;
|
|
28
|
+
export declare function executeToolCall(toolCall: ToolCall, abortSignal?: AbortSignal, onTokenUpdate?: (tokenCount: number) => void, onSubAgentMessage?: SubAgentMessageCallback, requestToolConfirmation?: ToolConfirmationCallback, isToolAutoApproved?: ToolApprovalChecker, yoloMode?: boolean, addToAlwaysApproved?: AddToAlwaysApprovedCallback): Promise<ToolResult>;
|
|
26
29
|
/**
|
|
27
30
|
* Execute multiple tool calls in parallel
|
|
28
31
|
*/
|
|
29
|
-
export declare function executeToolCalls(toolCalls: ToolCall[], abortSignal?: AbortSignal, onTokenUpdate?: (tokenCount: number) => void, onSubAgentMessage?: SubAgentMessageCallback, requestToolConfirmation?: ToolConfirmationCallback, isToolAutoApproved?: ToolApprovalChecker, yoloMode?: boolean): Promise<ToolResult[]>;
|
|
32
|
+
export declare function executeToolCalls(toolCalls: ToolCall[], abortSignal?: AbortSignal, onTokenUpdate?: (tokenCount: number) => void, onSubAgentMessage?: SubAgentMessageCallback, requestToolConfirmation?: ToolConfirmationCallback, isToolAutoApproved?: ToolApprovalChecker, yoloMode?: boolean, addToAlwaysApproved?: AddToAlwaysApprovedCallback): Promise<ToolResult[]>;
|
|
@@ -3,7 +3,7 @@ import { subAgentService } from '../mcp/subagent.js';
|
|
|
3
3
|
/**
|
|
4
4
|
* Execute a single tool call and return the result
|
|
5
5
|
*/
|
|
6
|
-
export async function executeToolCall(toolCall, abortSignal, onTokenUpdate, onSubAgentMessage, requestToolConfirmation, isToolAutoApproved, yoloMode) {
|
|
6
|
+
export async function executeToolCall(toolCall, abortSignal, onTokenUpdate, onSubAgentMessage, requestToolConfirmation, isToolAutoApproved, yoloMode, addToAlwaysApproved) {
|
|
7
7
|
try {
|
|
8
8
|
const args = JSON.parse(toolCall.function.arguments);
|
|
9
9
|
// Check if this is a sub-agent tool
|
|
@@ -38,6 +38,7 @@ export async function executeToolCall(toolCall, abortSignal, onTokenUpdate, onSu
|
|
|
38
38
|
: undefined,
|
|
39
39
|
isToolAutoApproved,
|
|
40
40
|
yoloMode,
|
|
41
|
+
addToAlwaysApproved,
|
|
41
42
|
});
|
|
42
43
|
return {
|
|
43
44
|
tool_call_id: toolCall.id,
|
|
@@ -64,6 +65,6 @@ export async function executeToolCall(toolCall, abortSignal, onTokenUpdate, onSu
|
|
|
64
65
|
/**
|
|
65
66
|
* Execute multiple tool calls in parallel
|
|
66
67
|
*/
|
|
67
|
-
export async function executeToolCalls(toolCalls, abortSignal, onTokenUpdate, onSubAgentMessage, requestToolConfirmation, isToolAutoApproved, yoloMode) {
|
|
68
|
-
return Promise.all(toolCalls.map(tc => executeToolCall(tc, abortSignal, onTokenUpdate, onSubAgentMessage, requestToolConfirmation, isToolAutoApproved, yoloMode)));
|
|
68
|
+
export async function executeToolCalls(toolCalls, abortSignal, onTokenUpdate, onSubAgentMessage, requestToolConfirmation, isToolAutoApproved, yoloMode, addToAlwaysApproved) {
|
|
69
|
+
return Promise.all(toolCalls.map(tc => executeToolCall(tc, abortSignal, onTokenUpdate, onSubAgentMessage, requestToolConfirmation, isToolAutoApproved, yoloMode, addToAlwaysApproved)));
|
|
69
70
|
}
|