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.
- package/dist/api/responses.js +1 -1
- package/dist/app.js +25 -2
- package/dist/mcp/filesystem.d.ts +2 -0
- package/dist/mcp/filesystem.js +22 -1
- package/dist/ui/components/ChatInput.js +3 -1
- package/dist/ui/pages/ChatScreen.d.ts +2 -0
- package/dist/ui/pages/ChatScreen.js +156 -7
- package/dist/ui/pages/ModelConfigScreen.js +118 -5
- package/dist/utils/apiConfig.d.ts +6 -0
- package/dist/utils/commandExecutor.d.ts +1 -1
- package/dist/utils/commands/compact.d.ts +2 -0
- package/dist/utils/commands/compact.js +12 -0
- package/dist/utils/commands/ide.d.ts +2 -0
- package/dist/utils/commands/ide.js +29 -0
- package/dist/utils/contextCompressor.d.ts +15 -0
- package/dist/utils/contextCompressor.js +69 -0
- package/dist/utils/fileUtils.d.ts +8 -0
- package/dist/utils/fileUtils.js +20 -1
- package/dist/utils/vscodeConnection.d.ts +41 -0
- package/dist/utils/vscodeConnection.js +155 -0
- package/package.json +4 -2
package/dist/api/responses.js
CHANGED
|
@@ -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)))));
|
package/dist/mcp/filesystem.d.ts
CHANGED
|
@@ -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
|
package/dist/mcp/filesystem.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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(
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,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,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)
|
package/dist/utils/fileUtils.js
CHANGED
|
@@ -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.
|
|
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",
|