snow-ai 0.3.12 → 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.
@@ -102,6 +102,7 @@ export class CompactAgent {
102
102
  messages,
103
103
  max_tokens: 4096,
104
104
  includeBuiltinSystemPrompt: false, // 不需要内置系统提示词
105
+ disableThinking: true, // Agents 不使用 Extended Thinking
105
106
  }, abortSignal);
106
107
  break;
107
108
  case 'gemini':
@@ -192,14 +193,14 @@ export class CompactAgent {
192
193
  stack: streamError.stack,
193
194
  name: streamError.name,
194
195
  chunkCount,
195
- contentLength: completeContent.length
196
+ contentLength: completeContent.length,
196
197
  });
197
198
  }
198
199
  else {
199
200
  logger.error('Compact agent: Unknown streaming error:', {
200
201
  error: streamError,
201
202
  chunkCount,
202
- contentLength: completeContent.length
203
+ contentLength: completeContent.length,
203
204
  });
204
205
  }
205
206
  throw streamError;
@@ -220,14 +221,14 @@ export class CompactAgent {
220
221
  stack: error.stack,
221
222
  name: error.name,
222
223
  requestMethod: this.requestMethod,
223
- modelName: this.modelName
224
+ modelName: this.modelName,
224
225
  });
225
226
  }
226
227
  else {
227
228
  logger.error('Compact agent: Unknown API error:', {
228
229
  error,
229
230
  requestMethod: this.requestMethod,
230
- modelName: this.modelName
231
+ modelName: this.modelName,
231
232
  });
232
233
  }
233
234
  throw error;
@@ -291,7 +292,7 @@ Provide the extracted content below:`;
291
292
  logger.warn('Compact agent extraction failed, using original content:', {
292
293
  error: error.message,
293
294
  stack: error.stack,
294
- name: error.name
295
+ name: error.name,
295
296
  });
296
297
  }
297
298
  else {
@@ -203,6 +203,7 @@ Please provide your review in a clear, structured format.`;
203
203
  model: this.modelName,
204
204
  messages: processedMessages,
205
205
  max_tokens: 4096,
206
+ disableThinking: true, // Agents 不使用 Extended Thinking
206
207
  }, abortSignal);
207
208
  break;
208
209
  case 'gemini':
@@ -93,6 +93,7 @@ export class SummaryAgent {
93
93
  messages,
94
94
  max_tokens: 500, // Limited tokens for summary generation
95
95
  includeBuiltinSystemPrompt: false, // 不需要内置系统提示词
96
+ disableThinking: true, // Agents 不使用 Extended Thinking
96
97
  }, abortSignal);
97
98
  break;
98
99
  case 'gemini':
@@ -7,9 +7,10 @@ export interface AnthropicOptions {
7
7
  tools?: ChatCompletionTool[];
8
8
  sessionId?: string;
9
9
  includeBuiltinSystemPrompt?: boolean;
10
+ disableThinking?: boolean;
10
11
  }
