snow-ai 0.3.26 → 0.3.28

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.
@@ -101,30 +101,50 @@ const SYSTEM_PROMPT_TEMPLATE = `You are Snow AI CLI, an intelligent command-line
101
101
 
102
102
  **Golden Rule: Read what you need to write correct code, nothing more.**
103
103
 
104
- ### 📋 TODO Management - For Complex Programming Tasks
105
-
106
- **🚫 CRITICAL: TODO tools MUST be called in parallel with other tools - NEVER call TODO tools alone!**
107
-
108
- **When to use TODO:**
109
- - Complex tasks with 5+ steps requiring tracking
110
- - Multi-file implementations needing coordination
111
- - User explicitly requests TODO
112
-
113
- **When to skip TODO:**
114
- - Simple 1-3 step tasks (just do them)
115
- - Single file edits or quick fixes
116
-
117
- **Usage rules:**
118
- 1. **Parallel calls only**: Always combine TODO with action tools (filesystem-edit, etc.)
119
- 2. **Action-focused**: "Fix parser.ts timeout" ✅, "Read parser.ts"
120
- 3. **Update immediately**: Mark completed right after task is done
121
- 4. **Keep minimal**: 3-7 tasks max, avoid over-planning
122
-
123
- **Example workflow:**
124
- - CORRECT: Call todo-create with filesystem-edit in parallel
125
- - CORRECT: Call todo-update with filesystem-edit in parallel
126
- - WRONG: Call todo-create alone, then wait, then call filesystem-edit
127
- - WRONG: Call todo-update alone, then wait, then proceed
104
+ ### 📋 TODO Management - STRONGLY RECOMMENDED for Better Results!
105
+
106
+ **🎯 DEFAULT BEHAVIOR: Use TODO for ALL multi-step tasks (3+ steps)**
107
+
108
+ **✨ WHY TODO IS ESSENTIAL:**
109
+ - 📊 **Track progress** - Never lose your place in complex work
110
+ - ✅ **Ensure completeness** - Verify all steps are done
111
+ - 🎯 **Stay focused** - Clear roadmap prevents confusion
112
+ - 💪 **Build confidence** - Users see structured progress
113
+ - 🚀 **Better quality** - Systematic approach reduces errors
114
+
115
+ **⚡ WHEN TO USE TODO (Default for most tasks):**
116
+ - ✅ **ANY multi-file modification** (always use)
117
+ - ✅ **ANY feature implementation** (always use)
118
+ - **ANY refactoring task** (always use)
119
+ - **Bug fixes touching 2+ files** (recommended)
120
+ - **User requests with multiple requirements** (always use)
121
+ - **Unfamiliar codebase changes** (recommended)
122
+ - ⚠️ **SKIP ONLY for**: Single-file trivial edits (1-2 lines)
123
+
124
+ **🔧 USAGE RULES (Critical):**
125
+ 1. **⚠️ PARALLEL CALLS ONLY**: ALWAYS call TODO tools with action tools in the SAME function call block
126
+ 2. **Immediate updates**: Mark completed while performing work (not after)
127
+ 3. **Right sizing**: 3-7 main tasks, add subtasks if needed
128
+ 4. **Lifecycle Management**:
129
+ - New task = Create TODO at start
130
+ - Major requirement change = Delete old + create new
131
+ - Minor adjustment = Use todo-add or todo-update
132
+ - **CRITICAL**: Keep using TODO throughout the entire conversation!
133
+
134
+ **✅ CORRECT PATTERNS (Do this):**
135
+ - ✅ todo-create + filesystem-read → Plan while gathering info
136
+ - ✅ todo-update(completed) + filesystem-edit → Update as you work
137
+ - ✅ todo-get + filesystem-read → Check status while reading
138
+ - ✅ todo-add + filesystem-edit → Add new task while working
139
+
140
+ **❌ FORBIDDEN PATTERNS (NEVER do this - WILL FAIL):**
141
+ - ❌ todo-create alone, wait for result, then work → VIOLATION! Call together!
142
+ - ❌ todo-update alone, wait, then continue → VIOLATION! Update while working!
143
+ - ❌ todo-get alone just to check → VIOLATION! Call with other tools!
144
+ - ❌ Skipping TODO for multi-file tasks → VIOLATION! Always use TODO!
145
+ - ❌ **Abandoning TODO mid-conversation** → VIOLATION! Keep using throughout dialogue!
146
+
147
+ **💡 BEST PRACTICE: Start every non-trivial task with todo-create + initial action in parallel!**
128
148
 
129
149
  ## 🛠️ Available Tools
130
150
 
@@ -175,9 +195,10 @@ Guidance and recommendations:
175
195
 
176
196
  ## 📚 Project Context (SNOW.md)
177
197
 
178
- - Read ONLY when implementing large features or unfamiliar architecture
179
- - Skip for simple tasks where you understand the structure
180
- - Contains: project overview, architecture, tech stack
198
+ - Contains: project overview, architecture, tech stack.
199
+ - Generally located in the project root directory.
200
+ - You can read this file at any time to understand the project and recommend reading.
201
+ - This file may not exist. If you can't find it, please ignore it.
181
202
 
182
203
  Remember: **ACTION > ANALYSIS**. Write code first, investigate only when blocked.`;
