snow-ai 0.3.6 → 0.3.7

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.
Files changed (101) hide show
  1. package/dist/agents/reviewAgent.d.ts +50 -0
  2. package/dist/agents/reviewAgent.js +264 -0
  3. package/dist/api/anthropic.js +104 -71
  4. package/dist/api/chat.d.ts +1 -1
  5. package/dist/api/chat.js +60 -41
  6. package/dist/api/gemini.js +97 -57
  7. package/dist/api/responses.d.ts +9 -1
  8. package/dist/api/responses.js +110 -70
  9. package/dist/api/systemPrompt.d.ts +1 -1
  10. package/dist/api/systemPrompt.js +36 -7
  11. package/dist/api/types.d.ts +8 -0
  12. package/dist/hooks/useCommandHandler.d.ts +1 -0
  13. package/dist/hooks/useCommandHandler.js +44 -1
  14. package/dist/hooks/useCommandPanel.js +13 -0
  15. package/dist/hooks/useConversation.d.ts +4 -1
  16. package/dist/hooks/useConversation.js +48 -6
  17. package/dist/hooks/useKeyboardInput.js +19 -0
  18. package/dist/hooks/useTerminalFocus.js +13 -3
  19. package/dist/mcp/aceCodeSearch.d.ts +2 -76
  20. package/dist/mcp/aceCodeSearch.js +31 -467
  21. package/dist/mcp/bash.d.ts +1 -8
  22. package/dist/mcp/bash.js +20 -40
  23. package/dist/mcp/filesystem.d.ts +3 -68
  24. package/dist/mcp/filesystem.js +32 -348
  25. package/dist/mcp/ideDiagnostics.js +2 -4
  26. package/dist/mcp/todo.d.ts +1 -17
  27. package/dist/mcp/todo.js +11 -15
  28. package/dist/mcp/types/aceCodeSearch.types.d.ts +92 -0
  29. package/dist/mcp/types/aceCodeSearch.types.js +4 -0
  30. package/dist/mcp/types/bash.types.d.ts +13 -0
  31. package/dist/mcp/types/bash.types.js +4 -0
  32. package/dist/mcp/types/filesystem.types.d.ts +44 -0
  33. package/dist/mcp/types/filesystem.types.js +4 -0
  34. package/dist/mcp/types/todo.types.d.ts +27 -0
  35. package/dist/mcp/types/todo.types.js +4 -0
  36. package/dist/mcp/types/websearch.types.d.ts +30 -0
  37. package/dist/mcp/types/websearch.types.js +4 -0
  38. package/dist/mcp/utils/aceCodeSearch/filesystem.utils.d.ts +34 -0
  39. package/dist/mcp/utils/aceCodeSearch/filesystem.utils.js +146 -0
  40. package/dist/mcp/utils/aceCodeSearch/language.utils.d.ts +14 -0
  41. package/dist/mcp/utils/aceCodeSearch/language.utils.js +99 -0
  42. package/dist/mcp/utils/aceCodeSearch/search.utils.d.ts +31 -0
  43. package/dist/mcp/utils/aceCodeSearch/search.utils.js +136 -0
  44. package/dist/mcp/utils/aceCodeSearch/symbol.utils.d.ts +20 -0
  45. package/dist/mcp/utils/aceCodeSearch/symbol.utils.js +141 -0
  46. package/dist/mcp/utils/bash/security.utils.d.ts +20 -0
  47. package/dist/mcp/utils/bash/security.utils.js +34 -0
  48. package/dist/mcp/utils/filesystem/code-analysis.utils.d.ts +18 -0
  49. package/dist/mcp/utils/filesystem/code-analysis.utils.js +165 -0
  50. package/dist/mcp/utils/filesystem/match-finder.utils.d.ts +16 -0
  51. package/dist/mcp/utils/filesystem/match-finder.utils.js +85 -0
  52. package/dist/mcp/utils/filesystem/similarity.utils.d.ts +22 -0
  53. package/dist/mcp/utils/filesystem/similarity.utils.js +75 -0
  54. package/dist/mcp/utils/todo/date.utils.d.ts +9 -0
  55. package/dist/mcp/utils/todo/date.utils.js +14 -0
  56. package/dist/mcp/utils/websearch/browser.utils.d.ts +8 -0
  57. package/dist/mcp/utils/websearch/browser.utils.js +58 -0
  58. package/dist/mcp/utils/websearch/text.utils.d.ts +16 -0
  59. package/dist/mcp/utils/websearch/text.utils.js +39 -0
  60. package/dist/mcp/websearch.d.ts +1 -31
  61. package/dist/mcp/websearch.js +21 -97
  62. package/dist/ui/components/ChatInput.d.ts +2 -1
  63. package/dist/ui/components/ChatInput.js +10 -3
  64. package/dist/ui/components/MarkdownRenderer.d.ts +1 -2
  65. package/dist/ui/components/MarkdownRenderer.js +16 -153
  66. package/dist/ui/components/MessageList.js +4 -4
  67. package/dist/ui/components/SessionListScreen.js +37 -17
  68. package/dist/ui/components/ToolResultPreview.js +6 -6
  69. package/dist/ui/components/UsagePanel.d.ts +2 -0
  70. package/dist/ui/components/UsagePanel.js +360 -0
  71. package/dist/ui/pages/ChatScreen.d.ts +4 -0
  72. package/dist/ui/pages/ChatScreen.js +70 -30
  73. package/dist/ui/pages/ConfigScreen.js +23 -19
  74. package/dist/ui/pages/HeadlessModeScreen.js +2 -4
  75. package/dist/ui/pages/SubAgentConfigScreen.js +17 -17
  76. package/dist/ui/pages/SystemPromptConfigScreen.js +7 -6
  77. package/dist/utils/commandExecutor.d.ts +3 -3
  78. package/dist/utils/commandExecutor.js +4 -4
  79. package/dist/utils/commands/home.d.ts +2 -0
  80. package/dist/utils/commands/home.js +12 -0
  81. package/dist/utils/commands/review.d.ts +2 -0
  82. package/dist/utils/commands/review.js +81 -0
  83. package/dist/utils/commands/role.d.ts +2 -0
  84. package/dist/utils/commands/role.js +37 -0
  85. package/dist/utils/commands/usage.d.ts +2 -0
  86. package/dist/utils/commands/usage.js +12 -0
  87. package/dist/utils/contextCompressor.js +99 -367
  88. package/dist/utils/fileUtils.js +3 -3
  89. package/dist/utils/mcpToolsManager.js +12 -12
  90. package/dist/utils/proxyUtils.d.ts +15 -0
  91. package/dist/utils/proxyUtils.js +50 -0
  92. package/dist/utils/retryUtils.d.ts +27 -0
  93. package/dist/utils/retryUtils.js +114 -2
  94. package/dist/utils/sessionManager.d.ts +2 -5
  95. package/dist/utils/sessionManager.js +16 -83
  96. package/dist/utils/terminal.js +4 -3
  97. package/dist/utils/usageLogger.d.ts +11 -0
  98. package/dist/utils/usageLogger.js +99 -0
  99. package/package.json +3 -7
  100. package/dist/agents/summaryAgent.d.ts +0 -31
  101. package/dist/agents/summaryAgent.js +0 -256