11
12
  export interface AnthropicStreamChunk {
12
- type: 'content' | 'tool_calls' | 'tool_call_delta' | 'done' | 'usage';
13
+ type: 'content' | 'tool_calls' | 'tool_call_delta' | 'done' | 'usage' | 'reasoning_started' | 'reasoning_delta';
13
14
  content?: string;
14
15
  tool_calls?: Array<{
15
16
  id: string;
@@ -21,6 +22,11 @@ export interface AnthropicStreamChunk {
21
22
  }>;
22
23
  delta?: string;
23
24
  usage?: UsageInfo;
25
+ thinking?: {
26
+ type: 'thinking';
27
+ thinking: string;
28
+ signature?: string;
29
+ };
24
30
  }
25
31
  export interface AnthropicTool {
26
32
  name: string;
@@ -20,6 +20,7 @@ function getAnthropicConfig() {
20
20
  : 'https://api.anthropic.com/v1',
21
21
  customHeaders,
22
22
  anthropicBeta: config.anthropicBeta,
23
+ thinking: config.thinking,
23
24
  };
24
25
  }
25
26
  return anthropicConfig;
@@ -124,6 +125,11 @@ function convertToAnthropicMessages(messages, includeBuiltinSystemPrompt = true)
124
125
  msg.tool_calls &&
125
126
  msg.tool_calls.length > 0) {
126
127
  const content = [];
128
+ // When thinking is enabled, thinking block must come first
129
+ if (msg.thinking) {
130
+ // Use the complete thinking block object (includes signature)
131
+ content.push(msg.thinking);
132
+ }
127
133
  if (msg.content) {
128
134
  content.push({
129
135
  type: 'text',
@@ -145,10 +151,29 @@ function convertToAnthropicMessages(messages, includeBuiltinSystemPrompt = true)
145
151
  continue;
146
152
  }
147
153
  if (msg.role === 'user' || msg.role === 'assistant') {
148
- anthropicMessages.push({
149
- role: msg.role,
150
- content: msg.content,
151
- });
154
+ // For assistant messages with thinking, convert to structured format
155
+ if (msg.role === 'assistant' && msg.thinking) {
156
+ const content = [];
157
+ // Thinking block must come first - use complete block object (includes signature)
158
+ content.push(msg.thinking);
159
+ // Then text content
160
+ if (msg.content) {
161
+ content.push({
162
+ type: 'text',
163
+ text: msg.content,
164
+ });
165
+ }
166
+ anthropicMessages.push({
167
+ role: 'assistant',
168
+ content,
169
+ });
170
+ }
171
+ else {
172
+ anthropicMessages.push({
173
+ role: msg.role,
174
+ content: msg.content,
175
+ });
176
+ }
152
177
  }
153
178
  }
154
179
  // 如果配置了自定义系统提示词(最高优先级,始终添加)
@@ -266,7 +291,6 @@ export async function* createStreamingAnthropicCompletion(options, abortSignal,
266
291
  const requestBody = {
267
292
  model: options.model,
268
293
  max_tokens: options.max_tokens || 4096,
269
- temperature: options.temperature ?? 0.7,
270
294
  system,
271
295
  messages,
272
296
  tools: convertToolsToAnthropic(options.tools),
@@ -275,11 +299,18 @@ export async function* createStreamingAnthropicCompletion(options, abortSignal,
275
299
  },
276
300
  stream: true,
277
301
  };
302
+ // Add thinking configuration if enabled and not explicitly disabled
303
+ // When thinking is enabled, temperature must be 1
304
+ // Note: agents and other internal tools should set disableThinking=true
305
+ if (config.thinking && !options.disableThinking) {
306
+ requestBody.thinking = config.thinking;
307
+ requestBody.temperature = 1;
308
+ }
278
309
  // Prepare headers
279
310
  const headers = {
280
311
  'Content-Type': 'application/json',
281
312
  'x-api-key': config.apiKey,
282
- 'Authorization': `Bearer ${config.apiKey}`,
313
+ Authorization: `Bearer ${config.apiKey}`,
283
314
  'anthropic-version': '2023-06-01',
284
315
  ...config.customHeaders,
285
316
  };
@@ -305,10 +336,13 @@ export async function* createStreamingAnthropicCompletion(options, abortSignal,
305
336
  throw new Error('No response body from Anthropic API');
306
337
  }
307
338
  let contentBuffer = '';
339
+ let thinkingTextBuffer = ''; // Accumulate thinking text content
340
+ let thinkingSignature = ''; // Accumulate thinking signature
308
341
  let toolCallsBuffer = new Map();
309
342
  let hasToolCalls = false;
310
343
  let usageData;
311
344
  let blockIndexToId = new Map();
345
+ let blockIndexToType = new Map(); // Track block types (text, thinking, tool_use)
312
346
  let completedToolBlocks = new Set(); // Track which tool blocks have finished streaming
313
347
  for await (const event of parseSSEStream(response.body.getReader())) {
314
348
  if (abortSignal?.aborted) {
@@ -316,9 +350,11 @@ export async function* createStreamingAnthropicCompletion(options, abortSignal,
316
350
  }
317
351
  if (event.type === 'content_block_start') {
318
352
  const block = event.content_block;
353
+ const blockIndex = event.index;
354
+ // Track block type for later reference
355
+ blockIndexToType.set(blockIndex, block.type);
319
356
  if (block.type === 'tool_use') {
320
357
  hasToolCalls = true;
321
- const blockIndex = event.index;
322
358
  blockIndexToId.set(blockIndex, block.id);
323
359
  toolCallsBuffer.set(block.id, {
324
360
  id: block.id,
@@ -333,6 +369,13 @@ export async function* createStreamingAnthropicCompletion(options, abortSignal,
333
369
  delta: block.name,
334
370
  };
335
371
  }
372
+ // Handle thinking block start (Extended Thinking feature)
373
+ else if (block.type === 'thinking') {
374
+ // Thinking block started - emit reasoning_started event
375
+ yield {
376
+ type: 'reasoning_started',
377
+ };
378
+ }
336
379
  }
337
380
  else if (event.type === 'content_block_delta') {
338
381
  const delta = event.delta;
@@ -344,6 +387,21 @@ export async function* createStreamingAnthropicCompletion(options, abortSignal,
344
387
  content: text,
345
388
  };
346
389
  }
390
+ // Handle thinking_delta (Extended Thinking feature)
391
+ // Emit reasoning_delta event for thinking content
392
+ if (delta.type === 'thinking_delta') {
393
+ const thinkingText = delta.thinking;
394
+ thinkingTextBuffer += thinkingText; // Accumulate thinking text
395
+ yield {
396
+ type: 'reasoning_delta',
397
+ delta: thinkingText,
398
+ };
399
+ }
400
+ // Handle signature_delta (Extended Thinking feature)
401
+ // Signature is required for thinking blocks
402
+ if (delta.type === 'signature_delta') {
403
+ thinkingSignature += delta.signature; // Accumulate signature
404
+ }
347
405
  if (delta.type === 'input_json_delta') {
348
406
  const jsonDelta = delta.partial_json;
349
407
  const blockIndex = event.index;
@@ -457,8 +515,17 @@ export async function* createStreamingAnthropicCompletion(options, abortSignal,
457
515
  usage: usageData,
458
516
  };
459
517
  }
518
+ // Return complete thinking block with signature if thinking content exists
519
+ const thinkingBlock = thinkingTextBuffer
520
+ ? {
521
+ type: 'thinking',
522
+ thinking: thinkingTextBuffer,
523
+ signature: thinkingSignature || undefined,
524
+ }
525
+ : undefined;
460
526
  yield {
461
527
  type: 'done',
528
+ thinking: thinkingBlock,
462
529
  };
463
530
  }, {
464
531
  abortSignal,
package/dist/api/chat.js CHANGED
@@ -185,7 +185,7 @@ export async function* createStreamingChatCompletion(options, abortSignal, onRet
185
185
  method: 'POST',
186
186
  headers: {
187
187
  'Content-Type': 'application/json',
188
- 'Authorization': `Bearer ${config.apiKey}`,
188
+ Authorization: `Bearer ${config.apiKey}`,
189
189
  ...config.customHeaders,
190
190
  },
191
191
  body: JSON.stringify(requestBody),
@@ -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,
@@ -256,7 +259,7 @@ export async function* createStreamingResponse(options, abortSignal, onRetry) {
256
259
  method: 'POST',
257
260
  headers: {
258
261
  'Content-Type': 'application/json',
259
- 'Authorization': `Bearer ${config.apiKey}`,
262
+ Authorization: `Bearer ${config.apiKey}`,
260
263
  ...config.customHeaders,
261
264
  },
262
265
  body: JSON.stringify(requestPayload),
@@ -442,7 +445,7 @@ export async function* createStreamingResponse(options, abortSignal, onRetry) {
442
445
  usage: usageData,
443
446
  };
444
447
  }
445
- // 发送完成信号
448
+ // 发送完成信号 - For Responses API, thinking content is in reasoning object, not separate thinking field
446
449
  yield {
447
450
  type: 'done',
448
451
  };
@@ -33,7 +33,7 @@ const SYSTEM_PROMPT_TEMPLATE = `You are Snow AI CLI, an intelligent command-line
33
33
  1. **Language Adaptation**: ALWAYS respond in the SAME language as the user's query
34
34
  2. **ACTION FIRST**: Write code immediately when task is clear - stop overthinking
35
35
  3. **Smart Context**: Read what's needed for correctness, skip excessive exploration
36
- 4. **Quality Verification**: Use \'ide-get_diagnostics\' to get diagnostic information or run build/test after changes
36
+ 4. **Quality Verification**: run build/test after changes
37
37
 
38
38
  ## 🚀 Execution Strategy - BALANCE ACTION & ANALYSIS
39
39
 
@@ -121,13 +121,19 @@ 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:
127
- 1. Use \`ide-get_diagnostics\` to verify quality
128
- 2. Run build: \`npm run build\` or \`tsc\`
129
- 3. Fix any errors immediately
130
- 4. Never leave broken code
134
+ 1. Run build: \`npm run build\` or \`tsc\`
135
+ 2. Fix any errors immediately
136
+ 3. Never leave broken code
131
137
 
132
138
  ## 📚 Project Context (SNOW.md)
133
139
 
@@ -29,6 +29,11 @@ export interface ChatMessage {
29
29
  content?: any;
30
30
  encrypted_content?: string;
31
31
  };
32
+ thinking?: {
33
+ type: 'thinking';
34
+ thinking: string;
35
+ signature?: string;
36
+ };
32
37
  }
33
38
  export interface ChatCompletionTool {
34
39
  type: 'function';
@@ -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;
@@ -114,6 +123,7 @@ export async function handleConversationWithTools(options) {
114
123
  let streamedContent = '';
115
124
  let receivedToolCalls;
116
125
  let receivedReasoning;
126
+ let receivedThinking; // Accumulate thinking content from all platforms
117
127
  // Stream AI response - choose API based on config
118
128
  let toolCallAccumulator = ''; // Accumulate tool call deltas for token counting
119
129
  let reasoningAccumulator = ''; // Accumulate reasoning summary deltas for token counting (Responses API only)
@@ -141,6 +151,8 @@ export async function handleConversationWithTools(options) {
141
151
  max_tokens: config.maxTokens || 4096,
142
152
  tools: mcpTools.length > 0 ? mcpTools : undefined,
143
153
  sessionId: currentSession?.id,
154
+ // Disable thinking for basicModel (e.g., init command)
155
+ disableThinking: options.useBasicModel,
144
156
  }, controller.signal, onRetry)
145
157
  : config.requestMethod === 'gemini'
146
158
  ? createStreamingGeminiCompletion({
@@ -157,6 +169,9 @@ export async function handleConversationWithTools(options) {
157
169
  tools: mcpTools.length > 0 ? mcpTools : undefined,
158
170
  tool_choice: 'auto',
159
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,
160
175
  }, controller.signal, onRetry)
161
176
  : createStreamingChatCompletion({
162
177
  model,
@@ -194,6 +209,8 @@ export async function handleConversationWithTools(options) {
194
209
  }
195
210
  else if (chunk.type === 'tool_call_delta' && chunk.delta) {
196
211
  // Accumulate tool call deltas and update token count in real-time
212
+ // When tool calls start, reasoning is done (OpenAI generally doesn't output text content during tool calls)
213
+ setIsReasoning?.(false);
197
214
  toolCallAccumulator += chunk.delta;
198
215
  try {
199
216
  const tokens = encoder.encode(streamedContent + toolCallAccumulator + reasoningAccumulator);
@@ -222,6 +239,10 @@ export async function handleConversationWithTools(options) {
222
239
  // Capture reasoning data from Responses API
223
240
  receivedReasoning = chunk.reasoning;
224
241
  }
242
+ else if (chunk.type === 'done' && chunk.thinking) {
243
+ // Capture thinking content from Anthropic only (includes signature)
244
+ receivedThinking = chunk.thinking;
245
+ }
225
246
  else if (chunk.type === 'usage' && chunk.usage) {
226
247
  // Capture usage information both in state and locally
227
248
  setContextUsage(chunk.usage);
@@ -256,7 +277,8 @@ export async function handleConversationWithTools(options) {
256
277
  }
257
278
  if (chunk.usage.cached_tokens !== undefined) {
258
279
  accumulatedUsage.cached_tokens =
259
- (accumulatedUsage.cached_tokens || 0) + chunk.usage.cached_tokens;
280
+ (accumulatedUsage.cached_tokens || 0) +
281
+ chunk.usage.cached_tokens;
260
282
  }
261
283
  }
262
284
  }
@@ -283,7 +305,8 @@ export async function handleConversationWithTools(options) {
283
305
  arguments: tc.function.arguments,
284
306
  },
285
307
  })),
286
- reasoning: receivedReasoning, // Include reasoning data for caching
308
+ reasoning: receivedReasoning, // Include reasoning data for caching (Responses API)
309
+ thinking: receivedThinking, // Include thinking content (Anthropic/OpenAI)
287
310
  };
288
311
  conversationMessages.push(assistantMessage);
289
312
  // Save assistant message with tool calls
@@ -402,6 +425,8 @@ export async function handleConversationWithTools(options) {
402
425
  approvedTools.push(...toolsNeedingConfirmation);
403
426
  }
404
427
  // Execute approved tools with sub-agent message callback and terminal output callback
428
+ // Track sub-agent content for token counting
429
+ let subAgentContentAccumulator = '';
405
430
  const toolResults = await executeToolCalls(approvedTools, controller.signal, setStreamTokenCount, async (subAgentMessage) => {
406
431
  // Handle sub-agent messages - display and save to session
407
432
  setMessages(prev => {
@@ -515,9 +540,20 @@ export async function handleConversationWithTools(options) {
515
540
  let content = '';
516
541
  if (subAgentMessage.message.type === 'content') {
517
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
+ }
518
552
  }
519
553
  else if (subAgentMessage.message.type === 'done') {
520
- // Mark as complete
554
+ // Mark as complete and reset token counter
555
+ subAgentContentAccumulator = '';
556
+ setStreamTokenCount(0);
521
557
  if (existingIndex !== -1) {
522
558
  const updated = [...prev];
523
559
  const existing = updated[existingIndex];
@@ -565,7 +601,7 @@ export async function handleConversationWithTools(options) {
565
601
  }
566
602
  return prev;
567
603
  });
568
- }, requestToolConfirmation, isToolAutoApproved, yoloMode);
604
+ }, requestToolConfirmation, isToolAutoApproved, yoloMode, addToAlwaysApproved);
569
605
  // Check if aborted during tool execution
570
606
  if (controller.signal.aborted) {
571
607
  freeEncoder();
@@ -819,7 +855,8 @@ export async function handleConversationWithTools(options) {
819
855
  const assistantMessage = {
820
856
  role: 'assistant',
821
857
  content: streamedContent.trim(),
822
- reasoning: receivedReasoning, // Include reasoning data for caching
858
+ reasoning: receivedReasoning, // Include reasoning data for caching (Responses API)
859
+ thinking: receivedThinking, // Include thinking content (Anthropic/OpenAI)
823
860
  };
824
861
  conversationMessages.push(assistantMessage);
825
862
  saveMessage(assistantMessage).catch(error => {
@@ -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
  }