183
204
  // Export SYSTEM_PROMPT as a getter function for real-time ROLE.md updates
@@ -27,6 +27,7 @@ export async function executeContextCompression() {
27
27
  tool_calls: msg.tool_calls,
28
28
  images: msg.images,
29
29
  reasoning: msg.reasoning,
30
+ thinking: msg.thinking, // 保留 thinking 字段(Anthropic Extended Thinking)
30
31
  subAgentInternal: msg.subAgentInternal,
31
32
  }));
32
33
  // Compress the context (全量压缩,保留最后一轮完整对话)
@@ -38,10 +39,10 @@ export async function executeContextCompression() {
38
39
  }
39
40
  // 构建新的会话消息列表
40
41
  const newSessionMessages = [];
41
- // 添加压缩摘要到会话
42
+ // 添加压缩摘要到会话(使用 user 角色,因为 Extended Thinking 模式下所有 assistant 消息都需要 thinking 块)
42
43
  newSessionMessages.push({
43
- role: 'assistant',
44
- content: compressionResult.summary,
44
+ role: 'user',
45
+ content: `[Context Summary from Previous Conversation]\n\n${compressionResult.summary}`,
45
46
  timestamp: Date.now(),
46
47
  });
47
48
  // 添加保留的最后一轮完整对话(保留完整的消息结构)
@@ -57,6 +58,7 @@ export async function executeContextCompression() {
57
58
  ...(msg.tool_calls && { tool_calls: msg.tool_calls }),
58
59
  ...(msg.images && { images: msg.images }),
59
60
  ...(msg.reasoning && { reasoning: msg.reasoning }),
61
+ ...(msg.thinking && { thinking: msg.thinking }), // 保留 thinking 字段(Anthropic Extended Thinking)
60
62
  ...(msg.subAgentInternal !== undefined && {
61
63
  subAgentInternal: msg.subAgentInternal,
62
64
  }),
@@ -53,8 +53,40 @@ export function useCommandPanel(buffer, isProcessing = false) {
53
53
  if (!text.startsWith('/'))
54
54
  return [];
55
55
  const query = text.slice(1).toLowerCase();
56
- return commands.filter(command => command.name.toLowerCase().includes(query) ||
57
- command.description.toLowerCase().includes(query));
56
+ // Filter and sort commands by priority
57
+ // Priority order:
58
+ // 1. Command starts with query (highest)
59
+ // 2. Command contains query
60
+ // 3. Description starts with query
61
+ // 4. Description contains query (lowest)
62
+ const filtered = commands
63
+ .filter(command => command.name.toLowerCase().includes(query) ||
64
+ command.description.toLowerCase().includes(query))
65
+ .map(command => {
66
+ const nameLower = command.name.toLowerCase();
67
+ const descLower = command.description.toLowerCase();
68
+ let priority = 4; // Default: description contains query
69
+ if (nameLower.startsWith(query)) {
70
+ priority = 1; // Command starts with query
71
+ }
72
+ else if (nameLower.includes(query)) {
73
+ priority = 2; // Command contains query
74
+ }
75
+ else if (descLower.startsWith(query)) {
76
+ priority = 3; // Description starts with query
77
+ }
78
+ return { command, priority };
79
+ })
80
+ .sort((a, b) => {
81
+ // Sort by priority (lower number = higher priority)
82
+ if (a.priority !== b.priority) {
83
+ return a.priority - b.priority;
84
+ }
85
+ // If same priority, sort alphabetically by name
86
+ return a.command.name.localeCompare(b.command.name);
87
+ })
88
+ .map(item => item.command);
89
+ return filtered;
58
90
  }, [buffer]);
59
91
  // Update command panel state
60
92
  const updateCommandPanelState = useCallback((text) => {
@@ -327,6 +327,10 @@ export async function handleConversationWithTools(options) {
327
327
  ]);
328
328
  }
329
329
  // Display tool calls in UI - 只有耗时工具才显示进行中状态
