snow-ai 0.2.2 → 0.2.4

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.
@@ -476,7 +476,7 @@ export async function createResponseWithTools(options, maxToolRounds = 5) {
476
476
  reasoning: options.reasoning || { summary: 'auto', effort: 'high' },
477
477
  store: options.store ?? false, // 默认不存储对话历史,提高缓存命中
478
478
  include: options.include || ['reasoning.encrypted_content'], // 包含加密推理内容
479
- prompt_cache_key: options.prompt_cache_key, // 缓存键(可选)
479
+ prompt_cache_key: options.prompt_cache_key, // 缓存键
480
480
  });
481
481
  const output = response.output;
482
482
  if (!output || output.length === 0) {
package/dist/app.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import React, { useState, useEffect } from 'react';
2
- import { Box, Text } from 'ink';
2
+ import { Box, Text, useStdout } from 'ink';
3
3
  import { Alert } from '@inkjs/ui';
4
4
  import WelcomeScreen from './ui/pages/WelcomeScreen.js';
5
5
  import ApiConfigScreen from './ui/pages/ApiConfigScreen.js';
@@ -14,6 +14,9 @@ export default function App({ version }) {
14
14
  show: false,
15
15
  message: ''
16
16
  });
17
+ // Terminal resize handling - force re-render on resize
18
+ const { stdout } = useStdout();
19
+ const [terminalSize, setTerminalSize] = useState({ columns: stdout?.columns || 80, rows: stdout?.rows || 24 });
17
20
  // Global exit handler
18
21
  useGlobalExit(setExitNotification);
19
22
  // Global navigation handler
@@ -23,6 +26,26 @@ export default function App({ version }) {
23
26
  });
24
27
  return unsubscribe;
25
28
  }, []);