@@ -9,7 +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 { getMCPTools as getSubAgentTools, subAgentService } from '../mcp/subagent.js';
12
+ import { getMCPTools as getSubAgentTools, subAgentService, } from '../mcp/subagent.js';
13
13
  import { sessionManager } from './sessionManager.js';
14
14
  import { logger } from './logger.js';
15
15
  import { resourceMonitor } from './resourceMonitor.js';
@@ -69,7 +69,7 @@ async function refreshToolsCache() {
69
69
  const servicesInfo = [];
70
70
  // Add built-in filesystem tools (always available)
71
71
  const filesystemServiceTools = filesystemTools.map(tool => ({
72
- name: tool.name.replace('filesystem_', ''),
72
+ name: tool.name.replace('filesystem-', ''),
73
73
  description: tool.description,
74
74
  inputSchema: tool.inputSchema,
75
75
  }));
@@ -83,7 +83,7 @@ async function refreshToolsCache() {
83
83
  allTools.push({
84
84
  type: 'function',
85
85
  function: {
86
- name: `filesystem-${tool.name.replace('filesystem_', '')}`,
86
+ name: tool.name,
87
87
  description: tool.description,
88
88
  parameters: tool.inputSchema,
89
89
  },
@@ -91,7 +91,7 @@ async function refreshToolsCache() {
91
91
  }
92
92
  // Add built-in terminal tools (always available)
93
93
  const terminalServiceTools = terminalTools.map(tool => ({
94
- name: tool.name.replace('terminal_', ''),
94
+ name: tool.name.replace('terminal-', ''),
95
95
  description: tool.description,
96
96
  inputSchema: tool.inputSchema,
97
97
  }));
@@ -105,7 +105,7 @@ async function refreshToolsCache() {
105
105
  allTools.push({
106
106
  type: 'function',
107
107
  function: {
108
- name: `terminal-${tool.name.replace('terminal_', '')}`,
108
+ name: tool.name,
109
109
  description: tool.description,
110
110
  parameters: tool.inputSchema,
111
111
  },
@@ -137,7 +137,7 @@ async function refreshToolsCache() {
137
137
  }
138
138
  // Add built-in ACE Code Search tools (always available)
139
139
  const aceServiceTools = aceCodeSearchTools.map(tool => ({
140
- name: tool.name.replace('ace_', ''),
140
+ name: tool.name.replace('ace-', ''),
141
141
  description: tool.description,
142
142
  inputSchema: tool.inputSchema,
143
143
  }));
@@ -151,7 +151,7 @@ async function refreshToolsCache() {
151
151
  allTools.push({
152
152
  type: 'function',
153
153
  function: {
154
- name: `ace-${tool.name.replace('ace_', '')}`,
154
+ name: tool.name,
155
155
  description: tool.description,
156
156
  parameters: tool.inputSchema,
157
157
  },
@@ -159,7 +159,7 @@ async function refreshToolsCache() {
159
159
  }
160
160
  // Add built-in Web Search tools (always available)
161
161
  const websearchServiceTools = websearchTools.map(tool => ({
162
- name: tool.name.replace('websearch_', ''),
162
+ name: tool.name.replace('websearch-', ''),
163
163
  description: tool.description,
164
164
  inputSchema: tool.inputSchema,
165
165
  }));
@@ -173,7 +173,7 @@ async function refreshToolsCache() {
173
173
  allTools.push({
174
174
  type: 'function',
175
175
  function: {
176
- name: `websearch-${tool.name.replace('websearch_', '')}`,
176
+ name: tool.name,
177
177
  description: tool.description,
178
178
  parameters: tool.inputSchema,
179
179
  },
@@ -181,7 +181,7 @@ async function refreshToolsCache() {
181
181
  }
182
182
  // Add built-in IDE Diagnostics tools (always available)
183
183
  const ideDiagnosticsServiceTools = ideDiagnosticsTools.map(tool => ({
184
- name: tool.name.replace('ide_', ''),
184
+ name: tool.name.replace('ide-', ''),
185
185
  description: tool.description,
186
186
  inputSchema: tool.inputSchema,
187
187
  }));
@@ -195,7 +195,7 @@ async function refreshToolsCache() {
195
195
  allTools.push({
196
196
  type: 'function',
197
197
  function: {
198
- name: `ide-${tool.name.replace('ide_', '')}`,
198
+ name: tool.name,
199
199
  description: tool.description,
200
200
  parameters: tool.inputSchema,
201
201
  },
@@ -598,7 +598,7 @@ export async function executeMCPTool(toolName, args, abortSignal, onTokenUpdate)
598
598
  }
599
599
  if (serviceName === 'todo') {
600
600
  // Handle built-in TODO tools (no connection needed)
601
- return await todoService.executeTool(toolName, args);
601
+ return await todoService.executeTool(actualToolName, args);
602
602
  }
603
603
  else if (serviceName === 'filesystem') {
604
604
  // Handle built-in filesystem tools (no connection needed)
@@ -0,0 +1,15 @@
1
+ import type { Agent as HttpAgent } from 'http';
2
+ import type { Agent as HttpsAgent } from 'https';
3
+ /**
4
+ * 创建代理 Agent(如果启用了代理)
5
+ * @param targetUrl - 目标 URL,用于判断是否使用 HTTPS
6
+ * @returns HTTP/HTTPS Agent,如果未启用代理则返回 undefined
7
+ */
8
+ export declare function createProxyAgent(targetUrl: string): HttpAgent | HttpsAgent | undefined;
9
+ /**
10
+ * 为 fetch 请求添加代理支持
11
+ * @param url - 请求 URL
12
+ * @param options - fetch 选项
13
+ * @returns 添加了代理支持的 fetch 选项
14
+ */
15
+ export declare function addProxyToFetchOptions(url: string, options?: RequestInit): RequestInit;
@@ -0,0 +1,50 @@
1
+ import { getProxyConfig } from './apiConfig.js';
2
+ import { HttpsProxyAgent } from 'https-proxy-agent';
3
+ import { HttpProxyAgent } from 'http-proxy-agent';
4
+ /**
5
+ * 创建代理 Agent(如果启用了代理)
6
+ * @param targetUrl - 目标 URL,用于判断是否使用 HTTPS
7
+ * @returns HTTP/HTTPS Agent,如果未启用代理则返回 undefined
8
+ */
9
+ export function createProxyAgent(targetUrl) {
10
+ const proxyConfig = getProxyConfig();
11
+ // 如果代理未启用,直接返回 undefined
12
+ if (!proxyConfig.enabled) {
13
+ return undefined;
14
+ }
15
+ // 构建代理 URL
16
+ const proxyUrl = `http://127.0.0.1:${proxyConfig.port}`;
17
+ // 根据目标 URL 协议选择合适的代理 Agent
18
+ try {
19
+ const url = new URL(targetUrl);
20
+ if (url.protocol === 'https:') {
21
+ return new HttpsProxyAgent(proxyUrl);
22
+ }
23
+ else {
24
+ return new HttpProxyAgent(proxyUrl);
25
+ }
26
+ }
27
+ catch (error) {
28
+ // URL 解析失败,默认使用 HTTPS
29
+ return new HttpsProxyAgent(proxyUrl);
30
+ }
31
+ }
32
+ /**
33
+ * 为 fetch 请求添加代理支持
34
+ * @param url - 请求 URL
35
+ * @param options - fetch 选项
36
+ * @returns 添加了代理支持的 fetch 选项
37
+ */
38
+ export function addProxyToFetchOptions(url, options = {}) {
39
+ const agent = createProxyAgent(url);
40
+ if (!agent) {
41
+ return options;
42
+ }
43
+ // 添加 agent 到 fetch 选项
44
+ // 注意:Node.js 的 fetch 支持 dispatcher 选项
45
+ return {
46
+ ...options,
47
+ // @ts-ignore - Node.js fetch 支持 agent
48
+ agent,
49
+ };
50
+ }
@@ -20,3 +20,30 @@ export declare function withRetry<T>(fn: () => Promise<T>, options?: RetryOption
20
20
  * 注意:如果生成器已经开始产生数据,则不会重试
21
21
  */
22
22
  export declare function withRetryGenerator<T>(fn: () => AsyncGenerator<T, void, unknown>, options?: RetryOptions): AsyncGenerator<T, void, unknown>;
23
+ /**
24
+ * JSON 解析结果
25
+ */
26
+ export interface JsonParseResult<T = any> {
27
+ success: boolean;
28
+ data?: T;
29
+ error?: Error;
30
+ wasFixed?: boolean;
31
+ originalJson?: string;
32
+ fixedJson?: string;
33
+ }
34
+ /**
35
+ * 尝试解析 JSON,如果失败则尝试修复常见的 JSON 错误
36
+ * @param jsonString - 要解析的 JSON 字符串
37
+ * @param options - 配置选项
38
+ * @returns 解析结果
39
+ */
40
+ export declare function parseJsonWithFix<T = any>(jsonString: string, options?: {
41
+ /** 是否在修复成功时记录警告 */
42
+ logWarning?: boolean;
43
+ /** 是否在修复失败时记录错误 */
44
+ logError?: boolean;
45
+ /** 工具名称(用于日志) */
46
+ toolName?: string;
47
+ /** 失败时的回退值 */
48
+ fallbackValue?: T;
49
+ }): JsonParseResult<T>;
@@ -5,6 +5,7 @@
5
5
  * - 延时递增策略 (1s, 2s, 4s, 8s, 16s)
6
6
  * - 支持 AbortSignal 中断
7
7
  */
8
+ import { logger } from './logger.js';
8
9
  /**
9
10
  * 延时函数,支持 AbortSignal 中断
10
11
  */
@@ -48,8 +49,11 @@ function isRetriableError(error) {
48
49
  errorMessage.includes('429')) {
49
50
  return true;
50
51
  }
51
- // Server errors (5xx)
52
- if (errorMessage.includes('500') ||
52
+ // Server errors
53
+ if (errorMessage.includes('400') ||
54
+ errorMessage.includes('403') ||
55
+ errorMessage.includes('405') ||
56
+ errorMessage.includes('500') ||
53
57
  errorMessage.includes('502') ||
54
58
  errorMessage.includes('503') ||
55
59
  errorMessage.includes('504') ||
@@ -189,3 +193,111 @@ export async function* withRetryGenerator(fn, options = {}) {
189
193
  // 不应该到达这里
190
194
  throw lastError || new Error('Retry failed');
191
195
  }
196
+ /**
197
+ * 尝试解析 JSON,如果失败则尝试修复常见的 JSON 错误
198
+ * @param jsonString - 要解析的 JSON 字符串
199
+ * @param options - 配置选项
200
+ * @returns 解析结果
201
+ */
202
+ export function parseJsonWithFix(jsonString, options = {}) {
203
+ const { logWarning = true, logError = true, toolName = 'unknown', fallbackValue, } = options;
204
+ // 首先尝试直接解析
205
+ try {
206
+ const data = JSON.parse(jsonString);
207
+ return { success: true, data };
208
+ }
209
+ catch (originalError) {
210
+ // 解析失败,尝试修复
211
+ let fixedJson = jsonString;
212
+ let wasFixed = false;
213
+ // Fix 1: 移除格式错误的模式,如 "endLine":685 ": ""
214
+ // 处理值后面有额外冒号和引号的情况
215
+ const malformedPattern = /(\"[\w]+\"\s*:\s*[^,}\]]+)\s*\":\s*\"[^\"]*\"/g;
216
+ if (malformedPattern.test(fixedJson)) {
217
+ fixedJson = fixedJson.replace(malformedPattern, '$1');
218
+ wasFixed = true;
219
+ }
220
+ // Fix 2: 移除闭合括号前的尾随逗号
221
+ if (/,(\s*[}\]])/.test(fixedJson)) {
222
+ fixedJson = fixedJson.replace(/,(\s*[}\]])/g, '$1');
223
+ wasFixed = true;
224
+ }
225
+ // Fix 3: 修复属性名缺少引号的问题
226
+ if (/{\s*\w+\s*:/.test(fixedJson)) {
227
+ fixedJson = fixedJson.replace(/{\s*(\w+)\s*:/g, '{"$1":');
228
+ fixedJson = fixedJson.replace(/,\s*(\w+)\s*:/g, ',"$1":');
229
+ wasFixed = true;
230
+ }
231
+ // Fix 4: 添加缺失的闭合括号
232
+ const openBraces = (fixedJson.match(/{/g) || []).length;
233
+ const closeBraces = (fixedJson.match(/}/g) || []).length;
234
+ const openBrackets = (fixedJson.match(/\[/g) || []).length;
235
+ const closeBrackets = (fixedJson.match(/\]/g) || []).length;
236
+ if (openBraces > closeBraces) {
237
+ fixedJson += '}'.repeat(openBraces - closeBraces);
238
+ wasFixed = true;
239
+ }
240
+ if (openBrackets > closeBrackets) {
241
+ fixedJson += ']'.repeat(openBrackets - closeBrackets);
242
+ wasFixed = true;
243
+ }
244
+ // Fix 5: 移除多余的闭合括号
245
+ if (closeBraces > openBraces) {
246
+ const extraBraces = closeBraces - openBraces;
247
+ for (let i = 0; i < extraBraces; i++) {
248
+ fixedJson = fixedJson.replace(/}([^}]*)$/, '$1');
249
+ }
250
+ wasFixed = true;
251
+ }
252
+ if (closeBrackets > openBrackets) {
253
+ const extraBrackets = closeBrackets - openBrackets;
254
+ for (let i = 0; i < extraBrackets; i++) {
255
+ fixedJson = fixedJson.replace(/\]([^\]]*)$/, '$1');
256
+ }
257
+ wasFixed = true;
258
+ }
259
+ // 尝试解析修复后的 JSON
260
+ try {
261
+ const data = JSON.parse(fixedJson);
262
+ if (wasFixed && logWarning) {
263
+ logger.warn(`Warning: Fixed malformed JSON for ${toolName}`);
264
+ }
265
+ return {
266
+ success: true,
267
+ data,
268
+ wasFixed,
269
+ originalJson: jsonString,
270
+ fixedJson,
271
+ };
272
+ }
273
+ catch (fixError) {
274
+ // 修复失败
275
+ if (logError) {
276
+ logger.error(`Error: Failed to parse JSON for ${toolName}`);
277
+ logger.error(`Original: ${jsonString}`);
278
+ if (wasFixed) {
279
+ logger.error(`After fixes: ${fixedJson}`);
280
+ }
281
+ logger.error(`Parse error: ${fixError instanceof Error ? fixError.message : 'Unknown'}`);
282
+ }
283
+ // 如果提供了回退值,使用回退值
284
+ if (fallbackValue !== undefined) {
285
+ return {
286
+ success: false,
287
+ data: fallbackValue,
288
+ error: fixError instanceof Error ? fixError : new Error(String(fixError)),
289
+ wasFixed,
290
+ originalJson: jsonString,
291
+ fixedJson: wasFixed ? fixedJson : undefined,
292
+ };
293
+ }
294
+ return {
295
+ success: false,
296
+ error: fixError instanceof Error ? fixError : new Error(String(fixError)),
297
+ wasFixed,
298
+ originalJson: jsonString,
299
+ fixedJson: wasFixed ? fixedJson : undefined,
300
+ };
301
+ }
302
+ }
303
+ }
@@ -22,17 +22,14 @@ export interface SessionListItem {
22
22
  declare class SessionManager {
23
23
  private readonly sessionsDir;
24
24
  private currentSession;
25
- private summaryAbortController;
26
- private summaryTimeoutId;
27
25
  constructor();
28
26
  private ensureSessionsDir;
29
27
  private getSessionPath;
30
28
  private formatDateForFolder;
31
29
  /**
32
- * Cancel any ongoing summary generation
33
- * This prevents wasted resources and race conditions
30
+ * Clean title by removing newlines and extra spaces
34
31
  */
35
- private cancelOngoingSummaryGeneration;
32
+ private cleanTitle;
36
33
  createNewSession(): Promise<Session>;
37
34
  saveSession(session: Session): Promise<void>;
38
35
  loadSession(sessionId: string): Promise<Session | null>;
@@ -2,8 +2,8 @@ import fs from 'fs/promises';
2
2
  import path from 'path';
3
3
  import os from 'os';
4
4
  import { randomUUID } from 'crypto';
5
- import { summaryAgent } from '../agents/summaryAgent.js';
6
5
  import { getTodoService } from './mcpToolsManager.js';
6
+ import { logger } from './logger.js';
7
7
  class SessionManager {
8
8
  constructor() {
9
9
  Object.defineProperty(this, "sessionsDir", {
@@ -18,18 +18,6 @@ class SessionManager {
18
18
  writable: true,
19
19
  value: null
20
20
  });
21
- Object.defineProperty(this, "summaryAbortController", {
22
- enumerable: true,
23
- configurable: true,
24
- writable: true,
25
- value: null
26
- });
27
- Object.defineProperty(this, "summaryTimeoutId", {
28
- enumerable: true,
29
- configurable: true,
30
- writable: true,
31
- value: null
32
- });
33
21
  this.sessionsDir = path.join(os.homedir(), '.snow', 'sessions');
34
22
  }
35
23
  async ensureSessionsDir(date) {
@@ -58,18 +46,13 @@ class SessionManager {
58
46
  return `${year}-${month}-${day}`;
59
47
  }
60
48
  /**
61
- * Cancel any ongoing summary generation
62
- * This prevents wasted resources and race conditions
49
+ * Clean title by removing newlines and extra spaces
63
50
  */
64
- cancelOngoingSummaryGeneration() {
65
- if (this.summaryAbortController) {
66
- this.summaryAbortController.abort();
67
- this.summaryAbortController = null;
68
- }
69
- if (this.summaryTimeoutId) {
70
- clearTimeout(this.summaryTimeoutId);
71
- this.summaryTimeoutId = null;
72
- }
51
+ cleanTitle(title) {
52
+ return title
53
+ .replace(/[\r\n]+/g, ' ') // Replace newlines with space
54
+ .replace(/\s+/g, ' ') // Replace multiple spaces with single space
55
+ .trim(); // Remove leading/trailing spaces
73
56
  }
74
57
  async createNewSession() {
75
58
  await this.ensureSessionsDir(new Date());
@@ -165,7 +148,7 @@ class SessionManager {
165
148
  const session = JSON.parse(data);
166
149
  sessions.push({
167
150
  id: session.id,
168
- title: session.title,
151
+ title: this.cleanTitle(session.title),
169
152
  summary: session.summary,
170
153
  createdAt: session.createdAt,
171
154
  updatedAt: session.updatedAt,
@@ -196,7 +179,7 @@ class SessionManager {
196
179
  const session = JSON.parse(data);
197
180
  sessions.push({
198
181
  id: session.id,
199
- title: session.title,
182
+ title: this.cleanTitle(session.title),
200
183
  summary: session.summary,
201
184
  createdAt: session.createdAt,
202
185
  updatedAt: session.updatedAt,
@@ -260,59 +243,13 @@ class SessionManager {
260
243
  this.currentSession.messages.push(message);
261
244
  this.currentSession.messageCount = this.currentSession.messages.length;
262
245
  this.currentSession.updatedAt = Date.now();
263
- // Generate summary from first user message using summaryAgent (parallel, non-blocking)
246
+ // Generate simple title and summary from first user message
264
247
  if (this.currentSession.messageCount === 1 && message.role === 'user') {
265
- // Set temporary title immediately (synchronous)
266
- this.currentSession.title = message.content.slice(0, 50);
267
- this.currentSession.summary = message.content.slice(0, 100);
268
- // Cancel any previous summary generation (防呆机制)
269
- this.cancelOngoingSummaryGeneration();
270
- // Create new AbortController for this summary generation
271
- this.summaryAbortController = new AbortController();
272
- const currentSessionId = this.currentSession.id;
273
- const abortSignal = this.summaryAbortController.signal;
274
- // Set timeout to cancel summary generation after 30 seconds (防呆机制)
275
- this.summaryTimeoutId = setTimeout(() => {
276
- if (this.summaryAbortController) {
277
- console.warn('Summary generation timeout after 30s, aborting...');
278
- this.summaryAbortController.abort();
279
- this.summaryAbortController = null;
280
- }
281
- }, 30000);
282
- // Generate better summary in parallel (non-blocking)
283
- // This won't delay the main conversation flow
284
- summaryAgent
285
- .generateSummary(message.content, abortSignal)
286
- .then(summary => {
287
- // 防呆检查:确保会话没有被切换,且仍然是第一条消息
288
- if (this.currentSession &&
289
- this.currentSession.id === currentSessionId &&
290
- this.currentSession.messageCount === 1) {
291
- // Only update if this is still the first message in the same session
292
- this.currentSession.title = summary;
293
- this.currentSession.summary = summary;
294
- this.saveSession(this.currentSession).catch(error => {
295
- console.error('Failed to save session with generated summary:', error);
296
- });
297
- }
298
- // Clean up
299
- this.cancelOngoingSummaryGeneration();
300
- })
301
- .catch(error => {
302
- // Clean up on error
303
- this.cancelOngoingSummaryGeneration();
304
- // Silently fail if aborted (expected behavior)
305
- if (error.name === 'AbortError' || abortSignal.aborted) {
306
- console.log('Summary generation cancelled (expected)');
307
- return;
308
- }
309
- // Log other errors - we already have a fallback title/summary
310
- console.warn('Summary generation failed, using fallback:', error);
311
- });
312
- }
313
- else if (this.currentSession.messageCount > 1) {
314
- // 防呆机制:如果不是第一条消息,取消任何正在进行的摘要生成
315
- this.cancelOngoingSummaryGeneration();
248
+ // Use first 50 chars as title, first 100 chars as summary
249
+ const title = message.content.slice(0, 50) + (message.content.length > 50 ? '...' : '');
250
+ const summary = message.content.slice(0, 100) + (message.content.length > 100 ? '...' : '');
251
+ this.currentSession.title = this.cleanTitle(title);
252
+ this.currentSession.summary = this.cleanTitle(summary);
316
253
  }
317
254
  await this.saveSession(this.currentSession);
318
255
  }
@@ -320,13 +257,9 @@ class SessionManager {
320
257
  return this.currentSession;
321
258
  }
322
259
  setCurrentSession(session) {
323
- // 防呆机制:切换会话时取消正在进行的摘要生成
324
- this.cancelOngoingSummaryGeneration();
325
260
  this.currentSession = session;
326
261
  }
327
262
  clearCurrentSession() {
328
- // 防呆机制:清除会话时取消正在进行的摘要生成
329
- this.cancelOngoingSummaryGeneration();
330
263
  this.currentSession = null;
331
264
  }
332
265
  async deleteSession(sessionId) {
@@ -374,7 +307,7 @@ class SessionManager {
374
307
  }
375
308
  catch (error) {
376
309
  // TODO删除失败不影响会话删除结果
377
- console.warn(`Failed to delete TODO list for session ${sessionId}:`, error);
310
+ logger.warn(`Failed to delete TODO list for session ${sessionId}:`, error);
378
311
  }
379
312
  }
380
313
  return sessionDeleted;
@@ -6,7 +6,8 @@ export function resetTerminal(stream) {
6
6
  // RIS (Reset to Initial State) clears scrollback and resets terminal modes
7
7
  target.write('\x1bc');
8
8
  target.write('\x1B[3J\x1B[2J\x1B[H');
9
- // DO NOT re-enable focus reporting here
10
- // Let useTerminalFocus handle it when ChatScreen mounts
11
- // This avoids the race condition where focus event arrives before listener is ready
9
+ // Re-enable focus reporting immediately after terminal reset
10
+ // The RIS command (\x1bc) disables focus reporting, so we must re-enable it
11
+ // This ensures focus state tracking continues to work after /clear command
12
+ target.write('\x1b[?1004h');
12
13
  }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Save usage data to file system
3
+ * This is called directly from API layers to ensure all usage is tracked
4
+ */
5
+ export declare function saveUsageToFile(model: string, usage: {
6
+ prompt_tokens?: number;
7
+ completion_tokens?: number;
8
+ cache_creation_input_tokens?: number;
9
+ cache_read_input_tokens?: number;
10
+ cached_tokens?: number;
11
+ }): void;
@@ -0,0 +1,99 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
5
+ // 队列来避免并发写入冲突
6
+ let writeQueue = Promise.resolve();
7
+ async function getActiveProfile() {
8
+ try {
9
+ const homeDir = os.homedir();
10
+ const profilePath = path.join(homeDir, '.snow', 'active-profile.txt');
11
+ const profileName = await fs.readFile(profilePath, 'utf-8');
12
+ return profileName.trim();
13
+ }
14
+ catch (error) {
15
+ return 'default';
16
+ }
17
+ }
18
+ async function getUsageDir() {
19
+ const homeDir = os.homedir();
20
+ const snowDir = path.join(homeDir, '.snow', 'usage');
21
+ const today = new Date().toISOString().split('T')[0] || ''; // YYYY-MM-DD
22
+ const dateDir = path.join(snowDir, today);
23
+ // 确保目录存在
24
+ try {
25
+ await fs.mkdir(dateDir, { recursive: true });
26
+ }
27
+ catch (error) {
28
+ // 目录可能已存在,忽略错误
29
+ }
30
+ return dateDir;
31
+ }
32
+ async function getCurrentLogFile(dateDir) {
33
+ try {
34
+ const files = (await fs.readdir(dateDir)).filter(f => f.startsWith('usage-') && f.endsWith('.jsonl'));
35
+ if (files.length === 0) {
36
+ return path.join(dateDir, 'usage-001.jsonl');
37
+ }
38
+ // 按文件名排序,获取最新的文件
39
+ files.sort();
40
+ const latestFileName = files[files.length - 1];
41
+ if (!latestFileName) {
42
+ return path.join(dateDir, 'usage-001.jsonl');
43
+ }
44
+ const latestFile = path.join(dateDir, latestFileName);
45
+ // 检查文件大小
46
+ const stats = await fs.stat(latestFile);
47
+ if (stats.size >= MAX_FILE_SIZE) {
48
+ // 创建新文件
49
+ const match = latestFileName.match(/usage-(\d+)\.jsonl/);
50
+ const nextNum = match && match[1] ? parseInt(match[1]) + 1 : 1;
51
+ return path.join(dateDir, `usage-${String(nextNum).padStart(3, '0')}.jsonl`);
52
+ }
53
+ return latestFile;
54
+ }
55
+ catch (error) {
56
+ // 如果目录不存在或读取失败,返回默认文件名
57
+ return path.join(dateDir, 'usage-001.jsonl');
58
+ }
59
+ }
60
+ /**
61
+ * Save usage data to file system
62
+ * This is called directly from API layers to ensure all usage is tracked
63
+ */
64
+ export function saveUsageToFile(model, usage) {
65
+ // Add to write queue to avoid concurrent writes
66
+ writeQueue = writeQueue
67
+ .then(async () => {
68
+ try {
69
+ const profileName = await getActiveProfile();
70
+ const dateDir = await getUsageDir();
71
+ const logFile = await getCurrentLogFile(dateDir);
72
+ // Extract cache tokens (different API formats)
73
+ const cacheReadTokens = usage.cache_read_input_tokens ?? usage.cached_tokens;
74
+ // Only save non-sensitive data: model name, profile, and token counts
75
+ const record = {
76
+ model,
77
+ profileName,
78
+ inputTokens: usage.prompt_tokens || 0,
79
+ outputTokens: usage.completion_tokens || 0,
80
+ ...(usage.cache_creation_input_tokens !== undefined && {
81
+ cacheCreationInputTokens: usage.cache_creation_input_tokens,
82
+ }),
83
+ ...(cacheReadTokens !== undefined && {
84
+ cacheReadInputTokens: cacheReadTokens,
85
+ }),
86
+ timestamp: new Date().toISOString(),
87
+ };
88
+ // Append to file (JSONL format: one JSON object per line)
89
+ const line = JSON.stringify(record) + '\n';
90
+ await fs.appendFile(logFile, line, 'utf-8');
91
+ }
92
+ catch (error) {
93
+ console.error('Failed to save usage data:', error);
94
+ }
95
+ })
96
+ .catch(error => {
97
+ console.error('Usage persistence queue error:', error);
98
+ });
99
+ }