330
+ // Generate parallel group ID when there are multiple tools
331
+ const parallelGroupId = receivedToolCalls.length > 1
332
+ ? `parallel-${Date.now()}-${Math.random()}`
333
+ : undefined;
330
334
  for (const toolCall of receivedToolCalls) {
331
335
  const toolDisplay = formatToolCallMessage(toolCall);
332
336
  let toolArgs;
@@ -351,6 +355,8 @@ export async function handleConversationWithTools(options) {
351
355
  toolDisplay,
352
356
  toolCallId: toolCall.id, // Store tool call ID for later update
353
357
  toolPending: true, // Mark as pending execution
358
+ // Mark parallel group for ALL tools (time-consuming or not)
359
+ parallelGroup: parallelGroupId,
354
360
  },
355
361
  ]);
356
362
  }
@@ -724,6 +730,8 @@ export async function handleConversationWithTools(options) {
724
730
  m.toolResult !== undefined ||
725
731
  m.subAgentInternal === true));
726
732
  // Update existing tool call messages with results
733
+ // Collect all result messages first, then add them in batch
734
+ const resultMessages = [];
727
735
  for (const result of toolResults) {
728
736
  const toolCall = receivedToolCalls.find(tc => tc.id === result.tool_call_id);
729
737
  if (toolCall) {
@@ -733,17 +741,13 @@ export async function handleConversationWithTools(options) {
733
741
  const isError = result.content.startsWith('Error:');
734
742
  const statusIcon = isError ? '✗' : '✓';
735
743
  const statusText = isError ? `\n └─ ${result.content}` : '';
736
- // Display subagent completion message in main flow
737
- setMessages(prev => [
738
- ...prev,
739
- {
740
- role: 'assistant',
741
- content: `${statusIcon} ${toolCall.function.name}${statusText}`,
742
- streaming: false,
743
- // Pass the full result.content for ToolResultPreview to parse
744
- toolResult: !isError ? result.content : undefined,
745
- },
746
- ]);
744
+ resultMessages.push({
745
+ role: 'assistant',
746
+ content: `${statusIcon} ${toolCall.function.name}${statusText}`,
747
+ streaming: false,
748
+ // Pass the full result.content for ToolResultPreview to parse
749
+ toolResult: !isError ? result.content : undefined,
750
+ });
747
751
  // Save the tool result to conversation history
748
752
  conversationMessages.push(result);
749
753
  saveMessage(result).catch(error => {
@@ -790,27 +794,24 @@ export async function handleConversationWithTools(options) {
790
794
  // - 普通工具(单步显示):完成消息需要包含参数和结果,使用 toolDisplay
791
795
  // 获取工具参数的格式化信息
792
796
  const toolDisplay = formatToolCallMessage(toolCall);
793
- setMessages(prev => [
794
- ...prev,
795
- // Add new completed message
796
- {
797
- role: 'assistant',
798
- content: `${statusIcon} ${toolCall.function.name}${statusText}`,
799
- streaming: false,
800
- toolCall: editDiffData
801
- ? {
802
- name: toolCall.function.name,
803
- arguments: editDiffData,
804
- }
805
- : undefined,
806
- // 为普通工具添加参数显示(耗时工具在进行中状态已经显示过参数)
807
- toolDisplay: !isToolNeedTwoStepDisplay(toolCall.function.name)
808
- ? toolDisplay
809
- : undefined,
810
- // Store tool result for preview rendering
811
- toolResult: !isError ? result.content : undefined,
812
- },
813
- ]);
797
+ const isNonTimeConsuming = !isToolNeedTwoStepDisplay(toolCall.function.name);
798
+ resultMessages.push({
799
+ role: 'assistant',
800
+ content: `${statusIcon} ${toolCall.function.name}${statusText}`,
801
+ streaming: false,
802
+ toolCall: editDiffData
803
+ ? {
804
+ name: toolCall.function.name,
805
+ arguments: editDiffData,
806
+ }
807
+ : undefined,
808
+ // 为普通工具添加参数显示(耗时工具在进行中状态已经显示过参数)
809
+ toolDisplay: isNonTimeConsuming ? toolDisplay : undefined,
810
+ // Store tool result for preview rendering
811
+ toolResult: !isError ? result.content : undefined,
812
+ // Mark parallel group for ALL tools (time-consuming or not)
813
+ parallelGroup: parallelGroupId,
814
+ });
814
815
  }
815
816
  // Add tool result to conversation history and save (skip if already saved above)
816
817
  if (toolCall && !toolCall.function.name.startsWith('subagent-')) {
@@ -820,6 +821,10 @@ export async function handleConversationWithTools(options) {
820
821
  });
821
822
  }
822
823
  }
824
+ // Add all result messages in batch to avoid intermediate renders
825
+ if (resultMessages.length > 0) {
826
+ setMessages(prev => [...prev, ...resultMessages]);
827
+ }
823
828
  // Check if there are pending user messages to insert
824
829
  if (options.getPendingMessages && options.clearPendingMessages) {
825
830
  const pendingMessages = options.getPendingMessages();
package/dist/mcp/todo.js CHANGED
@@ -213,36 +213,43 @@ export class TodoService {
213
213
  return [
214
214
  {
215
215
  name: 'todo-create',
216
- description: `Create a TODO list for complex multi-step tasks (optional planning tool).
216
+ description: `✅ RECOMMENDED: Create TODO list for structured task execution. Use this for ALL multi-step tasks!
217
217
 
218
- ## CORE PRINCIPLE - FOCUS ON EXECUTION:
219
- TODO lists are OPTIONAL helpers for complex tasks. Your PRIMARY goal is COMPLETING THE WORK, not maintaining perfect TODO lists. Use this tool only when it genuinely helps organize complex work - don't let TODO management slow you down.
218
+ ⚠️ MANDATORY RULE - PARALLEL CALLS ONLY:
219
+ 🚫 NEVER call todo-create alone! MUST call with other tools in the SAME function call block.
220
+ ✅ ALWAYS: todo-create + filesystem-read (or other action tool) in parallel
221
+ ❌ FORBIDDEN: Call todo-create, wait for result, then call other tools
220
222
 
221
- ## WHEN TO USE (Optional):
222
- - Complex tasks with 5+ distinct steps that benefit from tracking
223
- - Long-running tasks where progress visibility helps the user
224
- - Tasks with multiple dependencies that need careful ordering
225
- - User explicitly requests a TODO list
223
+ ## 🎯 DEFAULT USAGE - Use TODO by default for:
224
+ ANY multi-file changes (always create TODO first)
225
+ ANY feature implementation (plan with TODO)
226
+ ANY refactoring work (track with TODO)
227
+ Bug fixes involving 2+ files (use TODO)
228
+ ✅ Tasks with 3+ distinct steps (create TODO)
229
+ ⚠️ SKIP ONLY: Single-file trivial edits (1-2 lines)
226
230
 
227
- ## WHEN TO SKIP:
228
- - Simple 1-3 step tasks (just do the work directly)
229
- - Straightforward file edits or single-function changes
230
- - Quick fixes or minor modifications
231
- - When TODO creation takes longer than just doing the task
231
+ ## 🚀 WHY CREATE TODO:
232
+ - Ensures all requirements are addressed
233
+ - Prevents missing critical steps
234
+ - Provides clear progress tracking
235
+ - Improves code quality through systematic approach
236
+ - Builds user confidence with visible structure
232
237
 
233
- ## LIFECYCLE MANAGEMENT:
234
- 1. **NEW REQUEST = NEW TODO LIST**: Completely new requirement? Delete old todos first, then create new list.
235
- 2. **INCREMENTAL REQUEST = USE TODO-ADD**: Adding to existing requirement? Use "todo-add" instead.
236
- 3. Use this tool ONLY when starting fresh (new session or new requirement after cleanup).
238
+ ## 📋 WHEN TO CALL:
239
+ 1. **NEW TASK**: Create TODO immediately when starting work (with parallel action)
240
+ 2. **NEW REQUIREMENT**: Delete old todos, create fresh list (with parallel action)
241
+ 3. **BEST PRACTICE**: Call todo-create + filesystem-read in parallel
237
242
 
238
- ## CREATION GUIDELINES:
239
- - Keep it simple and actionable
240
- - 3-7 main tasks is usually sufficient (don't over-plan)
241
- - Include verification steps only if critical
242
- - Order by dependencies
243
+ ## CREATION GUIDELINES:
244
+ - Break work into 3-7 clear, actionable tasks
245
+ - Order by logical dependencies
246
+ - Be specific (e.g., "Modify validateInput in form.ts" not "fix validation")
247
+ - Include verification step if critical
243
248
 
244
- ## WARNING:
245
- This REPLACES the entire TODO list. Never use it to "add more tasks" - use "todo-add" instead.`,
249
+ ## ⚠️ LIFECYCLE:
250
+ This REPLACES the entire TODO list. For adding tasks to existing list, use "todo-add" instead.
251
+
252
+ ## 💡 REMEMBER: MUST call with other tools - never alone!`,
246
253
  inputSchema: {
247
254
  type: 'object',
248
255
  properties: {
@@ -270,7 +277,19 @@ This REPLACES the entire TODO list. Never use it to "add more tasks" - use "todo
270
277
  },
271
278
  {
272
279
  name: 'todo-get',
273
- description: `Get current TODO list with task IDs, status, and hierarchy. Call with other tools in parallel when needed.`,
280
+ description: `Get current TODO list with task IDs, status, and hierarchy.
281
+
282
+ ⚠️ MANDATORY RULE - PARALLEL CALLS ONLY:
283
+ 🚫 NEVER call todo-get alone! MUST call with other tools in the SAME function call block.
284
+ ✅ ALWAYS: todo-get + filesystem-read/terminal-execute/etc in parallel
285
+ ❌ FORBIDDEN: Call todo-get alone to check status
286
+
287
+ ## 🔄 WHEN TO USE IN DIALOGUE:
288
+ - **User provides additional info**: Use todo-get + filesystem-read to check what's done
289
+ - **User requests modifications**: Check current progress before adding/updating tasks
290
+ - **Continuing work**: Always check status first to avoid redoing completed tasks
291
+
292
+ USAGE: Combine with filesystem-read, terminal-execute, or other actions to check progress while working.`,
274
293
  inputSchema: {
275
294
  type: 'object',
276
295
  properties: {},
@@ -278,7 +297,17 @@ This REPLACES the entire TODO list. Never use it to "add more tasks" - use "todo
278
297
  },
279
298
  {
280
299
  name: 'todo-update',
281
- description: `Update TODO status/content. MUST call with other tools - mark "completed" ONLY after task is verified and done.`,
300
+ description: `Update TODO status/content - USE THIS FREQUENTLY to track progress!
301
+
302
+ ⚠️ MANDATORY RULE - PARALLEL CALLS ONLY:
303
+ 🚫 NEVER call todo-update alone! MUST call with other tools in the SAME function call block.
304
+ ✅ ALWAYS: todo-update + filesystem-edit/terminal-execute/etc in parallel
305
+ ❌ FORBIDDEN: Call todo-update, wait for result, then proceed
306
+
307
+ BEST PRACTICE: Mark "completed" ONLY after task is verified.
308
+ Example: todo-update(task1, completed) + filesystem-edit(task2) → Update while working!
309
+
310
+ 💡 This ensures efficient workflow and prevents unnecessary wait times.`,
282
311
  inputSchema: {
283
312
  type: 'object',
284
313
  properties: {
@@ -301,7 +330,19 @@ This REPLACES the entire TODO list. Never use it to "add more tasks" - use "todo
301
330
  },
302
331
  {
303
332
  name: 'todo-add',
304
- description: `Add task to existing TODO (rare use). MUST call with other tools. Only for user-requested or complex tasks, not small steps.`,
333
+ description: `Add new task to existing TODO list when requirements expand.
334
+
335
+ ⚠️ MANDATORY RULE - PARALLEL CALLS ONLY:
336
+ 🚫 NEVER call todo-add alone! MUST call with other tools in the SAME function call block.
337
+ ✅ ALWAYS: todo-add + filesystem-edit/filesystem-read/etc in parallel
338
+ ❌ FORBIDDEN: Call todo-add alone to add task
339
+
340
+ USE WHEN:
341
+ - User adds new requirements during work
342
+ - You discover additional necessary steps
343
+ - Breaking down a complex task into subtasks
344
+
345
+ DO NOT use for initial planning - use todo-create instead.`,
305
346
  inputSchema: {
306
347
  type: 'object',
307
348
  properties: {
@@ -319,7 +360,14 @@ This REPLACES the entire TODO list. Never use it to "add more tasks" - use "todo
319
360
  },
320
361
  {
321
362
  name: 'todo-delete',
322
- description: `Delete TODO item. MUST call with other tools. Deleting parent auto-deletes children (cascade).`,
363
+ description: `Delete TODO item from the list.
364
+
365
+ ⚠️ MANDATORY RULE - PARALLEL CALLS ONLY:
366
+ 🚫 NEVER call todo-delete alone! MUST call with other tools in the SAME function call block.
367
+ ✅ ALWAYS: todo-delete + filesystem-edit/todo-get/etc in parallel
368
+ ❌ FORBIDDEN: Call todo-delete alone
369
+
370
+ NOTE: Deleting a parent task will cascade delete all its children automatically.`,
323
371
  inputSchema: {
324
372
  type: 'object',
325
373
  properties: {
@@ -39,6 +39,7 @@ export interface Message {
39
39
  isComplete?: boolean;
40
40
  };
41
41
  subAgentInternal?: boolean;
42
+ parallelGroup?: string;
42
43
  }
43
44
  interface Props {
44
45
  messages: Message[];
@@ -23,6 +23,8 @@ export default function TodoTree({ todos }) {
23
23
  return '[✓]';
24
24
  case 'pending':
25
25
  return '[ ]';
26
+ default:
27
+ return '[ ]';
26
28
  }
27
29
  };
28
30
  const getStatusColor = (status) => {
@@ -913,6 +913,35 @@ export default function ChatScreen({ skipWelcome }) {
913
913
  let toolStatusColor = 'cyan';
914
914
  let isToolMessage = false;
915
915
  const isLastMessage = index === filteredMessages.length - 1;
916
+ // Check if this message is part of a parallel group
917
+ const isInParallelGroup = message.parallelGroup !== undefined &&
918
+ message.parallelGroup !== null;
919
+ // Check if this is a time-consuming tool (has toolPending or starts with ⚡)
920
+ // Time-consuming tools should not show parallel group indicators
921
+ const isTimeConsumingTool = message.toolPending ||
922
+ (message.role === 'assistant' &&
923
+ (message.content.startsWith('⚡') ||
924
+ message.content.includes('⚇⚡')));
925
+ // Only show parallel group indicators for non-time-consuming tools
926
+ const shouldShowParallelIndicator = isInParallelGroup && !isTimeConsumingTool;
927
+ const isFirstInGroup = shouldShowParallelIndicator &&
928
+ (index === 0 ||
929
+ filteredMessages[index - 1]?.parallelGroup !==
930
+ message.parallelGroup ||
931
+ // Previous message is time-consuming tool, so this is the first non-time-consuming one
932
+ filteredMessages[index - 1]?.toolPending ||
933
+ filteredMessages[index - 1]?.content.startsWith('⚡'));
934
+ // Check if this is the last message in the parallel group
935
+ // Only show end indicator if:
936
+ // 1. This is truly the last message, OR
937
+ // 2. Next message has a DIFFERENT non-null parallelGroup (not just undefined)
938
+ const nextMessage = filteredMessages[index + 1];
939
+ const nextHasDifferentGroup = nextMessage &&
940
+ nextMessage.parallelGroup !== undefined &&
941
+ nextMessage.parallelGroup !== null &&
942
+ nextMessage.parallelGroup !== message.parallelGroup;
943
+ const isLastInGroup = shouldShowParallelIndicator &&
944
+ (!nextMessage || nextHasDifferentGroup);
916
945
  if (message.role === 'assistant' || message.role === 'subagent') {
917
946
  if (message.content.startsWith('⚡') ||
918
947
  message.content.includes('⚇⚡')) {
@@ -934,17 +963,23 @@ export default function ChatScreen({ skipWelcome }) {
934
963
  message.role === 'subagent' ? 'magenta' : 'blue';
935
964
  }
936
965
  }
937
- return (React.createElement(Box, { key: `msg-${index}`, marginTop: index > 0 ? 1 : 0, marginBottom: isLastMessage ? 1 : 0, paddingX: 1, flexDirection: "column", width: terminalWidth },
966
+ return (React.createElement(Box, { key: `msg-${index}`, marginTop: index > 0 && !shouldShowParallelIndicator ? 1 : 0, marginBottom: isLastMessage ? 1 : 0, paddingX: 1, flexDirection: "column", width: terminalWidth },
967
+ isFirstInGroup && (React.createElement(Box, { marginBottom: 0 },
968
+ React.createElement(Text, { color: "#FF6EBF", dimColor: true }, "\u250C\u2500 Parallel execution"))),
938
969
  React.createElement(Box, null,
939
970
  React.createElement(Text, { color: message.role === 'user'
940
971
  ? 'green'
941
972
  : message.role === 'command'
942
973
  ? 'gray'
943
- : toolStatusColor, bold: true }, message.role === 'user'
944
- ? '⛇'
945
- : message.role === 'command'
946
- ? ''
947
- : ''),
974
+ : toolStatusColor, bold: true },
975
+ shouldShowParallelIndicator && !isFirstInGroup
976
+ ? ''
977
+ : '',
978
+ message.role === 'user'
979
+ ? '⛇'
980
+ : message.role === 'command'
981
+ ? '⌘'
982
+ : '❆'),
948
983
  React.createElement(Box, { marginLeft: 1, flexDirection: "column" }, message.role === 'command' ? (React.createElement(React.Fragment, null,
949
984
  React.createElement(Text, { color: "gray", dimColor: true },
950
985
  "\u2514\u2500 ",
@@ -1022,7 +1057,9 @@ export default function ChatScreen({ skipWelcome }) {
1022
1057
  "\u2514\u2500 [image #",
1023
1058
  imageIndex + 1,
1024
1059
  "]"))))),
1025
- message.discontinued && (React.createElement(Text, { color: "red", bold: true }, "\u2514\u2500 user discontinue"))))))));
1060
+ message.discontinued && (React.createElement(Text, { color: "red", bold: true }, "\u2514\u2500 user discontinue")))))),
1061
+ isLastInGroup && (React.createElement(Box, { marginTop: 0 },
1062
+ React.createElement(Text, { color: "#FF6EBF", dimColor: true }, "\u2514\u2500 End parallel execution")))));
1026
1063
  }),
1027
1064
  ] }, item => item),
1028
1065
  (streamingState.isStreaming || isSaving) && !pendingToolConfirmation && (React.createElement(Box, { marginBottom: 1, paddingX: 1, width: terminalWidth },
@@ -103,6 +103,12 @@ export function convertSessionMessagesToUI(sessionMessages) {
103
103
  msg.tool_calls &&
104
104
  msg.tool_calls.length > 0 &&
105
105
  !msg.subAgentInternal) {
106
+ // Generate parallel group ID for non-time-consuming tools
107
+ const hasMultipleTools = msg.tool_calls.length > 1;
108
+ const hasNonTimeConsumingTool = msg.tool_calls.some(tc => !isToolNeedTwoStepDisplay(tc.function.name));
109
+ const parallelGroupId = hasMultipleTools && hasNonTimeConsumingTool
110
+ ? `parallel-${i}-${Math.random()}`
111
+ : undefined;
106
112
  for (const toolCall of msg.tool_calls) {
107
113
  // Skip if already processed
108
114
  if (processedToolCalls.has(toolCall.id))
@@ -130,7 +136,15 @@ export function convertSessionMessagesToUI(sessionMessages) {
130
136
  toolDisplay,
131
137
  });
132
138
  }
133
- processedToolCalls.add(toolCall.id);
139
+ // Store parallel group info for this tool call
140
+ if (parallelGroupId && !needTwoSteps) {
141
+ processedToolCalls.add(toolCall.id);
142
+ // Mark this tool call with parallel group (will be used when processing tool results)
143
+ toolCall.parallelGroupId = parallelGroupId;
144
+ }
145
+ else {
146
+ processedToolCalls.add(toolCall.id);
147
+ }
134
148
  }
135
149
  continue;
136
150
  }
@@ -219,6 +233,23 @@ export function convertSessionMessagesToUI(sessionMessages) {
219
233
  }
220
234
  }
221
235
  }
236
+ // Check if this tool result is part of a parallel group
237
+ let parallelGroupId;
238
+ for (let j = i - 1; j >= 0; j--) {
239
+ const prevMsg = sessionMessages[j];
240
+ if (!prevMsg)
241
+ continue;
242
+ if (prevMsg.role === 'assistant' &&
243
+ prevMsg.tool_calls &&
244
+ !prevMsg.subAgentInternal) {
245
+ const tc = prevMsg.tool_calls.find(t => t.id === msg.tool_call_id);
246
+ if (tc) {
247
+ parallelGroupId = tc.parallelGroupId;
248
+ break;
249
+ }
250
+ }
251
+ }
252
+ const isNonTimeConsuming = !isToolNeedTwoStepDisplay(toolName);
222
253
  uiMessages.push({
223
254
  role: 'assistant',
224
255
  content: `${statusIcon} ${toolName}${statusText}`,
@@ -231,6 +262,19 @@ export function convertSessionMessagesToUI(sessionMessages) {
231
262
  }
232
263
  : undefined,
233
264
  terminalResult: terminalResultData,
265
+ // Add toolDisplay for non-time-consuming tools
266
+ toolDisplay: isNonTimeConsuming && !editDiffData
267
+ ? formatToolCallMessage({
268
+ id: msg.tool_call_id || '',
269
+ type: 'function',
270
+ function: {
271
+ name: toolName,
272
+ arguments: JSON.stringify(toolArgs),
273
+ },
274
+ })
275
+ : undefined,
276
+ // Mark parallel group for non-time-consuming tools
277
+ parallelGroup: isNonTimeConsuming && parallelGroupId ? parallelGroupId : undefined,
234
278
  });
235
279
  continue;
236
280
  }
@@ -27,6 +27,8 @@ export interface AddToAlwaysApprovedCallback {
27
27
  */
28
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>;
29
29
  /**
30
- * Execute multiple tool calls in parallel
30
+ * Execute multiple tool calls with intelligent sequencing
31
+ * - Tools modifying the same resource execute sequentially
32
+ * - Independent tools execute in parallel
31
33
  */
32
34
  export declare function executeToolCalls(toolCalls: ToolCall[], abortSignal?: AbortSignal, onTokenUpdate?: (tokenCount: number) => void, onSubAgentMessage?: SubAgentMessageCallback, requestToolConfirmation?: ToolConfirmationCallback, isToolAutoApproved?: ToolApprovalChecker, yoloMode?: boolean, addToAlwaysApproved?: AddToAlwaysApprovedCallback): Promise<ToolResult[]>;
@@ -63,8 +63,96 @@ export async function executeToolCall(toolCall, abortSignal, onTokenUpdate, onSu
63
63
  }
64
64
  }
65
65
  /**
66
- * Execute multiple tool calls in parallel
66
+ * Categorize tools by their resource type for proper execution sequencing
67
+ */
68
+ function getToolResourceType(toolName) {
69
+ // TODO tools all modify the same TODO file - must be sequential
70
+ if (toolName === 'todo-create' ||
71
+ toolName === 'todo-update' ||
72
+ toolName === 'todo-add' ||
73
+ toolName === 'todo-delete') {
74
+ return 'todo-state';
75
+ }
76
+ // Terminal commands must be sequential to avoid race conditions
77
+ // (e.g., npm install -> npm build, port conflicts, file locks)
78
+ if (toolName === 'terminal-execute') {
79
+ return 'terminal-execution';
80
+ }
81
+ // Each file is a separate resource
82
+ if (toolName === 'filesystem-edit' ||
83
+ toolName === 'filesystem-edit_search' ||
84
+ toolName === 'filesystem-create' ||
85
+ toolName === 'filesystem-delete') {
86
+ return 'filesystem'; // Will be further refined by file path
87
+ }
88
+ // Other tools are independent
89
+ return 'independent';
90
+ }
91
+ /**
92
+ * Get resource identifier for a tool call
93
+ * Tools modifying the same resource will have the same identifier
94
+ */
95
+ function getResourceIdentifier(toolCall) {
96
+ const toolName = toolCall.function.name;
97
+ const resourceType = getToolResourceType(toolName);
98
+ if (resourceType === 'todo-state') {
99
+ return 'todo-state'; // All TODO operations share same resource
100
+ }
101
+ if (resourceType === 'terminal-execution') {
102
+ return 'terminal-execution'; // All terminal commands share same execution context
103
+ }
104
+ if (resourceType === 'filesystem') {
105
+ try {
106
+ const args = JSON.parse(toolCall.function.arguments);
107
+ // Support both single file and array of files
108
+ const filePath = args.filePath;
109
+ if (typeof filePath === 'string') {
110
+ return `filesystem:${filePath}`;
111
+ }
112
+ else if (Array.isArray(filePath)) {
113
+ // For batch operations, treat as independent (already handling multiple files)
114
+ return `filesystem-batch:${toolCall.id}`;
115
+ }
116
+ }
117
+ catch {
118
+ // Parsing error, treat as independent
119
+ }
120
+ }
121
+ // Each independent tool gets its own unique identifier
122
+ return `independent:${toolCall.id}`;
123
+ }
124
+ /**
125
+ * Execute multiple tool calls with intelligent sequencing
126
+ * - Tools modifying the same resource execute sequentially
127
+ * - Independent tools execute in parallel
67
128
  */
68
129
  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)));
