snow-ai 0.3.27 → 0.3.29

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
  }
@@ -509,8 +515,10 @@ export async function handleConversationWithTools(options) {
509
515
  if (subAgentMessage.message.type === 'tool_calls') {
510
516
  const toolCalls = subAgentMessage.message.tool_calls;
511
517
  if (toolCalls && toolCalls.length > 0) {
512
- // Add tool call messages for each tool
513
- const toolMessages = toolCalls.map((toolCall) => {
518
+ // Add tool call messages for each tool (only for time-consuming tools)
519
+ const toolMessages = toolCalls
520
+ .filter((toolCall) => isToolNeedTwoStepDisplay(toolCall.function.name))
521
+ .map((toolCall) => {
514
522
  const toolDisplay = formatToolCallMessage(toolCall);
515
523
  let toolArgs;
516
524
  try {
@@ -537,16 +545,21 @@ export async function handleConversationWithTools(options) {
537
545
  },
538
546
  subAgentInternal: true, // Mark as internal sub-agent message
539
547
  };
540
- // Save to session as 'assistant' role for API compatibility
541
- const sessionMsg = {
542
- role: 'assistant',
543
- content: `⚇⚡ ${toolDisplay.toolName}`,
544
- subAgentInternal: true,
545
- tool_calls: [toolCall],
546
- };
547
- saveMessage(sessionMsg).catch(err => console.error('Failed to save sub-agent tool call:', err));
548
548
  return uiMsg;
549
549
  });
550
+ // Save all tool calls to session (regardless of display type)
551
+ const sessionMsg = {
552
+ role: 'assistant',
553
+ content: toolCalls
554
+ .map((tc) => {
555
+ const display = formatToolCallMessage(tc);
556
+ return `⚇⚡ ${display.toolName}`;
557
+ })
558
+ .join(', '),
559
+ subAgentInternal: true,
560
+ tool_calls: toolCalls,
561
+ };
562
+ saveMessage(sessionMsg).catch(err => console.error('Failed to save sub-agent tool call:', err));
550
563
  return [...prev, ...toolMessages];
551
564
  }
552
565
  }
@@ -724,6 +737,8 @@ export async function handleConversationWithTools(options) {
724
737
  m.toolResult !== undefined ||
725
738
  m.subAgentInternal === true));
726
739
  // Update existing tool call messages with results
740
+ // Collect all result messages first, then add them in batch
741
+ const resultMessages = [];
727
742
  for (const result of toolResults) {
728
743
  const toolCall = receivedToolCalls.find(tc => tc.id === result.tool_call_id);
729
744
  if (toolCall) {
@@ -733,17 +748,13 @@ export async function handleConversationWithTools(options) {
733
748
  const isError = result.content.startsWith('Error:');
734
749
  const statusIcon = isError ? '✗' : '✓';
735
750
  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
- ]);
751
+ resultMessages.push({
752
+ role: 'assistant',
753
+ content: `${statusIcon} ${toolCall.function.name}${statusText}`,
754
+ streaming: false,
755
+ // Pass the full result.content for ToolResultPreview to parse
756
+ toolResult: !isError ? result.content : undefined,
757
+ });
747
758
  // Save the tool result to conversation history
748
759
  conversationMessages.push(result);
749
760
  saveMessage(result).catch(error => {
@@ -790,27 +801,24 @@ export async function handleConversationWithTools(options) {
790
801
  // - 普通工具(单步显示):完成消息需要包含参数和结果,使用 toolDisplay
791
802
  // 获取工具参数的格式化信息
792
803
  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
- ]);
804
+ const isNonTimeConsuming = !isToolNeedTwoStepDisplay(toolCall.function.name);
805
+ resultMessages.push({
806
+ role: 'assistant',
807
+ content: `${statusIcon} ${toolCall.function.name}${statusText}`,
808
+ streaming: false,
809
+ toolCall: editDiffData
810
+ ? {
811
+ name: toolCall.function.name,
812
+ arguments: editDiffData,
813
+ }
814
+ : undefined,
815
+ // 为普通工具添加参数显示(耗时工具在进行中状态已经显示过参数)
816
+ toolDisplay: isNonTimeConsuming ? toolDisplay : undefined,
817
+ // Store tool result for preview rendering
818
+ toolResult: !isError ? result.content : undefined,
819
+ // Mark parallel group for ALL tools (time-consuming or not)
820
+ parallelGroup: parallelGroupId,
821
+ });
814
822
  }
815
823
  // Add tool result to conversation history and save (skip if already saved above)
816
824
  if (toolCall && !toolCall.function.name.startsWith('subagent-')) {
@@ -820,6 +828,10 @@ export async function handleConversationWithTools(options) {
820
828
  });
821
829
  }
822
830
  }
831
+ // Add all result messages in batch to avoid intermediate renders
832
+ if (resultMessages.length > 0) {
833
+ setMessages(prev => [...prev, ...resultMessages]);
834
+ }
823
835
  // Check if there are pending user messages to insert
824
836
  if (options.getPendingMessages && options.clearPendingMessages) {
825
837
  const pendingMessages = options.getPendingMessages();
@@ -10,8 +10,19 @@ export declare class FilesystemMCPService {
10
10
  */
11
11
  private readonly prettierSupportedExtensions;
12
12
  constructor(basePath?: string);
13
+ /**
14
+ * Extract relevant symbol information for a specific line range
15
+ * This provides context that helps AI make more accurate modifications
16
+ * @param symbols - All symbols in the file
17
+ * @param startLine - Start line of the range
18
+ * @param endLine - End line of the range
19
+ * @param _totalLines - Total lines in the file (reserved for future use)
20
+ * @returns Formatted string with relevant symbol information
21
+ */
22
+ private extractRelevantSymbols;
13
23
  /**
14
24
  * Get the content of a file with optional line range
25
+ * Enhanced with symbol information for better AI context
15
26
  * @param filePath - Path to the file (relative to base path or absolute) or array of file paths or array of file config objects
16
27
  * @param startLine - Starting line number (1-indexed, inclusive, optional - defaults to 1). Used for single file or as default for array of strings
17
28
  * @param endLine - Ending line number (1-indexed, inclusive, optional - defaults to file end). Used for single file or as default for array of strings
@@ -11,6 +11,8 @@ import { calculateSimilarity, normalizeForDisplay, } from './utils/filesystem/si
11
11
  import { analyzeCodeStructure, findSmartContextBoundaries, } from './utils/filesystem/code-analysis.utils.js';
12
12
  import { findClosestMatches, generateDiffMessage, } from './utils/filesystem/match-finder.utils.js';
13
13
  import { parseEditBySearchParams, parseEditByLineParams, executeBatchOperation, } from './utils/filesystem/batch-operations.utils.js';
14
+ // ACE Code Search utilities for symbol parsing
15
+ import { parseFileSymbols } from './utils/aceCodeSearch/symbol.utils.js';
14
16
  const { resolve, dirname, isAbsolute } = path;
15
17
  const execAsync = promisify(exec);
16
18
  /**
@@ -52,8 +54,76 @@ export class FilesystemMCPService {
52
54
  });
53
55
  this.basePath = resolve(basePath);
54
56
  }
57
+ /**
58
+ * Extract relevant symbol information for a specific line range
59
+ * This provides context that helps AI make more accurate modifications
60
+ * @param symbols - All symbols in the file
61
+ * @param startLine - Start line of the range
62
+ * @param endLine - End line of the range
63
+ * @param _totalLines - Total lines in the file (reserved for future use)
64
+ * @returns Formatted string with relevant symbol information
65
+ */
66
+ extractRelevantSymbols(symbols, startLine, endLine, _totalLines) {
67
+ if (symbols.length === 0) {
68
+ return '';
69
+ }
70
+ // Categorize symbols
71
+ const imports = symbols.filter(s => s.type === 'import');
72
+ const exports = symbols.filter(s => s.type === 'export');
73
+ // Symbols within the requested range
74
+ const symbolsInRange = symbols.filter(s => s.line >= startLine && s.line <= endLine);
75
+ // Symbols defined before the range that might be referenced
76
+ const symbolsBeforeRange = symbols.filter(s => s.line < startLine);
77
+ // Build context information
78
+ const parts = [];
79
+ // Always include imports (crucial for understanding dependencies)
80
+ if (imports.length > 0) {
81
+ const importList = imports
82
+ .slice(0, 10) // Limit to avoid excessive tokens
83
+ .map(s => ` • ${s.name} (line ${s.line})`)
84
+ .join('\n');
85
+ parts.push(`📦 Imports:\n${importList}`);
86
+ }
87
+ // Symbols defined in the current range
88
+ if (symbolsInRange.length > 0) {
89
+ const rangeSymbols = symbolsInRange
90
+ .slice(0, 15)
91
+ .map(s => ` • ${s.type}: ${s.name} (line ${s.line})${s.signature ? ` - ${s.signature.slice(0, 60)}` : ''}`)
92
+ .join('\n');
93
+ parts.push(`🎯 Symbols in this range:\n${rangeSymbols}`);
94
+ }
95
+ // Key definitions before this range (that might be referenced)
96
+ if (symbolsBeforeRange.length > 0 && startLine > 1) {
97
+ const relevantBefore = symbolsBeforeRange
98
+ .filter(s => s.type === 'function' || s.type === 'class')
99
+ .slice(-5) // Last 5 before the range
100
+ .map(s => ` • ${s.type}: ${s.name} (line ${s.line})`)
101
+ .join('\n');
102
+ if (relevantBefore) {
103
+ parts.push(`⬆️ Key definitions above:\n${relevantBefore}`);
104
+ }
105
+ }
106
+ // Exports (important for understanding module interface)
107
+ if (exports.length > 0) {
108
+ const exportList = exports
109
+ .slice(0, 10)
110
+ .map(s => ` • ${s.name} (line ${s.line})`)
111
+ .join('\n');
112
+ parts.push(`📤 Exports:\n${exportList}`);
113
+ }
114
+ if (parts.length === 0) {
115
+ return '';
116
+ }
117
+ return ('\n\n' +
118
+ '='.repeat(60) +
119
+ '\n📚 SYMBOL INDEX & DEFINITIONS:\n' +
120
+ '='.repeat(60) +
121
+ '\n' +
122
+ parts.join('\n\n'));
123
+ }
55
124
  /**
56
125
  * Get the content of a file with optional line range
126
+ * Enhanced with symbol information for better AI context
57
127
  * @param filePath - Path to the file (relative to base path or absolute) or array of file paths or array of file config objects
58
128
  * @param startLine - Starting line number (1-indexed, inclusive, optional - defaults to 1). Used for single file or as default for array of strings
59
129
  * @param endLine - Ending line number (1-indexed, inclusive, optional - defaults to file end). Used for single file or as default for array of strings
@@ -127,7 +197,18 @@ export class FilesystemMCPService {
127
197
  const lineNum = start + index;
128
198
  return `${lineNum}→${line}`;
129
199
  });
130
- const fileContent = `📄 ${file} (lines ${start}-${end}/${totalLines})\n${numberedLines.join('\n')}`;
200
+ let fileContent = `📄 ${file} (lines ${start}-${end}/${totalLines})\n${numberedLines.join('\n')}`;
201
+ // Parse and append symbol information
202
+ try {
203
+ const symbols = await parseFileSymbols(fullPath, content, this.basePath);
204
+ const symbolInfo = this.extractRelevantSymbols(symbols, start, end, totalLines);
205
+ if (symbolInfo) {
206
+ fileContent += symbolInfo;
207
+ }
208
+ }
209
+ catch {
210
+ // Silently fail symbol parsing
211
+ }
131
212
  allContents.push(fileContent);
132
213
  filesData.push({
133
214
  path: file,
@@ -197,7 +278,19 @@ export class FilesystemMCPService {
197
278
  const lineNum = start + index;
198
279
  return `${lineNum}→${line}`;
199
280
  });
200
- const partialContent = numberedLines.join('\n');
281
+ let partialContent = numberedLines.join('\n');
282
+ // Parse and append symbol information to provide better context for AI
283
+ try {
284
+ const symbols = await parseFileSymbols(fullPath, content, this.basePath);
285
+ const symbolInfo = this.extractRelevantSymbols(symbols, start, end, totalLines);
286
+ if (symbolInfo) {
287
+ partialContent += symbolInfo;
288
+ }
289
+ }
290
+ catch (error) {
291
+ // Silently fail symbol parsing - don't block file reading
292
+ // This is optional context enhancement, not critical
293
+ }
201
294
  return {
202
295
  content: partialContent,
203
296
  startLine: start,