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.
@@ -10,7 +10,7 @@ export interface ResponseOptions {
10
10
  reasoning?: {
11
11
  summary?: 'auto' | 'none';
12
12
  effort?: 'low' | 'medium' | 'high';
13
- };
13
+ } | null;
14
14
  prompt_cache_key?: string;
15
15
  store?: boolean;
16
16
  include?: string[];
@@ -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
- reasoning: options.reasoning || { effort: 'high', summary: 'auto' },
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,
@@ -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
- saveMessage({
68
- role: 'user',
69
- content: userContent,
70
- images: imageContents,
71
- }).catch(error => {
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((resolve) => {
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 alwaysApprovedTools.has(toolName) || toolName.startsWith('todo-') || toolName.startsWith('subagent-');
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
  }
@@ -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
@@ -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
- // Clean up incomplete conversation in session (handled in useConversation abort cleanup)
276
- // This will remove:
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 = 'blue';
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
- // Check if already connected to IDE plugin
6
+ // If already connected, disconnect first to force reconnection
7
7
  if (vscodeConnection.isConnected()) {
8
- return {
9
- success: true,
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 (isToolAutoApproved && isToolAutoApproved(toolName)) {
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 approved or approved_always, continue execution
183
- // (approved_always is handled by the main flow's session-approved list)
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "snow-ai",
3
- "version": "0.3.13",
3
+ "version": "0.3.14",
4
4
  "description": "Intelligent Command Line Assistant powered by AI",
5
5
  "license": "MIT",
6
6
  "bin": {