snow-ai 0.2.24 → 0.2.26

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 (46) hide show
  1. package/dist/api/chat.d.ts +0 -8
  2. package/dist/api/chat.js +1 -144
  3. package/dist/api/responses.d.ts +0 -11
  4. package/dist/api/responses.js +1 -189
  5. package/dist/api/systemPrompt.d.ts +1 -1
  6. package/dist/api/systemPrompt.js +90 -295
  7. package/dist/app.d.ts +2 -1
  8. package/dist/app.js +11 -13
  9. package/dist/cli.js +16 -3
  10. package/dist/hooks/useClipboard.js +4 -4
  11. package/dist/hooks/useGlobalNavigation.d.ts +1 -1
  12. package/dist/hooks/useKeyboardInput.d.ts +1 -0
  13. package/dist/hooks/useKeyboardInput.js +8 -4
  14. package/dist/hooks/useTerminalFocus.d.ts +5 -0
  15. package/dist/hooks/useTerminalFocus.js +22 -2
  16. package/dist/mcp/aceCodeSearch.d.ts +58 -4
  17. package/dist/mcp/aceCodeSearch.js +563 -20
  18. package/dist/mcp/filesystem.d.ts +59 -10
  19. package/dist/mcp/filesystem.js +431 -124
  20. package/dist/mcp/ideDiagnostics.d.ts +36 -0
  21. package/dist/mcp/ideDiagnostics.js +92 -0
  22. package/dist/ui/components/ChatInput.js +6 -3
  23. package/dist/ui/pages/ChatScreen.d.ts +4 -2
  24. package/dist/ui/pages/ChatScreen.js +31 -2
  25. package/dist/ui/pages/ConfigProfileScreen.d.ts +7 -0
  26. package/dist/ui/pages/ConfigProfileScreen.js +300 -0
  27. package/dist/ui/pages/{ApiConfigScreen.d.ts → ConfigScreen.d.ts} +1 -1
  28. package/dist/ui/pages/ConfigScreen.js +748 -0
  29. package/dist/ui/pages/WelcomeScreen.js +7 -18
  30. package/dist/utils/apiConfig.d.ts +0 -2
  31. package/dist/utils/apiConfig.js +12 -0
  32. package/dist/utils/configManager.d.ts +45 -0
  33. package/dist/utils/configManager.js +274 -0
  34. package/dist/utils/contextCompressor.js +355 -49
  35. package/dist/utils/escapeHandler.d.ts +79 -0
  36. package/dist/utils/escapeHandler.js +153 -0
  37. package/dist/utils/incrementalSnapshot.js +2 -1
  38. package/dist/utils/mcpToolsManager.js +44 -0
  39. package/dist/utils/retryUtils.js +6 -0
  40. package/dist/utils/textBuffer.js +13 -15
  41. package/dist/utils/vscodeConnection.js +26 -11
  42. package/dist/utils/workspaceSnapshot.js +2 -1
  43. package/package.json +2 -1
  44. package/dist/ui/pages/ApiConfigScreen.js +0 -161
  45. package/dist/ui/pages/ModelConfigScreen.d.ts +0 -8
  46. package/dist/ui/pages/ModelConfigScreen.js +0 -504
@@ -7,6 +7,7 @@ import { mcpTools as filesystemTools } from '../mcp/filesystem.js';
7
7
  import { mcpTools as terminalTools } from '../mcp/bash.js';
8
8
  import { mcpTools as aceCodeSearchTools } from '../mcp/aceCodeSearch.js';
9
9
  import { mcpTools as websearchTools } from '../mcp/websearch.js';
10
+ import { mcpTools as ideDiagnosticsTools } from '../mcp/ideDiagnostics.js';
10
11
  import { TodoService } from '../mcp/todo.js';
11
12
  import { sessionManager } from './sessionManager.js';
12
13
  import { logger } from './logger.js';
