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.
- package/dist/api/systemPrompt.js +48 -27
- package/dist/hooks/useCommandHandler.js +5 -3
- package/dist/hooks/useCommandPanel.js +34 -2
- package/dist/hooks/useConversation.js +37 -32
- package/dist/mcp/todo.js +76 -28
- package/dist/ui/components/MessageList.d.ts +1 -0
- package/dist/ui/components/TodoTree.js +2 -0
- package/dist/ui/pages/ChatScreen.js +44 -7
- package/dist/utils/sessionConverter.js +45 -1
- package/dist/utils/toolExecutor.d.ts +3 -1
- package/dist/utils/toolExecutor.js +90 -2
- package/package.json +1 -1
package/dist/api/systemPrompt.js
CHANGED
|
@@ -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 -
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
-
|
|
110
|
-
-
|
|
111
|
-
-
|
|
112
|
-
|
|
113
|
-
**
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
**
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
-
|
|
179
|
-
-
|
|
180
|
-
-
|
|
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: '
|
|
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
|
-
|
|
57
|
-
|
|
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
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
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
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
{
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
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:
|
|
216
|
+
description: `✅ RECOMMENDED: Create TODO list for structured task execution. Use this for ALL multi-step tasks!
|
|
217
217
|
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
##
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
##
|
|
228
|
-
-
|
|
229
|
-
-
|
|
230
|
-
-
|
|
231
|
-
-
|
|
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
|
-
##
|
|
234
|
-
1. **NEW
|
|
235
|
-
2. **
|
|
236
|
-
3.
|
|
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
|
-
-
|
|
240
|
-
-
|
|
241
|
-
-
|
|
242
|
-
-
|
|
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
|
-
##
|
|
245
|
-
This REPLACES the entire TODO list.
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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: {
|
|
@@ -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 },
|
|
944
|
-
|
|
945
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
}
|