snow-ai 0.3.29 → 0.3.31

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.
@@ -72,6 +72,7 @@ const SYSTEM_PROMPT_TEMPLATE = `You are Snow AI CLI, an intelligent command-line
72
72
  2. **ACTION FIRST**: Write code immediately when task is clear - stop overthinking
73
73
  3. **Smart Context**: Read what's needed for correctness, skip excessive exploration
74
74
  4. **Quality Verification**: run build/test after changes
75
+ 5. **NO Documentation Files**: ❌ NEVER create summary .md files after tasks - use \`notebook-add\` for important notes instead
75
76
 
76
77
  ## 🚀 Execution Strategy - BALANCE ACTION & ANALYSIS
77
78
 
@@ -166,6 +167,28 @@ const SYSTEM_PROMPT_TEMPLATE = `You are Snow AI CLI, an intelligent command-line
166
167
  - Requires IDE plugin installed and running
167
168
  - Use AFTER code changes to verify quality
168
169
 
170
+ **Notebook (Code Memory):**
171
+ - \`notebook-add\` - Record fragile code that new features might break during iteration
172
+ - 🎯 Core purpose: Prevent new functionality from breaking old functionality
173
+ - 📝 Record: Bugs that recurred, fragile dependencies, critical constraints
174
+ - ⚠️ Examples: "validateInput() must run first - broke twice", "null return required by X"
175
+ - 📌 **IMPORTANT**: Use notebook for documentation, NOT separate .md files
176
+ - \`notebook-query\` - Manual search (rarely needed, auto-shown when reading files)
177
+ - 🔍 Auto-attached: Last 10 notebooks appear when reading ANY file
178
+ - 💡 Use before: Adding features that might affect existing behavior
179
+ - \`notebook-update\` - Update existing note to fix mistakes or refine information
180
+ - ✏️ Fix errors in previously recorded notes
181
+ - 📝 Clarify or improve wording after better understanding
182
+ - 🔄 Update note when code changes but constraint still applies
183
+ - \`notebook-delete\` - Remove outdated or incorrect notes
184
+ - 🗑️ Delete when code is refactored and note is obsolete
185
+ - ❌ Remove notes recorded by mistake
186
+ - 🧹 Clean up after workarounds are properly fixed
187
+ - \`notebook-list\` - View all notes for a specific file
188
+ - 📋 List all constraints for a file before making changes
189
+ - 🔍 Find note IDs for update/delete operations
190
+ - 🧐 Review all warnings before refactoring
191
+
169
192
  **Web Search:**
170
193
  - \`websearch-search\` - Search web for latest docs/solutions
171
194
  - \`websearch-fetch\` - Read web page content (always provide userQuery)
@@ -178,13 +201,103 @@ manipulation, workflow automation, and complex command chaining to solve sophist
178
201
  system administration and data processing challenges.
179
202
 
180
203
  **Sub-Agent:**
181
- *If you don't have a sub-agent tool, ignore this feature*
182
- - A sub-agent is a separate session isolated from the main session, and a sub-agent may have some of the tools described above to focus on solving a specific problem.
183
- If you have a sub-agent tool, then you can leave some of the work to the sub-agent to solve.
184
- For example, if you have a sub-agent of a work plan, you can hand over the work plan to the sub-agent to solve when you receive user requirements.
185
- This way, the master agent can focus on task fulfillment.
186
204
 
187
- - The user may set a sub-agent, and there will be the word \`#agent_*\` in the user's message. \`*\` Is a wildcard,is the tool name of the sub-agent, and you must use this sub-agent.
205
+ ### 🎯 CRITICAL: AGGRESSIVE DELEGATION TO SUB-AGENTS
206
+
207
+ **⚡ Core Principle: MAXIMIZE context saving by delegating as much work as possible to sub-agents!**
208
+
209
+ **🔥 WHY DELEGATE AGGRESSIVELY:**
210
+ - 💾 **Save Main Context** - Each delegated task saves thousands of tokens in the main session
211
+ - 🚀 **Parallel Processing** - Sub-agents work independently without cluttering main context
212
+ - 🎯 **Focused Sessions** - Sub-agents have dedicated context for specific tasks
213
+ - 🔄 **Scalability** - Main agent stays lean and efficient even for complex projects
214
+
215
+ **📋 DELEGATION STRATEGY - DEFAULT TO SUB-AGENT:**
216
+
217
+ **✅ ALWAYS DELEGATE (High Priority):**
218
+ - 🔍 **Code Analysis & Planning** - File structure analysis, architecture review, impact analysis
219
+ - 📊 **Research Tasks** - Investigating patterns, finding similar code, exploring codebase
220
+ - 🗺️ **Work Planning** - Breaking down requirements, creating task plans, designing solutions
221
+ - 📝 **Documentation Review** - Reading and summarizing large files, extracting key information
222
+ - 🔎 **Dependency Mapping** - Finding all imports, exports, references across files
223
+ - 🧪 **Test Planning** - Analyzing what needs testing, planning test cases
224
+ - 🔧 **Refactoring Analysis** - Identifying refactoring opportunities, impact assessment
225
+
226
+ **✅ STRONGLY CONSIDER DELEGATING:**
227
+ - 🐛 **Bug Investigation** - Root cause analysis, reproduction steps, related code search
228
+ - 🔄 **Migration Planning** - Planning API changes, version upgrades, dependency updates
229
+ - 📐 **Design Reviews** - Evaluating architectural decisions, pattern consistency
230
+ - 🔍 **Code Quality Checks** - Finding code smells, inconsistencies, potential issues
231
+
232
+ **⚠️ KEEP IN MAIN AGENT (Low Volume):**
233
+ - ✏️ **Direct Code Edits** - Simple, well-understood modifications
234
+ - 🔨 **Quick Fixes** - Single-file changes with clear context
235
+ - ⚡ **Immediate Actions** - Terminal commands, file operations
236
+
237
+ **🎯 DELEGATION WORKFLOW:**
238
+
239
+ 1. **Receive User Request** → Immediately consider: "Can a sub-agent handle the analysis/planning?"
240
+ 2. **Complex Task** → Delegate research/planning to sub-agent, wait for result, then execute
241
+ 3. **Multi-Step Task** → Delegate planning to sub-agent, receive roadmap, execute in main
242
+ 4. **Unfamiliar Code** → Delegate exploration to sub-agent, get summary, then modify
243
+
244
+ **💡 PRACTICAL EXAMPLES:**
245
+
246
+ ❌ **BAD - Doing everything in main agent:**
247
+ - User: "Add user authentication"
248
+ - Main: *reads 20 files, analyzes auth patterns, plans implementation, writes code*
249
+ - Result: Main context bloated with analysis that won't be reused
250
+
251
+ ✅ **GOOD - Aggressive delegation:**
252
+ - User: "Add user authentication"
253
+ - Main: Delegate to sub-agent → "Analyze current auth patterns and create implementation plan"
254
+ - Sub-agent: *analyzes, returns concise plan*
255
+ - Main: Execute plan with focused context
256
+ - Result: Main context stays lean, only contains execution context
257
+
258
+ **🔧 USAGE RULES:**
259
+
260
+ 1. **When tool available**: Check if you have \`subagent-agent_*\` tools in your toolkit
261
+ 2. **Explicit user request**: User message contains \`#agent_*\` → MUST use that specific sub-agent
262
+ 3. **Implicit delegation**: Even without \`#agent_*\`, proactively delegate analysis/planning tasks
263
+ 4. **Return focus**: After sub-agent responds, main agent focuses purely on execution
264
+
265
+ **📌 REMEMBER: If it's not direct code editing or immediate action, consider delegating to sub-agent first!**
266
+
267
+ **🌲 DECISION TREE - When to Delegate to Sub-Agent:**
268
+
269
+ \`\`\`
270
+ 📥 User Request
271
+
272
+ ❓ Can a sub-agent handle this task?
273
+ ├─ ✅ YES → 🚀 DELEGATE to sub-agent
274
+ │ ├─ Code search/exploration
275
+ │ ├─ Analysis & planning
276
+ │ ├─ Research & investigation
277
+ │ ├─ Architecture review
278
+ │ ├─ Impact assessment
279
+ │ ├─ Dependency mapping
280
+ │ ├─ Documentation review
281
+ │ ├─ Test planning
282
+ │ ├─ Bug investigation
283
+ │ ├─ Pattern finding
284
+ │ └─ ANY task sub-agent can do
285
+
286
+ └─ ❌ NO → Execute directly in main agent
287
+ ├─ Direct code editing (clear target)
288
+ ├─ File operations (create/delete)
289
+ ├─ Simple terminal commands
290
+ └─ Immediate actions (no research needed)
291
+ \`\`\`
292
+
293
+ **🎯 Golden Rule:**
294
+ **"If sub-agent CAN do it → sub-agent SHOULD do it"**
295
+
296
+ **Decision in 3 seconds:**
297
+ 1. ❓ Does this need research/exploration/planning? → **Delegate**
298
+ 2. ❓ Is this a straightforward code edit? → **Execute directly**
299
+ 3. ⚠️ **When in doubt** → **Delegate to sub-agent** (safer default)
300
+
188
301
 
189
302
  ## 🔍 Quality Assurance
190
303
 
@@ -154,14 +154,9 @@ export function useCommandHandler(options) {
154
154
  // Handle /ide command
155
155
  if (commandName === 'ide') {
156
156
  if (result.success) {
157
- // If already connected, set status to connected immediately
158
- // Otherwise, set to connecting and wait for VSCode extension
159
- if (result.alreadyConnected) {
160
- options.setVscodeConnectionStatus('connected');
161
- }
162
- else {
163
- options.setVscodeConnectionStatus('connecting');
164
- }
157
+ // Connection successful, set status to connected immediately
158
+ // The轮询 mechanism will also update the status, but we do it here for immediate feedback
159
+ options.setVscodeConnectionStatus('connected');
165
160
  // Don't add command message to keep UI clean
166
161
  }
167
162
  else {
@@ -692,6 +692,21 @@ export async function handleConversationWithTools(options) {
692
692
  }, requestToolConfirmation, isToolAutoApproved, yoloMode, addToAlwaysApproved);
693
693
  // Check if aborted during tool execution
694
694
  if (controller.signal.aborted) {
695
+ // Need to add tool results for all pending tool calls to complete conversation history
696
+ // This is critical for sub-agents and any tools that were being executed
697
+ if (receivedToolCalls && receivedToolCalls.length > 0) {
698
+ for (const toolCall of receivedToolCalls) {
699
+ const abortedResult = {
700
+ role: 'tool',
701
+ tool_call_id: toolCall.id,
702
+ content: 'Error: Tool execution aborted by user',
703
+ };
704
+ conversationMessages.push(abortedResult);
705
+ saveMessage(abortedResult).catch(error => {
706
+ console.error('Failed to save aborted tool result:', error);
707
+ });
708
+ }
709
+ }
695
710
  freeEncoder();
696
711
  break;
697
712
  }
@@ -23,7 +23,7 @@ export function useVSCodeState() {
23
23
  lastStatusRef.current = 'disconnected';
24
24
  setVscodeConnectionStatus('disconnected');
25
25
  }
26
- }, 1000);
26
+ }, 1000); // Check every second
27
27
  const unsubscribe = vscodeConnection.onContextUpdate(context => {
28
28
  // Only update state if context has actually changed
29
29
  const hasChanged = context.activeFile !== lastEditorContextRef.current.activeFile ||
@@ -51,7 +51,7 @@ export function useVSCodeState() {
51
51
  if (vscodeConnectionStatus !== 'connecting') {
52
52
  return;
53
53
  }
54
- // Set timeout for connecting state (30 seconds to allow for IDE plugin reconnection)
54
+ // Set timeout for connecting state (15 seconds to allow for port scanning and connection)
55
55
  const connectingTimeout = setTimeout(() => {
56
56
  const isConnected = vscodeConnection.isConnected();
57
57
  const isClientRunning = vscodeConnection.isClientRunning();
@@ -65,8 +65,9 @@ export function useVSCodeState() {
65
65
  // Client not running - go back to disconnected
66
66
  setVscodeConnectionStatus('disconnected');
67
67
  }
68
+ lastStatusRef.current = isClientRunning ? 'error' : 'disconnected';
68
69
  }
69
- }, 30000); // Increased to 30 seconds
70
+ }, 15000); // 15 seconds: 10s for connection timeout + 5s buffer
70
71
  return () => {
71
72
  clearTimeout(connectingTimeout);
72
73
  };
@@ -20,6 +20,12 @@ export declare class FilesystemMCPService {
20
20
  * @returns Formatted string with relevant symbol information
21
21
  */
22
22
  private extractRelevantSymbols;
23
+ /**
24
+ * Get notebook entries for a file
25
+ * @param filePath - Path to the file
26
+ * @returns Formatted notebook entries string, or empty if none found
27
+ */
28
+ private getNotebookEntries;
23
29
  /**
24
30
  * Get the content of a file with optional line range
25
31
  * Enhanced with symbol information for better AI context
@@ -13,6 +13,8 @@ import { findClosestMatches, generateDiffMessage, } from './utils/filesystem/mat
13
13
  import { parseEditBySearchParams, parseEditByLineParams, executeBatchOperation, } from './utils/filesystem/batch-operations.utils.js';
14
14
  // ACE Code Search utilities for symbol parsing
15
15
  import { parseFileSymbols } from './utils/aceCodeSearch/symbol.utils.js';
16
+ // Notebook utilities for automatic note retrieval
17
+ import { queryNotebook } from '../utils/notebookManager.js';
16
18
  const { resolve, dirname, isAbsolute } = path;
17
19
  const execAsync = promisify(exec);
18
20
  /**
@@ -121,6 +123,37 @@ export class FilesystemMCPService {
121
123
  '\n' +
122
124
  parts.join('\n\n'));
123
125
  }
126
+ /**
127
+ * Get notebook entries for a file
128
+ * @param filePath - Path to the file
129
+ * @returns Formatted notebook entries string, or empty if none found
130
+ */
131
+ getNotebookEntries(filePath) {
132
+ try {
133
+ const entries = queryNotebook(filePath, 10);
134
+ if (entries.length === 0) {
135
+ return '';
136
+ }
137
+ const notesText = entries
138
+ .map((entry, index) => {
139
+ // createdAt 已经是本地时间格式: "YYYY-MM-DDTHH:mm:ss.SSS"
140
+ // 提取日期和时间部分: "YYYY-MM-DD HH:mm"
141
+ const dateStr = entry.createdAt.substring(0, 16).replace('T', ' ');
142
+ return ` ${index + 1}. [${dateStr}] ${entry.note}`;
143
+ })
144
+ .join('\n');
145
+ return ('\n\n' +
146
+ '='.repeat(60) +
147
+ '\n📝 CODE NOTEBOOKS (Latest 10):\n' +
148
+ '='.repeat(60) +
149
+ '\n' +
150
+ notesText);
151
+ }
152
+ catch {
153
+ // Silently fail notebook retrieval - don't block file reading
154
+ return '';
155
+ }
156
+ }
124
157
  /**
125
158
  * Get the content of a file with optional line range
126
159
  * Enhanced with symbol information for better AI context
@@ -209,6 +242,11 @@ export class FilesystemMCPService {
209
242
  catch {
210
243
  // Silently fail symbol parsing
211
244
  }
245
+ // Append notebook entries
246
+ const notebookInfo = this.getNotebookEntries(file);
247
+ if (notebookInfo) {
248
+ fileContent += notebookInfo;
249
+ }
212
250
  allContents.push(fileContent);
213
251
  filesData.push({
214
252
  path: file,
@@ -291,6 +329,11 @@ export class FilesystemMCPService {
291
329
  // Silently fail symbol parsing - don't block file reading
292
330
  // This is optional context enhancement, not critical
293
331
  }
332
+ // Append notebook entries
333
+ const notebookInfo = this.getNotebookEntries(filePath);
334
+ if (notebookInfo) {
335
+ partialContent += notebookInfo;
336
+ }
294
337
  return {
295
338
  content: partialContent,
296
339
  startLine: start,
@@ -0,0 +1,10 @@
1
+ import { Tool, type CallToolResult } from '@modelcontextprotocol/sdk/types.js';
2
+ /**
3
+ * Notebook MCP 工具定义
4
+ * 用于代码备忘录管理,帮助AI记录重要的代码注意事项
5
+ */
6
+ export declare const mcpTools: Tool[];
7
+ /**
8
+ * 执行 Notebook 工具
9
+ */
10
+ export declare function executeNotebookTool(toolName: string, args: any): Promise<CallToolResult>;
@@ -0,0 +1,367 @@
1
+ import { addNotebook, queryNotebook, updateNotebook, deleteNotebook, getNotebooksByFile, } from '../utils/notebookManager.js';
2
+ /**
3
+ * Notebook MCP 工具定义
4
+ * 用于代码备忘录管理,帮助AI记录重要的代码注意事项
5
+ */
6
+ export const mcpTools = [
7
+ {
8
+ name: 'notebook-add',
9
+ description: `📝 Record code parts that are fragile and easily broken during iteration.
10
+
11
+ **Core Purpose:** Prevent new features from breaking existing functionality.
12
+
13
+ **When to record:**
14
+ - After fixing bugs that could easily reoccur
15
+ - Fragile code that new features might break
16
+ - Non-obvious dependencies between components
17
+ - Workarounds that shouldn't be "optimized away"
18
+
19
+ **Examples:**
20
+ - "⚠️ validateInput() MUST be called first - new features broke this twice"
21
+ - "Component X depends on null return - DO NOT change to empty array"
22
+ - "setTimeout workaround for race condition - don't remove"
23
+ - "Parser expects exact format - adding fields breaks backward compat"`,
24
+ inputSchema: {
25
+ type: 'object',
26
+ properties: {
27
+ filePath: {
28
+ type: 'string',
29
+ description: 'File path (relative or absolute). Example: "src/utils/parser.ts"',
30
+ },
31
+ note: {
32
+ type: 'string',
33
+ description: 'Brief, specific note. Focus on risks/constraints, NOT what code does.',
34
+ },
35
+ },
36
+ required: ['filePath', 'note'],
37
+ },
38
+ },
39
+ {
40
+ name: 'notebook-query',
41
+ description: `🔍 Search notebook entries by file path pattern.
42
+
43
+ **Auto-triggered:** When reading files, last 10 notebooks are automatically shown.
44
+ **Manual use:** Query specific patterns or see more entries.`,
45
+ inputSchema: {
46
+ type: 'object',
47
+ properties: {
48
+ filePathPattern: {
49
+ type: 'string',
50
+ description: 'Fuzzy search pattern (e.g., "parser"). Empty = all entries.',
51
+ default: '',
52
+ },
53
+ topN: {
54
+ type: 'number',
55
+ description: 'Max results to return (default: 10, max: 50)',
56
+ default: 10,
57
+ minimum: 1,
58
+ maximum: 50,
59
+ },
60
+ },
61
+ },
62
+ },
63
+ {
64
+ name: 'notebook-update',
65
+ description: `✏️ Update an existing notebook entry to fix mistakes or refine notes.
66
+
67
+ **Core Purpose:** Correct errors in previously recorded notes or update outdated information.
68
+
69
+ **When to use:**
70
+ - Found a mistake in a previously recorded note
71
+ - Need to clarify or improve the wording
72
+ - Update note after code changes
73
+ - Refine warning messages for better clarity
74
+
75
+ **Usage:**
76
+ 1. Use notebook-query or notebook-list to find the entry ID
77
+ 2. Call notebook-update with the ID and new note content
78
+
79
+ **Example:**
80
+ - Old: "⚠️ Don't change this"
81
+ - New: "⚠️ validateInput() MUST be called first - parser depends on sanitized input"`,
82
+ inputSchema: {
83
+ type: 'object',
84
+ properties: {
85
+ notebookId: {
86
+ type: 'string',
87
+ description: 'Notebook entry ID to update (get from notebook-query or notebook-list)',
88
+ },
89
+ note: {
90
+ type: 'string',
91
+ description: 'New note content to replace the existing one',
92
+ },
93
+ },
94
+ required: ['notebookId', 'note'],
95
+ },
96
+ },
97
+ {
98
+ name: 'notebook-delete',
99
+ description: `🗑️ Delete an outdated or incorrect notebook entry.
100
+
101
+ **Core Purpose:** Remove notes that are no longer relevant or were recorded by mistake.
102
+
103
+ **When to use:**
104
+ - Code has been refactored and note is obsolete
105
+ - Note was recorded by mistake
106
+ - Workaround has been properly fixed
107
+ - Entry is duplicate or redundant
108
+
109
+ **Usage:**
110
+ 1. Use notebook-query or notebook-list to find the entry ID
111
+ 2. Call notebook-delete with the ID to remove it
112
+
113
+ **⚠️ Warning:** Deletion is permanent. Make sure the note is truly obsolete.`,
114
+ inputSchema: {
115
+ type: 'object',
116
+ properties: {
117
+ notebookId: {
118
+ type: 'string',
119
+ description: 'Notebook entry ID to delete (get from notebook-query)',
120
+ },
121
+ },
122
+ required: ['notebookId'],
123
+ },
124
+ },
125
+ {
126
+ name: 'notebook-list',
127
+ description: `📋 List all notebook entries for a specific file.
128
+
129
+ **Core Purpose:** View all notes associated with a particular file for management.
130
+
131
+ **When to use:**
132
+ - Need to see all notes for a file before editing
133
+ - Want to clean up old notes for a specific file
134
+ - Review constraints before making changes to a file
135
+
136
+ **Returns:** All notebook entries for the specified file, ordered by creation time.`,
137
+ inputSchema: {
138
+ type: 'object',
139
+ properties: {
140
+ filePath: {
141
+ type: 'string',
142
+ description: 'File path (relative or absolute) to list notebooks for',
143
+ },
144
+ },
145
+ required: ['filePath'],
146
+ },
147
+ },
148
+ ];
149
+ /**
150
+ * 执行 Notebook 工具
151
+ */
152
+ export async function executeNotebookTool(toolName, args) {
153
+ try {
154
+ switch (toolName) {
155
+ case 'notebook-add': {
156
+ const { filePath, note } = args;
157
+ if (!filePath || !note) {
158
+ return {
159
+ content: [
160
+ {
161
+ type: 'text',
162
+ text: 'Error: Both filePath and note are required',
163
+ },
164
+ ],
165
+ isError: true,
166
+ };
167
+ }
168
+ const entry = addNotebook(filePath, note);
169
+ return {
170
+ content: [
171
+ {
172
+ type: 'text',
173
+ text: JSON.stringify({
174
+ success: true,
175
+ message: `Notebook entry added for: ${entry.filePath}`,
176
+ entry: {
177
+ id: entry.id,
178
+ filePath: entry.filePath,
179
+ note: entry.note,
180
+ createdAt: entry.createdAt,
181
+ },
182
+ }, null, 2),
183
+ },
184
+ ],
185
+ };
186
+ }
187
+ case 'notebook-query': {
188
+ const { filePathPattern = '', topN = 10 } = args;
189
+ const results = queryNotebook(filePathPattern, topN);
190
+ if (results.length === 0) {
191
+ return {
192
+ content: [
193
+ {
194
+ type: 'text',
195
+ text: JSON.stringify({
196
+ message: 'No notebook entries found',
197
+ pattern: filePathPattern || '(all)',
198
+ totalResults: 0,
199
+ }, null, 2),
200
+ },
201
+ ],
202
+ };
203
+ }
204
+ return {
205
+ content: [
206
+ {
207
+ type: 'text',
208
+ text: JSON.stringify({
209
+ message: `Found ${results.length} notebook entries`,
210
+ pattern: filePathPattern || '(all)',
211
+ totalResults: results.length,
212
+ entries: results.map(entry => ({
213
+ id: entry.id,
214
+ filePath: entry.filePath,
215
+ note: entry.note,
216
+ createdAt: entry.createdAt,
217
+ })),
218
+ }, null, 2),
219
+ },
220
+ ],
221
+ };
222
+ }
223
+ case 'notebook-update': {
224
+ const { notebookId, note } = args;
225
+ if (!notebookId || !note) {
226
+ return {
227
+ content: [
228
+ {
229
+ type: 'text',
230
+ text: 'Error: Both notebookId and note are required',
231
+ },
232
+ ],
233
+ isError: true,
234
+ };
235
+ }
236
+ const updatedEntry = updateNotebook(notebookId, note);
237
+ if (!updatedEntry) {
238
+ return {
239
+ content: [
240
+ {
241
+ type: 'text',
242
+ text: JSON.stringify({
243
+ success: false,
244
+ message: `Notebook entry not found: ${notebookId}`,
245
+ }, null, 2),
246
+ },
247
+ ],
248
+ isError: true,
249
+ };
250
+ }
251
+ return {
252
+ content: [
253
+ {
254
+ type: 'text',
255
+ text: JSON.stringify({
256
+ success: true,
257
+ message: `Notebook entry updated: ${notebookId}`,
258
+ entry: {
259
+ id: updatedEntry.id,
260
+ filePath: updatedEntry.filePath,
261
+ note: updatedEntry.note,
262
+ updatedAt: updatedEntry.updatedAt,
263
+ },
264
+ }, null, 2),
265
+ },
266
+ ],
267
+ };
268
+ }
269
+ case 'notebook-delete': {
270
+ const { notebookId } = args;
271
+ if (!notebookId) {
272
+ return {
273
+ content: [
274
+ {
275
+ type: 'text',
276
+ text: 'Error: notebookId is required',
277
+ },
278
+ ],
279
+ isError: true,
280
+ };
281
+ }
282
+ const deleted = deleteNotebook(notebookId);
283
+ if (!deleted) {
284
+ return {
285
+ content: [
286
+ {
287
+ type: 'text',
288
+ text: JSON.stringify({
289
+ success: false,
290
+ message: `Notebook entry not found: ${notebookId}`,
291
+ }, null, 2),
292
+ },
293
+ ],
294
+ isError: true,
295
+ };
296
+ }
297
+ return {
298
+ content: [
299
+ {
300
+ type: 'text',
301
+ text: JSON.stringify({
302
+ success: true,
303
+ message: `Notebook entry deleted: ${notebookId}`,
304
+ }, null, 2),
305
+ },
306
+ ],
307
+ };
308
+ }
309
+ case 'notebook-list': {
310
+ const { filePath } = args;
311
+ if (!filePath) {
312
+ return {
313
+ content: [
314
+ {
315
+ type: 'text',
316
+ text: 'Error: filePath is required',
317
+ },
318
+ ],
319
+ isError: true,
320
+ };
321
+ }
322
+ const entries = getNotebooksByFile(filePath);
323
+ return {
324
+ content: [
325
+ {
326
+ type: 'text',
327
+ text: JSON.stringify({
328
+ message: entries.length > 0
329
+ ? `Found ${entries.length} notebook entries for: ${filePath}`
330
+ : `No notebook entries found for: ${filePath}`,
331
+ filePath,
332
+ totalEntries: entries.length,
333
+ entries: entries.map(entry => ({
334
+ id: entry.id,
335
+ note: entry.note,
336
+ createdAt: entry.createdAt,
337
+ updatedAt: entry.updatedAt,
338
+ })),
339
+ }, null, 2),
340
+ },
341
+ ],
342
+ };
343
+ }
344
+ default:
345
+ return {
346
+ content: [
347
+ {
348
+ type: 'text',
349
+ text: `Unknown notebook tool: ${toolName}`,
350
+ },
351
+ ],
352
+ isError: true,
353
+ };
354
+ }
355
+ }
356
+ catch (error) {
357
+ return {
358
+ content: [
359
+ {
360
+ type: 'text',
361
+ text: `Error executing notebook tool: ${error instanceof Error ? error.message : String(error)}`,
362
+ },
363
+ ],
364
+ isError: true,
365
+ };
366
+ }
367
+ }
@@ -7,6 +7,8 @@ registerCommand('ide', {
7
7
  if (vscodeConnection.isConnected()) {
8
8
  vscodeConnection.stop();
9
9
  vscodeConnection.resetReconnectAttempts();
10
+ // Wait a bit for cleanup to complete
11
+ await new Promise(resolve => setTimeout(resolve, 100));
10
12
  }
11
13
  // Try to connect to IDE plugin server
12
14
  try {
@@ -9,6 +9,7 @@ import { mcpTools as aceCodeSearchTools } from '../mcp/aceCodeSearch.js';
9
9
  import { mcpTools as websearchTools } from '../mcp/websearch.js';
10
10
  import { mcpTools as ideDiagnosticsTools } from '../mcp/ideDiagnostics.js';
11
11
  import { TodoService } from '../mcp/todo.js';
12
+ import { mcpTools as notebookTools, executeNotebookTool, } from '../mcp/notebook.js';
12
13
  import { getMCPTools as getSubAgentTools, subAgentService, } from '../mcp/subagent.js';
13
14
  import { sessionManager } from './sessionManager.js';
14
15
  import { logger } from './logger.js';
@@ -139,6 +140,28 @@ async function refreshToolsCache() {
139
140
  },
140
141
  });
141
142
  }
143
+ // Add built-in Notebook tools (always available)
144
+ const notebookServiceTools = notebookTools.map(tool => ({
145
+ name: tool.name.replace('notebook-', ''),
146
+ description: tool.description || '',
147
+ inputSchema: tool.inputSchema,
148
+ }));
149
+ servicesInfo.push({
150
+ serviceName: 'notebook',
151
+ tools: notebookServiceTools,
152
+ isBuiltIn: true,
153
+ connected: true,
154
+ });
155
+ for (const tool of notebookTools) {
156
+ allTools.push({
157
+ type: 'function',
158
+ function: {
159
+ name: tool.name,
160
+ description: tool.description || '',
161
+ parameters: tool.inputSchema,
162
+ },
163
+ });
164
+ }
142
165
  // Add built-in ACE Code Search tools (always available)
143
166
  const aceServiceTools = aceCodeSearchTools.map(tool => ({
144
167
  name: tool.name.replace('ace-', ''),
@@ -556,6 +579,10 @@ export async function executeMCPTool(toolName, args, abortSignal, onTokenUpdate)
556
579
  serviceName = 'todo';
557
580
  actualToolName = toolName.substring('todo-'.length);
558
581
  }
582
+ else if (toolName.startsWith('notebook-')) {
583
+ serviceName = 'notebook';
584
+ actualToolName = toolName.substring('notebook-'.length);
585
+ }
559
586
  else if (toolName.startsWith('filesystem-')) {
560
587
  serviceName = 'filesystem';
561
588
  actualToolName = toolName.substring('filesystem-'.length);
@@ -604,6 +631,10 @@ export async function executeMCPTool(toolName, args, abortSignal, onTokenUpdate)
604
631
  // Handle built-in TODO tools (no connection needed)
605
632
  return await todoService.executeTool(actualToolName, args);
606
633
  }
634
+ else if (serviceName === 'notebook') {
635
+ // Handle built-in Notebook tools (no connection needed)
636
+ return await executeNotebookTool(toolName, args);
637
+ }
607
638
  else if (serviceName === 'filesystem') {
608
639
  // Handle built-in filesystem tools (no connection needed)
609
640
  const { filesystemService } = await import('../mcp/filesystem.js');
@@ -0,0 +1,59 @@
1
+ /**
2
+ * 备忘录条目接口
3
+ */
4
+ export interface NotebookEntry {
5
+ id: string;
6
+ filePath: string;
7
+ note: string;
8
+ createdAt: string;
9
+ updatedAt: string;
10
+ }
11
+ /**
12
+ * 添加备忘录
13
+ * @param filePath 文件路径
14
+ * @param note 备忘说明
15
+ * @returns 添加的备忘录条目
16
+ */
17
+ export declare function addNotebook(filePath: string, note: string): NotebookEntry;
18
+ /**
19
+ * 查询备忘录
20
+ * @param filePathPattern 文件路径(支持模糊匹配)
21
+ * @param topN 返回最新的N条记录(默认10)
22
+ * @returns 匹配的备忘录条目列表
23
+ */
24
+ export declare function queryNotebook(filePathPattern?: string, topN?: number): NotebookEntry[];
25
+ /**
26
+ * 获取指定文件的所有备忘录
27
+ * @param filePath 文件路径
28
+ * @returns 该文件的所有备忘录
29
+ */
30
+ export declare function getNotebooksByFile(filePath: string): NotebookEntry[];
31
+ /**
32
+ * 更新备忘录内容
33
+ * @param notebookId 备忘录ID
34
+ * @param newNote 新的备忘说明
35
+ * @returns 更新后的备忘录条目,如果未找到则返回null
36
+ */
37
+ export declare function updateNotebook(notebookId: string, newNote: string): NotebookEntry | null;
38
+ /**
39
+ * 删除备忘录
40
+ * @param notebookId 备忘录ID
41
+ * @returns 是否删除成功
42
+ */
43
+ export declare function deleteNotebook(notebookId: string): boolean;
44
+ /**
45
+ * 清空指定文件的所有备忘录
46
+ * @param filePath 文件路径
47
+ */
48
+ export declare function clearNotebooksByFile(filePath: string): void;
49
+ /**
50
+ * 获取所有备忘录统计信息
51
+ */
52
+ export declare function getNotebookStats(): {
53
+ totalFiles: number;
54
+ totalEntries: number;
55
+ files: Array<{
56
+ path: string;
57
+ count: number;
58
+ }>;
59
+ };
@@ -0,0 +1,206 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ const MAX_ENTRIES_PER_FILE = 50;
4
+ /**
5
+ * 获取备忘录存储目录
6
+ */
7
+ function getNotebookDir() {
8
+ const projectRoot = process.cwd();
9
+ const notebookDir = path.join(projectRoot, '.snow', 'notebook');
10
+ if (!fs.existsSync(notebookDir)) {
11
+ fs.mkdirSync(notebookDir, { recursive: true });
12
+ }
13
+ return notebookDir;
14
+ }
15
+ /**
16
+ * 获取当前项目的备忘录文件路径
17
+ */
18
+ function getNotebookFilePath() {
19
+ const projectRoot = process.cwd();
20
+ const projectName = path.basename(projectRoot);
21
+ const notebookDir = getNotebookDir();
22
+ return path.join(notebookDir, `${projectName}.json`);
23
+ }
24
+ /**
25
+ * 读取备忘录数据
26
+ */
27
+ function readNotebookData() {
28
+ const filePath = getNotebookFilePath();
29
+ if (!fs.existsSync(filePath)) {
30
+ return {};
31
+ }
32
+ try {
33
+ const content = fs.readFileSync(filePath, 'utf-8');
34
+ return JSON.parse(content);
35
+ }
36
+ catch (error) {
37
+ console.error('Failed to read notebook data:', error);
38
+ return {};
39
+ }
40
+ }
41
+ /**
42
+ * 保存备忘录数据
43
+ */
44
+ function saveNotebookData(data) {
45
+ const filePath = getNotebookFilePath();
46
+ try {
47
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
48
+ }
49
+ catch (error) {
50
+ console.error('Failed to save notebook data:', error);
51
+ throw error;
52
+ }
53
+ }
54
+ /**
55
+ * 规范化文件路径(转换为相对于项目根目录的路径)
56
+ */
57
+ function normalizePath(filePath) {
58
+ const projectRoot = process.cwd();
59
+ // 如果是绝对路径,转换为相对路径
60
+ if (path.isAbsolute(filePath)) {
61
+ return path.relative(projectRoot, filePath).replace(/\\/g, '/');
62
+ }
63
+ // 已经是相对路径,规范化斜杠
64
+ return filePath.replace(/\\/g, '/');
65
+ }
66
+ /**
67
+ * 添加备忘录
68
+ * @param filePath 文件路径
69
+ * @param note 备忘说明
70
+ * @returns 添加的备忘录条目
71
+ */
72
+ export function addNotebook(filePath, note) {
73
+ const normalizedPath = normalizePath(filePath);
74
+ const data = readNotebookData();
75
+ if (!data[normalizedPath]) {
76
+ data[normalizedPath] = [];
77
+ }
78
+ // 创建新的备忘录条目(使用本地时间)
79
+ const now = new Date();
80
+ const localTimeStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}T${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}.${String(now.getMilliseconds()).padStart(3, '0')}`;
81
+ const entry = {
82
+ id: `notebook-${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
83
+ filePath: normalizedPath,
84
+ note,
85
+ createdAt: localTimeStr,
86
+ updatedAt: localTimeStr,
87
+ };
88
+ // 添加到数组开头(最新的在前面)
89
+ data[normalizedPath].unshift(entry);
90
+ // 限制每个文件最多50条备忘录
91
+ if (data[normalizedPath].length > MAX_ENTRIES_PER_FILE) {
92
+ data[normalizedPath] = data[normalizedPath].slice(0, MAX_ENTRIES_PER_FILE);
93
+ }
94
+ saveNotebookData(data);
95
+ return entry;
96
+ }
97
+ /**
98
+ * 查询备忘录
99
+ * @param filePathPattern 文件路径(支持模糊匹配)
100
+ * @param topN 返回最新的N条记录(默认10)
101
+ * @returns 匹配的备忘录条目列表
102
+ */
103
+ export function queryNotebook(filePathPattern = '', topN = 10) {
104
+ const data = readNotebookData();
105
+ const results = [];
106
+ // 规范化搜索模式
107
+ const normalizedPattern = filePathPattern.toLowerCase().replace(/\\/g, '/');
108
+ // 遍历所有文件路径
109
+ for (const [filePath, entries] of Object.entries(data)) {
110
+ // 如果没有指定模式,或者文件路径包含模式
111
+ if (!normalizedPattern ||
112
+ filePath.toLowerCase().includes(normalizedPattern)) {
113
+ results.push(...entries);
114
+ }
115
+ }
116
+ // 按创建时间倒序排序(最新的在前)
117
+ results.sort((a, b) => {
118
+ return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
119
+ });
120
+ // 返回 TopN 条记录
121
+ return results.slice(0, topN);
122
+ }
123
+ /**
124
+ * 获取指定文件的所有备忘录
125
+ * @param filePath 文件路径
126
+ * @returns 该文件的所有备忘录
127
+ */
128
+ export function getNotebooksByFile(filePath) {
129
+ const normalizedPath = normalizePath(filePath);
130
+ const data = readNotebookData();
131
+ return data[normalizedPath] || [];
132
+ }
133
+ /**
134
+ * 更新备忘录内容
135
+ * @param notebookId 备忘录ID
136
+ * @param newNote 新的备忘说明
137
+ * @returns 更新后的备忘录条目,如果未找到则返回null
138
+ */
139
+ export function updateNotebook(notebookId, newNote) {
140
+ const data = readNotebookData();
141
+ let updatedEntry = null;
142
+ for (const [, entries] of Object.entries(data)) {
143
+ const entry = entries.find(e => e.id === notebookId);
144
+ if (entry) {
145
+ // 更新笔记内容和更新时间
146
+ entry.note = newNote;
147
+ const now = new Date();
148
+ entry.updatedAt = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}T${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}.${String(now.getMilliseconds()).padStart(3, '0')}`;
149
+ updatedEntry = entry;
150
+ break;
151
+ }
152
+ }
153
+ if (updatedEntry) {
154
+ saveNotebookData(data);
155
+ }
156
+ return updatedEntry;
157
+ }
158
+ /**
159
+ * 删除备忘录
160
+ * @param notebookId 备忘录ID
161
+ * @returns 是否删除成功
162
+ */
163
+ export function deleteNotebook(notebookId) {
164
+ const data = readNotebookData();
165
+ let found = false;
166
+ for (const [, entries] of Object.entries(data)) {
167
+ const index = entries.findIndex(entry => entry.id === notebookId);
168
+ if (index !== -1) {
169
+ entries.splice(index, 1);
170
+ found = true;
171
+ break;
172
+ }
173
+ }
174
+ if (found) {
175
+ saveNotebookData(data);
176
+ }
177
+ return found;
178
+ }
179
+ /**
180
+ * 清空指定文件的所有备忘录
181
+ * @param filePath 文件路径
182
+ */
183
+ export function clearNotebooksByFile(filePath) {
184
+ const normalizedPath = normalizePath(filePath);
185
+ const data = readNotebookData();
186
+ if (data[normalizedPath]) {
187
+ delete data[normalizedPath];
188
+ saveNotebookData(data);
189
+ }
190
+ }
191
+ /**
192
+ * 获取所有备忘录统计信息
193
+ */
194
+ export function getNotebookStats() {
195
+ const data = readNotebookData();
196
+ const files = Object.entries(data).map(([path, entries]) => ({
197
+ path,
198
+ count: entries.length,
199
+ }));
200
+ const totalEntries = files.reduce((sum, file) => sum + file.count, 0);
201
+ return {
202
+ totalFiles: files.length,
203
+ totalEntries,
204
+ files: files.sort((a, b) => b.count - a.count),
205
+ };
206
+ }
@@ -70,8 +70,19 @@ export async function executeSubAgent(agentId, prompt, onMessage, abortSignal, r
70
70
  const sessionApprovedTools = new Set();
71
71
  // eslint-disable-next-line no-constant-condition
72
72
  while (true) {
73
- // Check abort signal
73
+ // Check abort signal before streaming
74
74
  if (abortSignal?.aborted) {
75
+ // Send done message to mark completion (like normal tool abort)
76
+ if (onMessage) {
77
+ onMessage({
78
+ type: 'sub_agent_message',
79
+ agentId: agent.id,
80
+ agentName: agent.name,
81
+ message: {
82
+ type: 'done',
83
+ },
84
+ });
85
+ }
75
86
  return {
76
87
  success: false,
77
88
  result: finalResponse,
@@ -200,6 +211,17 @@ export async function executeSubAgent(agentId, prompt, onMessage, abortSignal, r
200
211
  }
201
212
  // Handle rejected tools
202
213
  if (rejectedToolCalls.length > 0) {
214
+ // Send done message to mark completion when tools are rejected
215
+ if (onMessage) {
216
+ onMessage({
217
+ type: 'sub_agent_message',
218
+ agentId: agent.id,
219
+ agentName: agent.name,
220
+ message: {
221
+ type: 'done',
222
+ },
223
+ });
224
+ }
203
225
  return {
204
226
  success: false,
205
227
  result: finalResponse,
@@ -211,6 +233,25 @@ export async function executeSubAgent(agentId, prompt, onMessage, abortSignal, r
211
233
  // Execute approved tool calls
212
234
  const toolResults = [];
213
235
  for (const toolCall of approvedToolCalls) {
236
+ // Check abort signal before executing each tool
237
+ if (abortSignal?.aborted) {
238
+ // Send done message to mark completion
239
+ if (onMessage) {
240
+ onMessage({
241
+ type: 'sub_agent_message',
242
+ agentId: agent.id,
243
+ agentName: agent.name,
244
+ message: {
245
+ type: 'done',
246
+ },
247
+ });
248
+ }
249
+ return {
250
+ success: false,
251
+ result: finalResponse,
252
+ error: 'Sub-agent execution aborted during tool execution',
253
+ };
254
+ }
214
255
  try {
215
256
  const args = JSON.parse(toolCall.function.arguments);
216
257
  const result = await executeMCPTool(toolCall.function.name, args, abortSignal);
@@ -122,12 +122,20 @@ class VSCodeConnectionManager {
122
122
  const targetPort = this.findPortForWorkspace();
123
123
  // Create a new connection promise and store it
124
124
  this.connectingPromise = new Promise((resolve, reject) => {
125
+ let isSettled = false; // Prevent double resolve/reject
125
126
  // Set connection timeout
126
127
  this.connectionTimeout = setTimeout(() => {
127
- this.cleanupConnection();
128
- reject(new Error('Connection timeout after 10 seconds'));
128
+ if (!isSettled) {
129
+ isSettled = true;
130
+ this.cleanupConnection();
131
+ reject(new Error('Connection timeout after 10 seconds'));
132
+ }
129
133
  }, this.CONNECTION_TIMEOUT);
130
134
  const tryConnect = (port) => {
135
+ // If already settled (resolved or rejected), stop trying
136
+ if (isSettled) {
137
+ return;
138
+ }
131
139
  // Check both VSCode and JetBrains port ranges
132
140
  if (port > this.VSCODE_MAX_PORT && port < this.JETBRAINS_BASE_PORT) {
133
141
  // Jump from VSCode range to JetBrains range
@@ -135,23 +143,29 @@ class VSCodeConnectionManager {
135
143
  return;
136
144
  }
137
145
  if (port > this.JETBRAINS_MAX_PORT) {
138
- this.cleanupConnection();
139
- reject(new Error(`Failed to connect: no IDE server found on ports ${this.VSCODE_BASE_PORT}-${this.VSCODE_MAX_PORT} or ${this.JETBRAINS_BASE_PORT}-${this.JETBRAINS_MAX_PORT}`));
146
+ if (!isSettled) {
147
+ isSettled = true;
148
+ this.cleanupConnection();
149
+ reject(new Error(`Failed to connect: no IDE server found on ports ${this.VSCODE_BASE_PORT}-${this.VSCODE_MAX_PORT} or ${this.JETBRAINS_BASE_PORT}-${this.JETBRAINS_MAX_PORT}`));
150
+ }
140
151
  return;
141
152
  }
142
153
  try {
143
154
  this.client = new WebSocket(`ws://localhost:${port}`);
144
155
  this.client.on('open', () => {
145
- // Reset reconnect attempts on successful connection
146
- this.reconnectAttempts = 0;
147
- this.port = port;
148
- // Clear connection state
149
- if (this.connectionTimeout) {
150
- clearTimeout(this.connectionTimeout);
151
- this.connectionTimeout = null;
156
+ if (!isSettled) {
157
+ isSettled = true;
158
+ // Reset reconnect attempts on successful connection
159
+ this.reconnectAttempts = 0;
160
+ this.port = port;
161
+ // Clear connection state
162
+ if (this.connectionTimeout) {
163
+ clearTimeout(this.connectionTimeout);
164
+ this.connectionTimeout = null;
165
+ }
166
+ this.connectingPromise = null;
167
+ resolve();
152
168
  }
153
- this.connectingPromise = null;
154
- resolve();
155
169
  });
156
170
  this.client.on('message', message => {
157
171
  try {
@@ -167,19 +181,25 @@ class VSCodeConnectionManager {
167
181
  });
168
182
  this.client.on('close', () => {
169
183
  this.client = null;
170
- this.scheduleReconnect();
184
+ // Only schedule reconnect if this was an established connection (not initial scan)
185
+ if (this.reconnectAttempts > 0 || isSettled) {
186
+ this.scheduleReconnect();
187
+ }
171
188
  });
172
189
  this.client.on('error', _error => {
173
190
  // On initial connection, try next port
174
- if (this.reconnectAttempts === 0) {
191
+ if (this.reconnectAttempts === 0 && !isSettled) {
175
192
  this.client = null;
176
- tryConnect(port + 1);
193
+ // Small delay before trying next port to avoid rapid fire
194
+ setTimeout(() => tryConnect(port + 1), 50);
177
195
  }
178
196
  // For reconnections, silently handle and let close event trigger reconnect
179
197
  });
180
198
  }
181
199
  catch (error) {
182
- tryConnect(port + 1);
200
+ if (!isSettled) {
201
+ setTimeout(() => tryConnect(port + 1), 50);
202
+ }
183
203
  }
184
204
  };
185
205
  tryConnect(targetPort);
@@ -297,7 +317,7 @@ class VSCodeConnectionManager {
297
317
  clearTimeout(this.connectionTimeout);
298
318
  this.connectionTimeout = null;
299
319
  }
300
- // Clear connecting promise
320
+ // Clear connecting promise - this is critical for restart
301
321
  this.connectingPromise = null;
302
322
  if (this.client) {
303
323
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "snow-ai",
3
- "version": "0.3.29",
3
+ "version": "0.3.31",
4
4
  "description": "Intelligent Command Line Assistant powered by AI",
5
5
  "license": "MIT",
6
6
  "bin": {