@@ -177,6 +178,28 @@ async function refreshToolsCache() {
177
178
  },
178
179
  });
179
180
  }
181
+ // Add built-in IDE Diagnostics tools (always available)
182
+ const ideDiagnosticsServiceTools = ideDiagnosticsTools.map(tool => ({
183
+ name: tool.name.replace('ide_', ''),
184
+ description: tool.description,
185
+ inputSchema: tool.inputSchema,
186
+ }));
187
+ servicesInfo.push({
188
+ serviceName: 'ide',
189
+ tools: ideDiagnosticsServiceTools,
190
+ isBuiltIn: true,
191
+ connected: true,
192
+ });
193
+ for (const tool of ideDiagnosticsTools) {
194
+ allTools.push({
195
+ type: 'function',
196
+ function: {
197
+ name: `ide-${tool.name.replace('ide_', '')}`,
198
+ description: tool.description,
199
+ parameters: tool.inputSchema,
200
+ },
201
+ });
202
+ }
180
203
  // Add user-configured MCP server tools (probe for availability but don't maintain connections)
181
204
  try {
182
205
  const mcpConfig = getMCPConfig();
@@ -519,6 +542,10 @@ export async function executeMCPTool(toolName, args, abortSignal, onTokenUpdate)
519
542
  serviceName = 'websearch';
520
543
  actualToolName = toolName.substring('websearch-'.length);
521
544
  }
545
+ else if (toolName.startsWith('ide-')) {
546
+ serviceName = 'ide';
547
+ actualToolName = toolName.substring('ide-'.length);
548
+ }
522
549
  else {
523
550
  // Check configured MCP services
524
551
  try {
@@ -625,6 +652,23 @@ export async function executeMCPTool(toolName, args, abortSignal, onTokenUpdate)
625
652
  throw new Error(`Unknown websearch tool: ${actualToolName}`);
626
653
  }
627
654
  }
655
+ else if (serviceName === 'ide') {
656
+ // Handle built-in IDE Diagnostics tools (no connection needed)
657
+ const { ideDiagnosticsService } = await import('../mcp/ideDiagnostics.js');
658
+ switch (actualToolName) {
659
+ case 'get_diagnostics':
660
+ const diagnostics = await ideDiagnosticsService.getDiagnostics(args.filePath);
661
+ // Format diagnostics for better readability
662
+ const formatted = ideDiagnosticsService.formatDiagnostics(diagnostics, args.filePath);
663
+ return {
664
+ diagnostics,
665
+ formatted,
666
+ summary: `Found ${diagnostics.length} diagnostic(s) in ${args.filePath}`,
667
+ };
668
+ default:
669
+ throw new Error(`Unknown IDE tool: ${actualToolName}`);
670
+ }
671
+ }
628
672
  else {
629
673
  // Handle user-configured MCP service tools - connect only when needed
630
674
  const mcpConfig = getMCPConfig();
@@ -64,6 +64,12 @@ function isRetriableError(error) {
64
64
  errorMessage.includes('unavailable')) {
65
65
  return true;
66
66
  }
67
+ // Connection terminated by server
68
+ if (errorMessage.includes('terminated') ||
69
+ errorMessage.includes('connection reset') ||
70
+ errorMessage.includes('socket hang up')) {
71
+ return true;
72
+ }
67
73
  return false;
68
74
  }
69
75
  /**
@@ -4,21 +4,16 @@ import { cpLen, cpSlice, visualWidth, toCodePoints } from './textUtils.js';
4
4
  */