130
+ // Group tool calls by their resource identifier
131
+ const resourceGroups = new Map();
132
+ for (const toolCall of toolCalls) {
133
+ const resourceId = getResourceIdentifier(toolCall);
134
+ const group = resourceGroups.get(resourceId) || [];
135
+ group.push(toolCall);
136
+ resourceGroups.set(resourceId, group);
137
+ }
138
+ // Execute each resource group sequentially, but execute different groups in parallel
139
+ const results = await Promise.all(Array.from(resourceGroups.values()).map(async (group) => {
140
+ // Within the same resource group, execute sequentially
141
+ const groupResults = [];
142
+ for (const toolCall of group) {
143
+ const result = await executeToolCall(toolCall, abortSignal, onTokenUpdate, onSubAgentMessage, requestToolConfirmation, isToolAutoApproved, yoloMode, addToAlwaysApproved);
144
+ groupResults.push(result);
145
+ }
146
+ return groupResults;
147
+ }));
148
+ // Flatten results and restore original order
149
+ const flatResults = results.flat();
150
+ const resultMap = new Map(flatResults.map(r => [r.tool_call_id, r]));
151
+ return toolCalls.map(tc => {
152
+ const result = resultMap.get(tc.id);
153
+ if (!result) {
154
+ throw new Error(`Result not found for tool call ${tc.id}`);
155
+ }
156
+ return result;
157
+ });
70
158
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "snow-ai",
3
- "version": "0.3.26",
3
+ "version": "0.3.28",
4
4
  "description": "Intelligent Command Line Assistant powered by AI",
5
5
  "license": "MIT",
6
6
  "bin": {