29
+ // Terminal resize listener with debounce
30
+ useEffect(() => {
31
+ if (!stdout)
32
+ return;
33
+ let resizeTimeout;
34
+ const handleResize = () => {
35
+ // Debounce resize events - wait for resize to stabilize
36
+ clearTimeout(resizeTimeout);
37
+ resizeTimeout = setTimeout(() => {
38
+ // Clear screen before re-render
39
+ stdout.write('\x1Bc'); // Full reset
40
+ setTerminalSize({ columns: stdout.columns, rows: stdout.rows });
41
+ }, 100); // 100ms debounce
42
+ };
43
+ stdout.on('resize', handleResize);
44
+ return () => {
45
+ stdout.off('resize', handleResize);
46
+ clearTimeout(resizeTimeout);
47
+ };
48
+ }, [stdout]);
26
49
  const handleMenuSelect = (value) => {
27
50
  if (value === 'chat' || value === 'settings' || value === 'config' || value === 'models' || value === 'mcp') {
28
51
  setCurrentView(value);
@@ -51,7 +74,7 @@ export default function App({ version }) {
51
74
  return (React.createElement(WelcomeScreen, { version: version, onMenuSelect: handleMenuSelect }));
52
75
  }
53
76
  };
54
- return (React.createElement(Box, { flexDirection: "column" },
77
+ return (React.createElement(Box, { flexDirection: "column", key: `term-${terminalSize.columns}x${terminalSize.rows}` },
55
78
  renderView(),
56
79
  exitNotification.show && (React.createElement(Box, { paddingX: 1 },
57
80
  React.createElement(Alert, { variant: "warning" }, exitNotification.message)))));
@@ -1,3 +1,4 @@
1
+ import { type Diagnostic } from '../utils/vscodeConnection.js';
1
2
  interface SearchMatch {
2
3
  filePath: string;
3
4
  lineNumber: number;
@@ -90,6 +91,7 @@ export declare class FilesystemMCPService {
90
91
  contextStartLine: number;
91
92
  contextEndLine: number;
92
93
  totalLines: number;
94
+ diagnostics?: Diagnostic[];
93
95
  }>;
94
96
  /**
95
97
  * Search for code keywords in files within a directory
@@ -1,5 +1,6 @@
1
1
  import { promises as fs } from 'fs';
2
2
  import * as path from 'path';
3
+ import { vscodeConnection } from '../utils/vscodeConnection.js';
3
4
  const { resolve, dirname, isAbsolute } = path;
4
5
  /**
5
6
  * Filesystem MCP Service
@@ -251,7 +252,17 @@ export class FilesystemMCPService {
251
252
  const newContextContent = newContextLines.join('\n');
252
253
  // Write the modified content back to file
253
254
  await fs.writeFile(fullPath, modifiedLines.join('\n'), 'utf-8');
254
- return {
255
+ // Try to get diagnostics from VS Code after editing
256
+ let diagnostics = [];
257
+ try {
258
+ // Wait a bit for VS Code to process the file change
259
+ await new Promise(resolve => setTimeout(resolve, 500));
260
+ diagnostics = await vscodeConnection.requestDiagnostics(fullPath);
261
+ }
262
+ catch (error) {
263
+ // Ignore diagnostics errors, they are optional
264
+ }
265
+ const result = {
255
266
  message: `File edited successfully: ${filePath} (lines ${startLine}-${adjustedEndLine} replaced)`,
256
267
  oldContent,
257
268
  newContent: newContextContent,
@@ -259,6 +270,16 @@ export class FilesystemMCPService {
259
270
  contextEndLine: newContextEnd,
260
271
  totalLines: newTotalLines
261
272
  };
273
+ // Add diagnostics if any were found
274
+ if (diagnostics.length > 0) {
275
+ result.diagnostics = diagnostics;
276
+ const errorCount = diagnostics.filter(d => d.severity === 'error').length;
277
+ const warningCount = diagnostics.filter(d => d.severity === 'warning').length;
278
+ if (errorCount > 0 || warningCount > 0) {
279
+ result.message += `\n\n⚠️ Diagnostics detected: ${errorCount} error(s), ${warningCount} warning(s)`;
280
+ }
281
+ }
282
+ return result;
262
283
  }
263
284
  catch (error) {
264
285
  throw new Error(`Failed to edit file ${filePath}: ${error instanceof Error ? error.message : 'Unknown error'}`);
@@ -12,7 +12,9 @@ const commands = [
12
12
  { name: 'resume', description: 'Resume a conversation' },
13
13
  { name: 'mcp', description: 'Show Model Context Protocol services and tools' },
14
14
  { name: 'yolo', description: 'Toggle unattended mode (auto-approve all tools)' },
15
- { name: 'init', description: 'Analyze project and generate/update SNOW.md documentation' }
15
+ { name: 'init', description: 'Analyze project and generate/update SNOW.md documentation' },
16
+ { name: 'ide', description: 'Connect to VSCode editor and sync context' },
17
+ { name: 'compact', description: 'Compress conversation history using compact model' }
16
18
  ];
17
19
  export default function ChatInput({ onSubmit, onCommand, placeholder = 'Type your message...', disabled = false, chatHistory = [], onHistorySelect, yoloMode = false, contextUsage }) {
18
20
  const { stdout } = useStdout();
@@ -4,6 +4,8 @@ import '../../utils/commands/resume.js';
4
4
  import '../../utils/commands/mcp.js';
5
5
  import '../../utils/commands/yolo.js';
6
6
  import '../../utils/commands/init.js';
7
+ import '../../utils/commands/ide.js';
8
+ import '../../utils/commands/compact.js';
7
9
  type Props = {};
8
10
  export default function ChatScreen({}: Props): React.JSX.Element;
9
11
  export {};
@@ -18,13 +18,17 @@ import { useSessionManagement } from '../../hooks/useSessionManagement.js';
18
18
  import { useToolConfirmation } from '../../hooks/useToolConfirmation.js';
19
19
  import { handleConversationWithTools } from '../../hooks/useConversation.js';
20
20
  import { parseAndValidateFileReferences, createMessageWithFileInstructions, getSystemInfo } from '../../utils/fileUtils.js';
21
+ import { compressContext } from '../../utils/contextCompressor.js';
21
22
  // Import commands to register them
22
23
  import '../../utils/commands/clear.js';
23
24
  import '../../utils/commands/resume.js';
24
25
  import '../../utils/commands/mcp.js';
25
26
  import '../../utils/commands/yolo.js';
26
27
  import '../../utils/commands/init.js';
28
+ import '../../utils/commands/ide.js';
29
+ import '../../utils/commands/compact.js';
27
30
  import { navigateTo } from '../../hooks/useGlobalNavigation.js';
31
+ import { vscodeConnection } from '../../utils/vscodeConnection.js';
28
32
  // Format elapsed time to human readable format
29
33
  function formatElapsedTime(seconds) {
30
34
  if (seconds < 60) {
@@ -59,6 +63,11 @@ export default function ChatScreen({}) {
59
63
  const [contextUsage, setContextUsage] = useState(null);
60
64
  const [elapsedSeconds, setElapsedSeconds] = useState(0);
61
65
  const [timerStartTime, setTimerStartTime] = useState(null);
66
+ const [vscodeConnected, setVscodeConnected] = useState(false);
67
+ const [vscodeConnectionStatus, setVscodeConnectionStatus] = useState('disconnected');
68
+ const [editorContext, setEditorContext] = useState({});
69
+ const [isCompressing, setIsCompressing] = useState(false);
70
+ const [compressionError, setCompressionError] = useState(null);
62
71
  const { stdout } = useStdout();
63
72
  const workingDirectory = process.cwd();
64
73
  // Use session save hook
@@ -105,6 +114,60 @@ export default function ChatScreen({}) {
105
114
  }, 1000);
106
115
  return () => clearInterval(interval);
107
116
  }, [timerStartTime]);
117
+ // Monitor VSCode connection status and editor context
118
+ useEffect(() => {
119
+ let connectingTimeout = null;
120
+ const checkConnection = setInterval(() => {
121
+ const isConnected = vscodeConnection.isConnected();
122
+ const isServerRunning = vscodeConnection.isServerRunning();
123
+ setVscodeConnected(isConnected);
124
+ // Update connection status based on actual connection state
125
+ if (isConnected && vscodeConnectionStatus !== 'connected') {
126
+ setVscodeConnectionStatus('connected');
127
+ if (connectingTimeout) {
128
+ clearTimeout(connectingTimeout);
129
+ connectingTimeout = null;
130
+ }
131
+ }
132
+ else if (!isConnected && vscodeConnectionStatus === 'connected') {
133
+ setVscodeConnectionStatus('disconnected');
134
+ }
135
+ else if (vscodeConnectionStatus === 'connecting' && !isServerRunning) {
136
+ // Server failed to start
137
+ setVscodeConnectionStatus('error');
138
+ if (connectingTimeout) {
139
+ clearTimeout(connectingTimeout);
140
+ connectingTimeout = null;
141
+ }
142
+ }
143
+ }, 1000);
144
+ // Set timeout for connecting state (15 seconds)
145
+ if (vscodeConnectionStatus === 'connecting') {
146
+ connectingTimeout = setTimeout(() => {
147
+ if (vscodeConnectionStatus === 'connecting') {
148
+ setVscodeConnectionStatus('error');
149
+ }
150
+ }, 15000);
151
+ }
152
+ const unsubscribe = vscodeConnection.onContextUpdate((context) => {
153
+ setEditorContext(context);
154
+ // When we receive context, it means connection is successful
155
+ if (vscodeConnectionStatus !== 'connected') {
156
+ setVscodeConnectionStatus('connected');
157
+ if (connectingTimeout) {
158
+ clearTimeout(connectingTimeout);
159
+ connectingTimeout = null;
160
+ }
161
+ }
162
+ });
163
+ return () => {
164
+ clearInterval(checkConnection);
165
+ if (connectingTimeout) {
166
+ clearTimeout(connectingTimeout);
167
+ }
168
+ unsubscribe();
169
+ };
170
+ }, [vscodeConnectionStatus]);
108
171
  // Pending messages are now handled inline during tool execution in useConversation
109
172
  // Auto-send pending messages when streaming completely stops (as fallback)
110
173
  useEffect(() => {
@@ -140,7 +203,74 @@ export default function ChatScreen({}) {
140
203
  setStreamTokenCount(0);
141
204
  }
142
205
  });
143
- const handleCommandExecution = (commandName, result) => {
206
+ const handleCommandExecution = async (commandName, result) => {
207
+ // Handle /compact command
208
+ if (commandName === 'compact' && result.success && result.action === 'compact') {
209
+ // Set compressing state (不添加命令面板消息)
210
+ setIsCompressing(true);
211
+ setCompressionError(null);
212
+ try {
213
+ // Convert messages to ChatMessage format for compression
214
+ const chatMessages = messages
215
+ .filter(msg => msg.role !== 'command')
216
+ .map(msg => ({
217
+ role: msg.role,
218
+ content: msg.content,
219
+ tool_call_id: msg.toolCallId
220
+ }));
221
+ // Compress the context
222
+ const result = await compressContext(chatMessages);
223
+ // Replace all messages with a summary message (不包含 "Context Compressed" 标题)
224
+ const summaryMessage = {
225
+ role: 'assistant',
226
+ content: result.summary,
227
+ streaming: false
228
+ };
229
+ // Clear session and set new compressed state
230
+ sessionManager.clearCurrentSession();
231
+ clearSavedMessages();
232
+ setMessages([summaryMessage]);
233
+ setRemountKey(prev => prev + 1);
234
+ // Update token usage with compression result
235
+ setContextUsage({
236
+ prompt_tokens: result.usage.prompt_tokens,
237
+ completion_tokens: result.usage.completion_tokens,
238
+ total_tokens: result.usage.total_tokens
239
+ });
240
+ }
241
+ catch (error) {
242
+ // Show error message
243
+ const errorMsg = error instanceof Error ? error.message : 'Unknown compression error';
244
+ setCompressionError(errorMsg);
245
+ const errorMessage = {
246
+ role: 'assistant',
247
+ content: `**Compression Failed**\n\n${errorMsg}`,
248
+ streaming: false
249
+ };
250
+ setMessages(prev => [...prev, errorMessage]);
251
+ }
252
+ finally {
253
+ setIsCompressing(false);
254
+ }
255
+ return;
256
+ }
257
+ // Handle /ide command
258
+ if (commandName === 'ide') {
259
+ if (result.success) {
260
+ setVscodeConnectionStatus('connecting');
261
+ // Add command execution feedback
262
+ const commandMessage = {
263
+ role: 'command',
264
+ content: '',
265
+ commandName: commandName
266
+ };
267
+ setMessages(prev => [...prev, commandMessage]);
268
+ }
269
+ else {
270
+ setVscodeConnectionStatus('error');
271
+ }
272
+ return;
273
+ }
144
274
  if (result.success && result.action === 'clear') {
145
275
  if (stdout && typeof stdout.write === 'function') {
146
276
  stdout.write('\x1B[3J\x1B[2J\x1B[H');
@@ -241,8 +371,8 @@ export default function ChatScreen({}) {
241
371
  const controller = new AbortController();
242
372
  setAbortController(controller);
243
373
  try {
244
- // Create message for AI with file read instructions and system info
245
- const messageForAI = createMessageWithFileInstructions(cleanContent, regularFiles, systemInfo);
374
+ // Create message for AI with file read instructions, system info, and editor context
375
+ const messageForAI = createMessageWithFileInstructions(cleanContent, regularFiles, systemInfo, vscodeConnected ? editorContext : undefined);
246
376
  // Start conversation with tool support
247
377
  await handleConversationWithTools({
248
378
  userContent: messageForAI,
@@ -469,8 +599,27 @@ export default function ChatScreen({}) {
469
599
  React.createElement(Box, { marginX: 1 },
470
600
  React.createElement(PendingMessages, { pendingMessages: pendingMessages })),
471
601
  pendingToolConfirmation && (React.createElement(ToolConfirmation, { toolName: pendingToolConfirmation.batchToolNames || pendingToolConfirmation.tool.function.name, onConfirm: pendingToolConfirmation.resolve })),
472
- !pendingToolConfirmation && (React.createElement(ChatInput, { onSubmit: handleMessageSubmit, onCommand: handleCommandExecution, placeholder: "Ask me anything about coding...", disabled: !!pendingToolConfirmation, chatHistory: messages, onHistorySelect: handleHistorySelect, yoloMode: yoloMode, contextUsage: contextUsage ? {
473
- inputTokens: contextUsage.prompt_tokens,
474
- maxContextTokens: getOpenAiConfig().maxContextTokens || 4000
475
- } : undefined }))));
602
+ !pendingToolConfirmation && !isCompressing && (React.createElement(React.Fragment, null,
603
+ React.createElement(ChatInput, { onSubmit: handleMessageSubmit, onCommand: handleCommandExecution, placeholder: "Ask me anything about coding...", disabled: !!pendingToolConfirmation, chatHistory: messages, onHistorySelect: handleHistorySelect, yoloMode: yoloMode, contextUsage: contextUsage ? {
604
+ inputTokens: contextUsage.prompt_tokens,
605
+ maxContextTokens: getOpenAiConfig().maxContextTokens || 4000
606
+ } : undefined }),
607
+ vscodeConnectionStatus !== 'disconnected' && (React.createElement(Box, { marginTop: 1 },
608
+ React.createElement(Text, { color: vscodeConnectionStatus === 'connecting' ? 'yellow' :
609
+ vscodeConnectionStatus === 'connected' ? 'green' :
610
+ vscodeConnectionStatus === 'error' ? 'red' : 'gray', dimColor: vscodeConnectionStatus !== 'error' },
611
+ "\u25CF ",
612
+ vscodeConnectionStatus === 'connecting' ? 'Connecting to VSCode...' :
613
+ vscodeConnectionStatus === 'connected' ? 'VSCode Connected' :
614
+ vscodeConnectionStatus === 'error' ? 'Connection Failed' : 'VSCode',
615
+ vscodeConnectionStatus === 'connected' && editorContext.activeFile && ` | ${editorContext.activeFile}`,
616
+ vscodeConnectionStatus === 'connected' && editorContext.selectedText && ` | ${editorContext.selectedText.length} chars selected`))))),
617
+ isCompressing && (React.createElement(Box, { marginTop: 1 },
618
+ React.createElement(Text, { color: "cyan" },
619
+ React.createElement(Spinner, { type: "dots" }),
620
+ " Compressing conversation history..."))),
621
+ compressionError && (React.createElement(Box, { marginTop: 1 },
622
+ React.createElement(Text, { color: "red" },
623
+ "\u2717 Compression failed: ",
624
+ compressionError)))));
476
625
  }
@@ -8,6 +8,9 @@ export default function ModelConfigScreen({ onBack, onSave }) {
8
8
  const [advancedModel, setAdvancedModel] = useState('');
9
9
  const [basicModel, setBasicModel] = useState('');
10
10
  const [maxContextTokens, setMaxContextTokens] = useState(4000);
11
+ const [compactBaseUrl, setCompactBaseUrl] = useState('');
12
+ const [compactApiKey, setCompactApiKey] = useState('');
13
+ const [compactModelName, setCompactModelName] = useState('');
11
14
  const [currentField, setCurrentField] = useState('advancedModel');
12
15
  const [isEditing, setIsEditing] = useState(false);
13
16
  const [models, setModels] = useState([]);
@@ -21,6 +24,9 @@ export default function ModelConfigScreen({ onBack, onSave }) {
21
24
  setAdvancedModel(config.advancedModel || '');
22
25
  setBasicModel(config.basicModel || '');
23
26
  setMaxContextTokens(config.maxContextTokens || 4000);
27
+ setCompactBaseUrl(config.compactModel?.baseUrl || '');
28
+ setCompactApiKey(config.compactModel?.apiKey || '');
29
+ setCompactModelName(config.compactModel?.modelName || '');
24
30
  if (!config.baseUrl) {
25
31
  setBaseUrlMissing(true);
26
32
  return;
@@ -57,7 +63,15 @@ export default function ModelConfigScreen({ onBack, onSave }) {
57
63
  return advancedModel;
58
64
  if (currentField === 'basicModel')
59
65
  return basicModel;
60
- return maxContextTokens.toString();
66
+ if (currentField === 'maxContextTokens')
67
+ return maxContextTokens.toString();
68
+ if (currentField === 'compactBaseUrl')
69
+ return compactBaseUrl;
70
+ if (currentField === 'compactApiKey')
71
+ return compactApiKey;
72
+ if (currentField === 'compactModelName')
73
+ return compactModelName;
74
+ return '';
61
75
  };
62
76
  const handleModelChange = (value) => {
63
77
  // 如果选择了手动输入选项
@@ -146,6 +160,34 @@ export default function ModelConfigScreen({ onBack, onSave }) {
146
160
  setIsEditing(false);
147
161
  }
148
162
  }
163
+ else if (currentField === 'compactBaseUrl' || currentField === 'compactApiKey' || currentField === 'compactModelName') {
164
+ // Handle text input for compact model fields
165
+ if (key.return) {
166
+ setIsEditing(false);
167
+ }
168
+ else if (key.backspace || key.delete) {
169
+ if (currentField === 'compactBaseUrl') {
170
+ setCompactBaseUrl(prev => prev.slice(0, -1));
171
+ }
172
+ else if (currentField === 'compactApiKey') {
173
+ setCompactApiKey(prev => prev.slice(0, -1));
174
+ }
175
+ else if (currentField === 'compactModelName') {
176
+ setCompactModelName(prev => prev.slice(0, -1));
177
+ }
178
+ }
179
+ else if (input && input.match(/[a-zA-Z0-9-_./:]/)) {
180
+ if (currentField === 'compactBaseUrl') {
181
+ setCompactBaseUrl(prev => prev + input);
182
+ }
183
+ else if (currentField === 'compactApiKey') {
184
+ setCompactApiKey(prev => prev + input);
185
+ }
186
+ else if (currentField === 'compactModelName') {
187
+ setCompactModelName(prev => prev + input);
188
+ }
189
+ }
190
+ }
149
191
  else {
150
192
  // Allow typing to filter in edit mode for model selection
151
193
  if (input && input.match(/[a-zA-Z0-9-_.]/)) {
@@ -164,6 +206,14 @@ export default function ModelConfigScreen({ onBack, onSave }) {
164
206
  basicModel,
165
207
  maxContextTokens,
166
208
  };
209
+ // 只有当所有字段都填写时才保存 compactModel
210
+ if (compactBaseUrl && compactApiKey && compactModelName) {
211
+ config.compactModel = {
212
+ baseUrl: compactBaseUrl,
213
+ apiKey: compactApiKey,
214
+ modelName: compactModelName,
215
+ };
216
+ }
167
217
  updateOpenAiConfig(config);
168
218
  onSave();
169
219
  }
@@ -173,13 +223,22 @@ export default function ModelConfigScreen({ onBack, onSave }) {
173
223
  basicModel,
174
224
  maxContextTokens,
175
225
  };
226
+ // 只有当所有字段都填写时才保存 compactModel
227
+ if (compactBaseUrl && compactApiKey && compactModelName) {
228
+ config.compactModel = {
229
+ baseUrl: compactBaseUrl,
230
+ apiKey: compactApiKey,
231
+ modelName: compactModelName,
232
+ };
233
+ }
176
234
  updateOpenAiConfig(config);
177
235
  onBack();
178
236
  }
179
237
  else if (key.return) {
180
- // Load models first for model fields, or enter edit mode directly for maxContextTokens
238
+ // Load models first for model fields, or enter edit mode directly for maxContextTokens and compact fields
181
239
  setSearchTerm(''); // Reset search when entering edit mode
182
- if (currentField === 'maxContextTokens') {
240
+ const isCompactField = currentField === 'compactBaseUrl' || currentField === 'compactApiKey' || currentField === 'compactModelName';
241
+ if (currentField === 'maxContextTokens' || isCompactField) {
183
242
  setIsEditing(true);
184
243
  }
185
244
  else {
@@ -194,7 +253,8 @@ export default function ModelConfigScreen({ onBack, onSave }) {
194
253
  }
195
254
  else if (input === 'm') {
196
255
  // 快捷键:按 'm' 直接进入手动输入模式
197
- if (currentField !== 'maxContextTokens') {
256
+ const isCompactField = currentField === 'compactBaseUrl' || currentField === 'compactApiKey' || currentField === 'compactModelName';
257
+ if (currentField !== 'maxContextTokens' && !isCompactField) {
198
258
  setManualInputMode(true);
199
259
  setManualInputValue(getCurrentValue());
200
260
  }
@@ -206,6 +266,15 @@ export default function ModelConfigScreen({ onBack, onSave }) {
206
266
  else if (currentField === 'maxContextTokens') {
207
267
  setCurrentField('basicModel');
208
268
  }
269
+ else if (currentField === 'compactBaseUrl') {
270
+ setCurrentField('maxContextTokens');
271
+ }
272
+ else if (currentField === 'compactApiKey') {
273
+ setCurrentField('compactBaseUrl');
274
+ }
275
+ else if (currentField === 'compactModelName') {
276
+ setCurrentField('compactApiKey');
277
+ }
209
278
  }
210
279
  else if (key.downArrow) {
211
280
  if (currentField === 'advancedModel') {
@@ -214,6 +283,15 @@ export default function ModelConfigScreen({ onBack, onSave }) {
214
283
  else if (currentField === 'basicModel') {
215
284
  setCurrentField('maxContextTokens');
216
285
  }
286
+ else if (currentField === 'maxContextTokens') {
287
+ setCurrentField('compactBaseUrl');
288
+ }
289
+ else if (currentField === 'compactBaseUrl') {
290
+ setCurrentField('compactApiKey');
291
+ }
292
+ else if (currentField === 'compactApiKey') {
293
+ setCurrentField('compactModelName');
294
+ }
217
295
  }
218
296
  });
219
297
  if (baseUrlMissing) {
@@ -293,7 +371,42 @@ export default function ModelConfigScreen({ onBack, onSave }) {
293
371
  "Enter value: ",
294
372
  maxContextTokens))),
295
373
  (!isEditing || currentField !== 'maxContextTokens') && (React.createElement(Box, { marginLeft: 3 },
296
- React.createElement(Text, { color: "gray" }, maxContextTokens)))))),
374
+ React.createElement(Text, { color: "gray" }, maxContextTokens))))),
375
+ React.createElement(Box, { marginBottom: 2, marginTop: 1 },
376
+ React.createElement(Text, { color: "cyan", bold: true }, "Compact Model (Context Compression):")),
377
+ React.createElement(Box, { marginBottom: 1 },
378
+ React.createElement(Box, { flexDirection: "column" },
379
+ React.createElement(Text, { color: currentField === 'compactBaseUrl' ? 'green' : 'white' },
380
+ currentField === 'compactBaseUrl' ? '➣ ' : ' ',
381
+ "Base URL:"),
382
+ currentField === 'compactBaseUrl' && isEditing && (React.createElement(Box, { marginLeft: 3 },
383
+ React.createElement(Text, { color: "cyan" },
384
+ compactBaseUrl,
385
+ React.createElement(Text, { color: "white" }, "_")))),
386
+ (!isEditing || currentField !== 'compactBaseUrl') && (React.createElement(Box, { marginLeft: 3 },
387
+ React.createElement(Text, { color: "gray" }, compactBaseUrl || 'Not set'))))),
388
+ React.createElement(Box, { marginBottom: 1 },
389
+ React.createElement(Box, { flexDirection: "column" },
390
+ React.createElement(Text, { color: currentField === 'compactApiKey' ? 'green' : 'white' },
391
+ currentField === 'compactApiKey' ? '➣ ' : ' ',
392
+ "API Key:"),
393
+ currentField === 'compactApiKey' && isEditing && (React.createElement(Box, { marginLeft: 3 },
394
+ React.createElement(Text, { color: "cyan" },
395
+ compactApiKey.replace(/./g, '*'),
396
+ React.createElement(Text, { color: "white" }, "_")))),
397
+ (!isEditing || currentField !== 'compactApiKey') && (React.createElement(Box, { marginLeft: 3 },
398
+ React.createElement(Text, { color: "gray" }, compactApiKey ? compactApiKey.replace(/./g, '*') : 'Not set'))))),
399
+ React.createElement(Box, { marginBottom: 1 },
400
+ React.createElement(Box, { flexDirection: "column" },
401
+ React.createElement(Text, { color: currentField === 'compactModelName' ? 'green' : 'white' },
402
+ currentField === 'compactModelName' ? '➣ ' : ' ',
403
+ "Model Name:"),
404
+ currentField === 'compactModelName' && isEditing && (React.createElement(Box, { marginLeft: 3 },
405
+ React.createElement(Text, { color: "cyan" },
406
+ compactModelName,
407
+ React.createElement(Text, { color: "white" }, "_")))),
408
+ (!isEditing || currentField !== 'compactModelName') && (React.createElement(Box, { marginLeft: 3 },
409
+ React.createElement(Text, { color: "gray" }, compactModelName || 'Not set')))))),
297
410
  React.createElement(Box, { flexDirection: "column" }, isEditing ? (React.createElement(React.Fragment, null,
298
411
  React.createElement(Alert, { variant: "info" }, "Editing mode: Type to filter models, \u2191\u2193 to select, Enter to confirm"))) : (React.createElement(React.Fragment, null,
299
412
  React.createElement(Alert, { variant: "info" }, "Use \u2191\u2193 to navigate, Enter to edit, M for manual input, Ctrl+S or Esc to save"))))));
@@ -1,4 +1,9 @@
1
1
  export type RequestMethod = 'chat' | 'responses';
2
+ export interface CompactModelConfig {
3
+ baseUrl: string;
4
+ apiKey: string;
5
+ modelName: string;
6
+ }
2
7
  export interface ApiConfig {
3
8
  baseUrl: string;
4
9
  apiKey: string;
@@ -6,6 +11,7 @@ export interface ApiConfig {
6
11
  advancedModel?: string;
7
12
  basicModel?: string;
8
13
  maxContextTokens?: number;
14
+ compactModel?: CompactModelConfig;
9
15
  }
10
16
  export interface MCPServer {
11
17
  url?: string;
@@ -1,7 +1,7 @@
1
1
  export interface CommandResult {
2
2
  success: boolean;
3
3
  message?: string;
4
- action?: 'clear' | 'resume' | 'info' | 'showMcpInfo' | 'goHome' | 'toggleYolo' | 'initProject';
4
+ action?: 'clear' | 'resume' | 'info' | 'showMcpInfo' | 'goHome' | 'toggleYolo' | 'initProject' | 'compact';
5
5
  prompt?: string;
6
6
  }
7
7
  export interface CommandHandler {
@@ -0,0 +1,2 @@
1
+ declare const _default: {};
2
+ export default _default;
@@ -0,0 +1,12 @@
1
+ import { registerCommand } from '../commandExecutor.js';
2
+ // Compact command handler - compress conversation history
3
+ registerCommand('compact', {
4
+ execute: () => {
5
+ return {
6
+ success: true,
7
+ action: 'compact',
8
+ message: 'Compressing conversation history...'
9
+ };
10
+ }
11
+ });
12
+ export default {};
@@ -0,0 +1,2 @@
1
+ declare const _default: {};
2
+ export default _default;
@@ -0,0 +1,29 @@
1
+ import { registerCommand } from '../commandExecutor.js';
2
+ import { vscodeConnection } from '../vscodeConnection.js';
3
+ // IDE connection command handler
4
+ registerCommand('ide', {
5
+ execute: async () => {
6
+ if (vscodeConnection.isConnected()) {
7
+ return {
8
+ success: true,
9
+ action: 'info',
10
+ message: 'Already connected to VSCode editor'
11
+ };
12
+ }
13
+ try {
14
+ await vscodeConnection.start();
15
+ return {
16
+ success: true,
17
+ action: 'info',
18
+ message: `VSCode connection server started on port ${vscodeConnection.getPort()}\nPlease connect from the Snow CLI extension in VSCode`
19
+ };
20
+ }
21
+ catch (error) {
22
+ return {
23
+ success: false,
24
+ message: error instanceof Error ? error.message : 'Failed to start IDE connection'
25
+ };
26
+ }
27
+ }
28
+ });
29
+ export default {};
@@ -0,0 +1,15 @@
1
+ import type { ChatMessage } from '../api/chat.js';
2
+ export interface CompressionResult {
3
+ summary: string;
4
+ usage: {
5
+ prompt_tokens: number;
6
+ completion_tokens: number;
7
+ total_tokens: number;
8
+ };
9
+ }
10
+ /**
11
+ * Compress conversation history using the compact model
12
+ * @param messages - Array of messages to compress
13
+ * @returns Compressed summary and token usage information
14
+ */
15
+ export declare function compressContext(messages: ChatMessage[]): Promise<CompressionResult>;
@@ -0,0 +1,69 @@
1
+ import OpenAI from 'openai';
2
+ import { getOpenAiConfig } from './apiConfig.js';
3
+ /**
4
+ * Compress conversation history using the compact model
5
+ * @param messages - Array of messages to compress
6
+ * @returns Compressed summary and token usage information
7
+ */
8
+ export async function compressContext(messages) {
9
+ const config = getOpenAiConfig();
10
+ // Check if compact model is configured
11
+ if (!config.compactModel || !config.compactModel.baseUrl || !config.compactModel.apiKey || !config.compactModel.modelName) {
12
+ throw new Error('Compact model not configured. Please configure it in Model Settings.');
13
+ }
14
+ // Create OpenAI client with compact model config
15
+ const client = new OpenAI({
16
+ apiKey: config.compactModel.apiKey,
17
+ baseURL: config.compactModel.baseUrl,
18
+ });
19
+ // Filter out system messages and create a conversation text
20
+ const conversationText = messages
21
+ .filter(msg => msg.role !== 'system')
22
+ .map(msg => {
23
+ const role = msg.role === 'user' ? 'User' : msg.role === 'assistant' ? 'Assistant' : 'Tool';
24
+ return `${role}: ${msg.content}`;
25
+ })
26
+ .join('\n\n');
27
+ // Create compression prompt
28
+ const compressionPrompt = `Please summarize the following conversation history in a concise way, preserving all important context, decisions, and key information. The summary should be detailed enough to continue the conversation seamlessly.
29
+
30
+ Conversation:
31
+ ${conversationText}
32
+
33
+ Summary:`;
34
+ try {
35
+ const response = await client.chat.completions.create({
36
+ model: config.compactModel.modelName,
37
+ messages: [
38
+ {
39
+ role: 'user',
40
+ content: compressionPrompt,
41
+ },
42
+ ]
43
+ });
44
+ const summary = response.choices[0]?.message?.content;
45
+ if (!summary) {
46
+ throw new Error('Failed to generate summary from compact model');
47
+ }
48
+ // Extract usage information
49
+ const usage = response.usage || {
50
+ prompt_tokens: 0,
51
+ completion_tokens: 0,
52
+ total_tokens: 0
53
+ };
54
+ return {
55
+ summary,
56
+ usage: {
57
+ prompt_tokens: usage.prompt_tokens,
58
+ completion_tokens: usage.completion_tokens,
59
+ total_tokens: usage.total_tokens
60
+ }
61
+ };
62
+ }
63
+ catch (error) {
64
+ if (error instanceof Error) {
65
+ throw new Error(`Context compression failed: ${error.message}`);
66
+ }
67
+ throw new Error('Unknown error occurred during context compression');
68
+ }
69
+ }
@@ -33,6 +33,14 @@ export declare function createMessageWithFileInstructions(content: string, files
33
33
  platform: string;
34
34
  shell: string;
35
35
  workingDirectory: string;
36
+ }, editorContext?: {
37
+ activeFile?: string;
38
+ selectedText?: string;
39
+ cursorPosition?: {
40
+ line: number;
41
+ character: number;
42
+ };
43
+ workspaceFolder?: string;
36
44
  }): string;
37
45
  /**
38
46
  * Get system information (OS, shell, working directory)
@@ -154,7 +154,7 @@ export async function parseAndValidateFileReferences(content) {
154
154
  /**
155
155
  * Create message with file read instructions for AI
156
156
  */
157
- export function createMessageWithFileInstructions(content, files, systemInfo) {
157
+ export function createMessageWithFileInstructions(content, files, systemInfo, editorContext) {
158
158
  const parts = [content];
159
159
  // Add system info if provided
160
160
  if (systemInfo) {
@@ -165,6 +165,25 @@ export function createMessageWithFileInstructions(content, files, systemInfo) {
165
165
  ];
166
166
  parts.push(systemInfoLines.join('\n'));
167
167
  }
168
+ // Add editor context if provided (from VSCode connection)
169
+ if (editorContext) {
170
+ const editorLines = [];
171
+ if (editorContext.workspaceFolder) {
172
+ editorLines.push(`└─ VSCode Workspace: ${editorContext.workspaceFolder}`);
173
+ }
174
+ if (editorContext.activeFile) {
175
+ editorLines.push(`└─ Active File: ${editorContext.activeFile}`);
176
+ }
177
+ if (editorContext.cursorPosition) {
178
+ editorLines.push(`└─ Cursor: Line ${editorContext.cursorPosition.line + 1}, Column ${editorContext.cursorPosition.character + 1}`);
179
+ }
180
+ if (editorContext.selectedText) {
181
+ editorLines.push(`└─ Selected Code:\n\`\`\`\n${editorContext.selectedText}\n\`\`\``);
182
+ }
183
+ if (editorLines.length > 0) {
184
+ parts.push(editorLines.join('\n'));
185
+ }
186
+ }
168
187
  // Add file instructions if provided
169
188
  if (files.length > 0) {
170
189
  const fileInstructions = files
@@ -0,0 +1,41 @@
1
+ interface EditorContext {
2
+ activeFile?: string;
3
+ selectedText?: string;
4
+ cursorPosition?: {
5
+ line: number;
6
+ character: number;
7
+ };
8
+ workspaceFolder?: string;
9
+ }
10
+ interface Diagnostic {
11
+ message: string;
12
+ severity: 'error' | 'warning' | 'info' | 'hint';
13
+ line: number;
14
+ character: number;
15
+ source?: string;
16
+ code?: string | number;
17
+ }
18
+ declare class VSCodeConnectionManager {
19
+ private server;
20
+ private client;
21
+ private port;
22
+ private editorContext;
23
+ private listeners;
24
+ start(): Promise<void>;
25
+ stop(): void;
26
+ isConnected(): boolean;
27
+ isServerRunning(): boolean;
28
+ getContext(): EditorContext;
29
+ onContextUpdate(listener: (context: EditorContext) => void): () => void;
30
+ private handleMessage;
31
+ private notifyListeners;
32
+ getPort(): number;
33
+ /**
34
+ * Request diagnostics for a specific file from VS Code
35
+ * @param filePath - The file path to get diagnostics for
36
+ * @returns Promise that resolves with diagnostics array
37
+ */
38
+ requestDiagnostics(filePath: string): Promise<Diagnostic[]>;
39
+ }
40
+ export declare const vscodeConnection: VSCodeConnectionManager;
41
+ export type { EditorContext, Diagnostic };
@@ -0,0 +1,155 @@
1
+ import { WebSocketServer, WebSocket } from 'ws';
2
+ class VSCodeConnectionManager {
3
+ constructor() {
4
+ Object.defineProperty(this, "server", {
5
+ enumerable: true,
6
+ configurable: true,
7
+ writable: true,
8
+ value: null
9
+ });
10
+ Object.defineProperty(this, "client", {
11
+ enumerable: true,
12
+ configurable: true,
13
+ writable: true,
14
+ value: null
15
+ });
16
+ Object.defineProperty(this, "port", {
17
+ enumerable: true,
18
+ configurable: true,
19
+ writable: true,
20
+ value: 9527
21
+ });
22
+ Object.defineProperty(this, "editorContext", {
23
+ enumerable: true,
24
+ configurable: true,
25
+ writable: true,
26
+ value: {}
27
+ });
28
+ Object.defineProperty(this, "listeners", {
29
+ enumerable: true,
30
+ configurable: true,
31
+ writable: true,
32
+ value: []
33
+ });
34
+ }
35
+ async start() {
36
+ // If already running, just return success
37
+ if (this.server) {
38
+ return Promise.resolve();
39
+ }
40
+ return new Promise((resolve, reject) => {
41
+ try {
42
+ this.server = new WebSocketServer({ port: this.port });
43
+ this.server.on('connection', (ws) => {
44
+ this.client = ws;
45
+ ws.on('message', (message) => {
46
+ try {
47
+ const data = JSON.parse(message.toString());
48
+ this.handleMessage(data);
49
+ }
50
+ catch (error) {
51
+ // Ignore invalid JSON
52
+ }
53
+ });
54
+ ws.on('close', () => {
55
+ this.client = null;
56
+ });
57
+ });
58
+ this.server.on('listening', () => {
59
+ resolve();
60
+ });
61
+ this.server.on('error', (error) => {
62
+ reject(error);
63
+ });
64
+ }
65
+ catch (error) {
66
+ reject(error);
67
+ }
68
+ });
69
+ }
70
+ stop() {
71
+ if (this.client) {
72
+ this.client.close();
73
+ this.client = null;
74
+ }
75
+ if (this.server) {
76
+ this.server.close();
77
+ this.server = null;
78
+ }
79
+ }
80
+ isConnected() {
81
+ return this.client !== null && this.client.readyState === WebSocket.OPEN;
82
+ }
83
+ isServerRunning() {
84
+ return this.server !== null;
85
+ }
86
+ getContext() {
87
+ return { ...this.editorContext };
88
+ }
89
+ onContextUpdate(listener) {
90
+ this.listeners.push(listener);
91
+ return () => {
92
+ this.listeners = this.listeners.filter((l) => l !== listener);
93
+ };
94
+ }
95
+ handleMessage(data) {
96
+ if (data.type === 'context') {
97
+ this.editorContext = {
98
+ activeFile: data.activeFile,
99
+ selectedText: data.selectedText,
100
+ cursorPosition: data.cursorPosition,
101
+ workspaceFolder: data.workspaceFolder
102
+ };
103
+ this.notifyListeners();
104
+ }
105
+ }
106
+ notifyListeners() {
107
+ for (const listener of this.listeners) {
108
+ listener(this.editorContext);
109
+ }
110
+ }
111
+ getPort() {
112
+ return this.port;
113
+ }
114
+ /**
115
+ * Request diagnostics for a specific file from VS Code
116
+ * @param filePath - The file path to get diagnostics for
117
+ * @returns Promise that resolves with diagnostics array
118
+ */
119
+ async requestDiagnostics(filePath) {
120
+ return new Promise((resolve) => {
121
+ if (!this.client || this.client.readyState !== WebSocket.OPEN) {
122
+ resolve([]); // Return empty array if not connected
123
+ return;
124
+ }
125
+ const requestId = Math.random().toString(36).substring(7);
126
+ const timeout = setTimeout(() => {
127
+ cleanup();
128
+ resolve([]); // Timeout, return empty array
129
+ }, 5000); // 5 second timeout
130
+ const handler = (message) => {
131
+ try {
132
+ const data = JSON.parse(message.toString());
133
+ if (data.type === 'diagnostics' && data.requestId === requestId) {
134
+ cleanup();
135
+ resolve(data.diagnostics || []);
136
+ }
137
+ }
138
+ catch (error) {
139
+ // Ignore invalid JSON
140
+ }
141
+ };
142
+ const cleanup = () => {
143
+ clearTimeout(timeout);
144
+ this.client?.removeListener('message', handler);
145
+ };
146
+ this.client.on('message', handler);
147
+ this.client.send(JSON.stringify({
148
+ type: 'getDiagnostics',
149
+ requestId,
150
+ filePath
151
+ }));
152
+ });
153
+ }
154
+ }
155
+ export const vscodeConnection = new VSCodeConnectionManager();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "snow-ai",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "Intelligent Command Line Assistant powered by AI",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -56,13 +56,15 @@
56
56
  "openai": "^6.1.0",
57
57
  "react": "^18.2.0",
58
58
  "string-width": "^7.2.0",
59
- "tiktoken": "^1.0.22"
59
+ "tiktoken": "^1.0.22",
60
+ "ws": "^8.14.2"
60
61
  },
61
62
  "devDependencies": {
62
63
  "@sindresorhus/tsconfig": "^3.0.1",
63
64
  "@types/diff": "^7.0.2",
64
65
  "@types/figlet": "^1.7.0",
65
66
  "@types/react": "^18.0.32",
67
+ "@types/ws": "^8.5.8",
66
68
  "@vdemedes/prettier-config": "^2.0.1",
67
69
  "ava": "^5.2.0",
68
70
  "chalk": "^5.2.0",