5
5
  function sanitizeInput(str) {
6
6
  // Replace problematic characters but preserve basic formatting
7
- return str
7
+ return (str
8
8
  .replace(/\r\n/g, '\n') // Normalize line endings
9
9
  .replace(/\r/g, '\n') // Convert remaining \r to \n
10
10
  .replace(/\t/g, ' ') // Convert tabs to spaces
11
- // Remove focus events - complete escape sequences and standalone patterns
12
- // ESC[I and ESC[O are focus events from terminal focus in/out
13
- .replace(/\x1b\[I/g, '')
14
- .replace(/\x1b\[O/g, '')
15
- // Also remove standalone [I and [O if they appear at word boundaries
16
- // This catches cases where escape sequences arrive fragmented
17
- // But we preserve legitimate text like "FOO[I]BAR" or "[Inside]"
18
- .replace(/(?:^|\s)\[I(?:\s|$)/g, ' ')
19
- .replace(/(?:^|\s)\[O(?:\s|$)/g, ' ')
11
+ // Remove focus events emitted during terminal focus changes
12
+ .replace(/\x1b\[[IO]/g, '')
13
+ // Remove stray [I/[O] tokens that precede drag-and-drop payloads
14
+ .replace(/(^|\s+)\[(?:I|O)(?=(?:\s|$|["'~\\\/]|[A-Za-z]:))/g, '$1')
20
15
  // Remove control characters except newlines
21
- .replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
16
+ .replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, ''));
22
17
  }
23
18
  export class TextBuffer {
24
19
  constructor(viewport, onUpdate) {
@@ -145,7 +140,9 @@ export class TextBuffer {
145
140
  let fullText = this.content;
146
141
  for (const placeholder of this.pasteStorage.values()) {
147
142
  if (placeholder.placeholder) {
148
- fullText = fullText.split(placeholder.placeholder).join(placeholder.content);
143
+ fullText = fullText
144
+ .split(placeholder.placeholder)
145
+ .join(placeholder.content);
149
146
  }
150
147
  }
151
148
  return fullText;
@@ -258,7 +255,7 @@ export class TextBuffer {
258
255
  content: this.pasteAccumulator,
259
256
  charCount: totalChars,
260
257
  index: this.pasteCounter,
261
- placeholder: placeholderText
258
+ placeholder: placeholderText,
262
259
  });
263
260
  // 在记录的位置插入占位符
264
261
  const before = cpSlice(this.content, 0, this.pastePlaceholderPosition);
@@ -271,7 +268,8 @@ export class TextBuffer {
271
268
  const before = cpSlice(this.content, 0, this.pastePlaceholderPosition);
272
269
  const after = cpSlice(this.content, this.pastePlaceholderPosition);
273
270
  this.content = before + this.pasteAccumulator + after;
274
- this.cursorIndex = this.pastePlaceholderPosition + cpLen(this.pasteAccumulator);
271
+ this.cursorIndex =
272
+ this.pastePlaceholderPosition + cpLen(this.pasteAccumulator);
275
273
  }
276
274
  // 清理状态
277
275
  this.pasteAccumulator = '';
@@ -501,7 +499,7 @@ export class TextBuffer {
501
499
  data: base64Data,
502
500
  mimeType: mimeType,
503
501
  index: this.imageCounter,
504
- placeholder: placeholderText
502
+ placeholder: placeholderText,
505
503
  });
506
504
  this.insertPlainText(placeholderText);
507
505
  this.scheduleUpdate();
@@ -280,16 +280,21 @@ class VSCodeConnectionManager {
280
280
  return;
281
281
  }
282
282
  const requestId = Math.random().toString(36).substring(7);
283
+ let isResolved = false;
283
284
  const timeout = setTimeout(() => {
284
- cleanup();
285
- resolve([]); // Timeout, return empty array
286
- }, 5000); // 5 second timeout
285
+ if (!isResolved) {
286
+ cleanup();
287
+ resolve([]); // Timeout, return empty array
288
+ }
289
+ }, 2000); // Reduce timeout from 5s to 2s to avoid long blocking
287
290
  const handler = (message) => {
288
291
  try {
289
292
  const data = JSON.parse(message.toString());
290
293
  if (data.type === 'diagnostics' && data.requestId === requestId) {
291
- cleanup();
292
- resolve(data.diagnostics || []);
294
+ if (!isResolved) {
295
+ cleanup();
296
+ resolve(data.diagnostics || []);
297
+ }
293
298
  }
294
299
  }
295
300
  catch (error) {
@@ -297,15 +302,25 @@ class VSCodeConnectionManager {
297
302
  }
298
303
  };
299
304
  const cleanup = () => {
305
+ isResolved = true;
300
306
  clearTimeout(timeout);
301
- this.client?.removeListener('message', handler);
307
+ if (this.client) {
308
+ this.client.off('message', handler);
309
+ }
302
310
  };
303
311
  this.client.on('message', handler);
304
- this.client.send(JSON.stringify({
305
- type: 'getDiagnostics',
306
- requestId,
307
- filePath,
308
- }));
312
+ // Add error handling for send operation
313
+ try {
314
+ this.client.send(JSON.stringify({
315
+ type: 'getDiagnostics',
316
+ requestId,
317
+ filePath,
318
+ }));
319
+ }
320
+ catch (error) {
321
+ cleanup();
322
+ resolve([]); // If send fails, return empty array
323
+ }
309
324
  });
310
325
  }
311
326
  /**
@@ -2,6 +2,7 @@ import fs from 'fs/promises';
2
2
  import path from 'path';
3
3
  import os from 'os';
4
4
  import crypto from 'crypto';
5
+ import { logger } from '../utils/logger.js';
5
6
  /**
6
7
  * Workspace Snapshot Manager
7
8
  * Provides git-like version control for workspace files
@@ -196,7 +197,7 @@ class WorkspaceSnapshotManager {
196
197
  }
197
198
  }
198
199
  catch (error) {
199
- console.error('Failed to list snapshots:', error);
200
+ logger.error('Failed to list snapshots:', error);
200
201
  }
201
202
  return snapshots.sort((a, b) => b.messageIndex - a.messageIndex);
202
203
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "snow-ai",
3
- "version": "0.2.24",
3
+ "version": "0.2.26",
4
4
  "description": "Intelligent Command Line Assistant powered by AI",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -47,6 +47,7 @@
47
47
  "cli-highlight": "^2.1.11",
48
48
  "diff": "^8.0.2",
49
49
  "figlet": "^1.8.2",
50
+ "fzf": "^0.5.2",
50
51
  "https-proxy-agent": "^7.0.6",
51
52
  "ink": "^5.2.1",
52
53
  "ink-gradient": "^3.0.0",
@@ -1,161 +0,0 @@
1
- import React, { useState, useEffect } from 'react';
2
- import { Box, Text, useInput } from 'ink';
3
- import Gradient from 'ink-gradient';
4
- import { Select, Alert } from '@inkjs/ui';
5
- import TextInput from 'ink-text-input';
6
- import { getOpenAiConfig, updateOpenAiConfig, validateApiConfig, } from '../../utils/apiConfig.js';
7
- export default function ApiConfigScreen({ onBack, onSave, inlineMode = false }) {
8
- const [baseUrl, setBaseUrl] = useState('');
9
- const [apiKey, setApiKey] = useState('');
10
- const [requestMethod, setRequestMethod] = useState('chat');
11
- const [anthropicBeta, setAnthropicBeta] = useState(false);
12
- const [currentField, setCurrentField] = useState('baseUrl');
13
- const [errors, setErrors] = useState([]);
14
- const [isEditing, setIsEditing] = useState(false);
15
- const requestMethodOptions = [
16
- {
17
- label: 'Chat Completions - Modern chat API (GPT-4, GPT-3.5-turbo)',
18
- value: 'chat',
19
- },
20
- {
21
- label: 'Responses - New responses API (2025, with built-in tools)',
22
- value: 'responses',
23
- },
24
- {
25
- label: 'Gemini - Google Gemini API',
26
- value: 'gemini',
27
- },
28
- {
29
- label: 'Anthropic - Claude API',
30
- value: 'anthropic',
31
- },
32
- ];
33
- useEffect(() => {
34
- const config = getOpenAiConfig();
35
- setBaseUrl(config.baseUrl);
36
- setApiKey(config.apiKey);
37
- setRequestMethod(config.requestMethod || 'chat');
38
- setAnthropicBeta(config.anthropicBeta || false);
39
- }, []);
40
- useInput((input, key) => {
41
- // Allow Escape key to exit Select component without changes
42
- if (isEditing && currentField === 'requestMethod' && key.escape) {
43
- setIsEditing(false);
44
- return;
45
- }
46
- // Don't handle other input when Select component is active
47
- if (isEditing && currentField === 'requestMethod') {
48
- return;
49
- }
50
- // Handle save/exit globally
51
- if (input === 's' && (key.ctrl || key.meta)) {
52
- const validationErrors = validateApiConfig({ baseUrl, apiKey, requestMethod });
53
- if (validationErrors.length === 0) {
54
- updateOpenAiConfig({ baseUrl, apiKey, requestMethod, anthropicBeta });
55
- setErrors([]);
56
- onSave();
57
- }
58
- else {
59
- setErrors(validationErrors);
60
- }
61
- }
62
- else if (key.escape) {
63
- const validationErrors = validateApiConfig({ baseUrl, apiKey, requestMethod });
64
- if (validationErrors.length === 0) {
65
- updateOpenAiConfig({ baseUrl, apiKey, requestMethod, anthropicBeta });
66
- setErrors([]);
67
- }
68
- onBack();
69
- }
70
- else if (key.return) {
71
- if (isEditing) {
72
- // Exit edit mode, return to navigation
73
- setIsEditing(false);
74
- }
75
- else {
76
- // Enter edit mode for current field (toggle for checkbox)
77
- if (currentField === 'anthropicBeta') {
78
- setAnthropicBeta(!anthropicBeta);
79
- }
80
- else {
81
- setIsEditing(true);
82
- }
83
- }
84
- }
85
- else if (!isEditing && key.upArrow) {
86
- if (currentField === 'apiKey') {
87
- setCurrentField('baseUrl');
88
- }
89
- else if (currentField === 'requestMethod') {
90
- setCurrentField('apiKey');
91
- }
92
- else if (currentField === 'anthropicBeta') {
93
- setCurrentField('requestMethod');
94
- }
95
- }
96
- else if (!isEditing && key.downArrow) {
97
- if (currentField === 'baseUrl') {
98
- setCurrentField('apiKey');
99
- }
100
- else if (currentField === 'apiKey') {
101
- setCurrentField('requestMethod');
102
- }
103
- else if (currentField === 'requestMethod') {
104
- setCurrentField('anthropicBeta');
105
- }
106
- }
107
- });
108
- return (React.createElement(Box, { flexDirection: "column", padding: 1 },
109
- !inlineMode && (React.createElement(Box, { marginBottom: 2, borderStyle: "double", borderColor: "cyan", paddingX: 2, paddingY: 1 },
110
- React.createElement(Box, { flexDirection: "column" },
111
- React.createElement(Gradient, { name: "rainbow" }, "OpenAI API Configuration"),
112
- React.createElement(Text, { color: "gray", dimColor: true }, "Configure your OpenAI API settings")))),
113
- React.createElement(Box, { flexDirection: "column", marginBottom: 2 },
114
- React.createElement(Box, { marginBottom: 1 },
115
- React.createElement(Box, { flexDirection: "column" },
116
- React.createElement(Text, { color: currentField === 'baseUrl' ? 'green' : 'white' },
117
- currentField === 'baseUrl' ? '❯ ' : ' ',
118
- "Base URL:"),
119
- currentField === 'baseUrl' && isEditing && (React.createElement(Box, { marginLeft: 3 },
120
- React.createElement(TextInput, { value: baseUrl, onChange: setBaseUrl, placeholder: "https://api.openai.com/v1" }))),
121
- (!isEditing || currentField !== 'baseUrl') && (React.createElement(Box, { marginLeft: 3 },
122
- React.createElement(Text, { color: "gray" }, baseUrl || 'Not set'))))),
123
- React.createElement(Box, { marginBottom: 1 },
124
- React.createElement(Box, { flexDirection: "column" },
125
- React.createElement(Text, { color: currentField === 'apiKey' ? 'green' : 'white' },
126
- currentField === 'apiKey' ? '❯ ' : ' ',
127
- "API Key:"),
128
- currentField === 'apiKey' && isEditing && (React.createElement(Box, { marginLeft: 3 },
129
- React.createElement(TextInput, { value: apiKey, onChange: setApiKey, placeholder: "sk-...", mask: "*" }))),
130
- (!isEditing || currentField !== 'apiKey') && (React.createElement(Box, { marginLeft: 3 },
131
- React.createElement(Text, { color: "gray" }, apiKey ? '*'.repeat(Math.min(apiKey.length, 20)) : 'Not set'))))),
132
- React.createElement(Box, { marginBottom: 1 },
133
- React.createElement(Box, { flexDirection: "column" },
134
- React.createElement(Text, { color: currentField === 'requestMethod' ? 'green' : 'white' },
135
- currentField === 'requestMethod' ? '❯ ' : ' ',
136
- "Request Method:"),
137
- currentField === 'requestMethod' && isEditing && (React.createElement(Box, { marginLeft: 3 },
138
- React.createElement(Select, { options: requestMethodOptions, defaultValue: requestMethod, onChange: (value) => {
139
- setRequestMethod(value);
140
- setIsEditing(false); // Auto exit edit mode after selection
141
- } }))),
142
- (!isEditing || currentField !== 'requestMethod') && (React.createElement(Box, { marginLeft: 3 },
143
- React.createElement(Text, { color: "gray" }, requestMethodOptions.find(opt => opt.value === requestMethod)?.label || 'Not set'))))),
144
- React.createElement(Box, { marginBottom: 1 },
145
- React.createElement(Box, { flexDirection: "column" },
146
- React.createElement(Text, { color: currentField === 'anthropicBeta' ? 'green' : 'white' },
147
- currentField === 'anthropicBeta' ? '❯ ' : ' ',
148
- "Anthropic Beta (for Claude API):"),
149
- React.createElement(Box, { marginLeft: 3 },
150
- React.createElement(Text, { color: "gray" },
151
- anthropicBeta ? '☑ Enabled' : '☐ Disabled',
152
- " (Press Enter to toggle)"))))),
153
- errors.length > 0 && (React.createElement(Box, { flexDirection: "column", marginBottom: 2 },
154
- React.createElement(Text, { color: "red", bold: true }, "Errors:"),
155
- errors.map((error, index) => (React.createElement(Text, { key: index, color: "red" },
156
- "\u2022 ",
157
- error))))),
158
- React.createElement(Box, { flexDirection: "column" }, isEditing ? (React.createElement(React.Fragment, null,
159
- React.createElement(Alert, { variant: "info" }, "Editing mode: Press Enter to save and exit editing (Make your changes and press Enter when done)"))) : (React.createElement(React.Fragment, null,
160
- React.createElement(Alert, { variant: "info" }, "Use \u2191\u2193 to navigate between fields, press Enter to edit, and press Ctrl+S or Esc to save and return"))))));
161
- }
@@ -1,8 +0,0 @@
1
- import React from 'react';
2
- type Props = {
3
- onBack: () => void;
4
- onSave: () => void;
5
- inlineMode?: boolean;
6
- };
7
- export default function ModelConfigScreen({ onBack, onSave, inlineMode, }: Props): React.JSX.Element;
8
- export {};