snow-ai 0.3.27 → 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.
@@ -195,9 +195,10 @@ Guidance and recommendations:
195
195
 
196
196
  ## 📚 Project Context (SNOW.md)
197
197
 
198
- - Read ONLY when implementing large features or unfamiliar architecture
199
- - Skip for simple tasks where you understand the structure
200
- - 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.
201
202
 
202
203
  Remember: **ACTION > ANALYSIS**. Write code first, investigate only when blocked.`;
203
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
  }),
@@ -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();
@@ -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[];
@@ -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.27",
3
+ "version": "0.3.28",
4
4
  "description": "Intelligent Command Line Assistant powered by AI",
5
5
  "license": "MIT",
6
6
  "bin": {