snow-ai 0.3.29 → 0.3.30

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,16 @@ 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
+
169
180
  **Web Search:**
170
181
  - \`websearch-search\` - Search web for latest docs/solutions
171
182
  - \`websearch-fetch\` - Read web page content (always provide userQuery)
@@ -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 {
@@ -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,161 @@
1
+ import { addNotebook, queryNotebook } 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
+ /**
65
+ * 执行 Notebook 工具
66
+ */
67
+ export async function executeNotebookTool(toolName, args) {
68
+ try {
69
+ switch (toolName) {
70
+ case 'notebook-add': {
71
+ const { filePath, note } = args;
72
+ if (!filePath || !note) {
73
+ return {
74
+ content: [
75
+ {
76
+ type: 'text',
77
+ text: 'Error: Both filePath and note are required',
78
+ },
79
+ ],
80
+ isError: true,
81
+ };
82
+ }
83
+ const entry = addNotebook(filePath, note);
84
+ return {
85
+ content: [
86
+ {
87
+ type: 'text',
88
+ text: JSON.stringify({
89
+ success: true,
90
+ message: `Notebook entry added for: ${entry.filePath}`,
91
+ entry: {
92
+ id: entry.id,
93
+ filePath: entry.filePath,
94
+ note: entry.note,
95
+ createdAt: entry.createdAt,
96
+ },
97
+ }, null, 2),
98
+ },
99
+ ],
100
+ };
101
+ }
102
+ case 'notebook-query': {
103
+ const { filePathPattern = '', topN = 10 } = args;
104
+ const results = queryNotebook(filePathPattern, topN);
105
+ if (results.length === 0) {
106
+ return {
107
+ content: [
108
+ {
109
+ type: 'text',
110
+ text: JSON.stringify({
111
+ message: 'No notebook entries found',
112
+ pattern: filePathPattern || '(all)',
113
+ totalResults: 0,
114
+ }, null, 2),
115
+ },
116
+ ],
117
+ };
118
+ }
119
+ return {
120
+ content: [
121
+ {
122
+ type: 'text',
123
+ text: JSON.stringify({
124
+ message: `Found ${results.length} notebook entries`,
125
+ pattern: filePathPattern || '(all)',
126
+ totalResults: results.length,
127
+ entries: results.map(entry => ({
128
+ id: entry.id,
129
+ filePath: entry.filePath,
130
+ note: entry.note,
131
+ createdAt: entry.createdAt,
132
+ })),
133
+ }, null, 2),
134
+ },
135
+ ],
136
+ };
137
+ }
138
+ default:
139
+ return {
140
+ content: [
141
+ {
142
+ type: 'text',
143
+ text: `Unknown notebook tool: ${toolName}`,
144
+ },
145
+ ],
146
+ isError: true,
147
+ };
148
+ }
149
+ }
150
+ catch (error) {
151
+ return {
152
+ content: [
153
+ {
154
+ type: 'text',
155
+ text: `Error executing notebook tool: ${error instanceof Error ? error.message : String(error)}`,
156
+ },
157
+ ],
158
+ isError: true,
159
+ };
160
+ }
161
+ }
@@ -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,52 @@
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
+ * @returns 是否删除成功
35
+ */
36
+ export declare function deleteNotebook(notebookId: string): boolean;
37
+ /**
38
+ * 清空指定文件的所有备忘录
39
+ * @param filePath 文件路径
40
+ */
41
+ export declare function clearNotebooksByFile(filePath: string): void;
42
+ /**
43
+ * 获取所有备忘录统计信息
44
+ */
45
+ export declare function getNotebookStats(): {
46
+ totalFiles: number;
47
+ totalEntries: number;
48
+ files: Array<{
49
+ path: string;
50
+ count: number;
51
+ }>;
52
+ };
@@ -0,0 +1,181 @@
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
+ * @returns 是否删除成功
137
+ */
138
+ export function deleteNotebook(notebookId) {
139
+ const data = readNotebookData();
140
+ let found = false;
141
+ for (const [, entries] of Object.entries(data)) {
142
+ const index = entries.findIndex(entry => entry.id === notebookId);
143
+ if (index !== -1) {
144
+ entries.splice(index, 1);
145
+ found = true;
146
+ break;
147
+ }
148
+ }
149
+ if (found) {
150
+ saveNotebookData(data);
151
+ }
152
+ return found;
153
+ }
154
+ /**
155
+ * 清空指定文件的所有备忘录
156
+ * @param filePath 文件路径
157
+ */
158
+ export function clearNotebooksByFile(filePath) {
159
+ const normalizedPath = normalizePath(filePath);
160
+ const data = readNotebookData();
161
+ if (data[normalizedPath]) {
162
+ delete data[normalizedPath];
163
+ saveNotebookData(data);
164
+ }
165
+ }
166
+ /**
167
+ * 获取所有备忘录统计信息
168
+ */
169
+ export function getNotebookStats() {
170
+ const data = readNotebookData();
171
+ const files = Object.entries(data).map(([path, entries]) => ({
172
+ path,
173
+ count: entries.length,
174
+ }));
175
+ const totalEntries = files.reduce((sum, file) => sum + file.count, 0);
176
+ return {
177
+ totalFiles: files.length,
178
+ totalEntries,
179
+ files: files.sort((a, b) => b.count - a.count),
180
+ };
181
+ }
@@ -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.30",
4
4
  "description": "Intelligent Command Line Assistant powered by AI",
5
5
  "license": "MIT",
6
6
  "bin": {