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.
- package/dist/api/systemPrompt.js +119 -6
- package/dist/hooks/useCommandHandler.js +3 -8
- package/dist/hooks/useConversation.js +15 -0
- package/dist/hooks/useVSCodeState.js +4 -3
- package/dist/mcp/filesystem.d.ts +6 -0
- package/dist/mcp/filesystem.js +43 -0
- package/dist/mcp/notebook.d.ts +10 -0
- package/dist/mcp/notebook.js +367 -0
- package/dist/utils/commands/ide.js +2 -0
- package/dist/utils/mcpToolsManager.js +31 -0
- package/dist/utils/notebookManager.d.ts +59 -0
- package/dist/utils/notebookManager.js +206 -0
- package/dist/utils/subAgentExecutor.js +42 -1
- package/dist/utils/vscodeConnection.js +38 -18
- package/package.json +1 -1
package/dist/api/systemPrompt.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
158
|
-
//
|
|
159
|
-
|
|
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 (
|
|
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
|
-
},
|
|
70
|
+
}, 15000); // 15 seconds: 10s for connection timeout + 5s buffer
|
|
70
71
|
return () => {
|
|
71
72
|
clearTimeout(connectingTimeout);
|
|
72
73
|
};
|
package/dist/mcp/filesystem.d.ts
CHANGED
|
@@ -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
|
package/dist/mcp/filesystem.js
CHANGED
|
@@ -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
|
-
|
|
128
|
-
|
|
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
|
-
|
|
139
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
this.connectionTimeout
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|