snow-ai 0.2.20 → 0.2.21
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.
|
@@ -34,18 +34,18 @@ export function useVSCodeState() {
|
|
|
34
34
|
if (vscodeConnectionStatus !== 'connecting') {
|
|
35
35
|
return;
|
|
36
36
|
}
|
|
37
|
-
// Set timeout for connecting state (30 seconds to allow for
|
|
37
|
+
// Set timeout for connecting state (30 seconds to allow for IDE plugin reconnection)
|
|
38
38
|
const connectingTimeout = setTimeout(() => {
|
|
39
39
|
const isConnected = vscodeConnection.isConnected();
|
|
40
|
-
const
|
|
40
|
+
const isClientRunning = vscodeConnection.isClientRunning();
|
|
41
41
|
// Only set error if still not connected after timeout
|
|
42
42
|
if (!isConnected) {
|
|
43
|
-
if (
|
|
44
|
-
//
|
|
43
|
+
if (isClientRunning) {
|
|
44
|
+
// Client is running but no connection - show error with helpful message
|
|
45
45
|
setVscodeConnectionStatus('error');
|
|
46
46
|
}
|
|
47
47
|
else {
|
|
48
|
-
//
|
|
48
|
+
// Client not running - go back to disconnected
|
|
49
49
|
setVscodeConnectionStatus('disconnected');
|
|
50
50
|
}
|
|
51
51
|
}
|
|
@@ -177,7 +177,7 @@ const FileList = memo(forwardRef(({ query, selectedIndex, visible, maxItems = 10
|
|
|
177
177
|
return (React.createElement(Box, { paddingX: 1, marginTop: 1, flexDirection: "column" },
|
|
178
178
|
React.createElement(Box, { marginBottom: 1 },
|
|
179
179
|
React.createElement(Text, { color: "blue", bold: true },
|
|
180
|
-
"\
|
|
180
|
+
"\u2261 Files",
|
|
181
181
|
' ',
|
|
182
182
|
allFilteredFiles.length > effectiveMaxItems &&
|
|
183
183
|
`(${selectedIndex + 1}/${allFilteredFiles.length})`)),
|
|
@@ -691,12 +691,12 @@ export default function ChatScreen({}) {
|
|
|
691
691
|
"\u25CF",
|
|
692
692
|
' ',
|
|
693
693
|
vscodeState.vscodeConnectionStatus === 'connecting'
|
|
694
|
-
? '
|
|
694
|
+
? 'Connecting to IDE...'
|
|
695
695
|
: vscodeState.vscodeConnectionStatus === 'connected'
|
|
696
|
-
? '
|
|
696
|
+
? 'IDE Connected'
|
|
697
697
|
: vscodeState.vscodeConnectionStatus === 'error'
|
|
698
|
-
? 'Connection Failed - Make sure Snow CLI
|
|
699
|
-
: '
|
|
698
|
+
? 'Connection Failed - Make sure Snow CLI plugin is installed and active in your IDE'
|
|
699
|
+
: 'IDE',
|
|
700
700
|
vscodeState.vscodeConnectionStatus === 'connected' &&
|
|
701
701
|
vscodeState.editorContext.activeFile &&
|
|
702
702
|
` | ${vscodeState.editorContext.activeFile}`,
|
|
@@ -3,42 +3,30 @@ import { vscodeConnection } from '../vscodeConnection.js';
|
|
|
3
3
|
// IDE connection command handler
|
|
4
4
|
registerCommand('ide', {
|
|
5
5
|
execute: async () => {
|
|
6
|
-
// Check if
|
|
7
|
-
if (vscodeConnection.
|
|
8
|
-
const isConnected = vscodeConnection.isConnected();
|
|
6
|
+
// Check if already connected to IDE plugin
|
|
7
|
+
if (vscodeConnection.isConnected()) {
|
|
9
8
|
return {
|
|
10
9
|
success: true,
|
|
11
10
|
action: 'info',
|
|
12
|
-
alreadyConnected:
|
|
13
|
-
message:
|
|
14
|
-
? `Connected to VSCode (server running on port ${vscodeConnection.getPort()})`
|
|
15
|
-
: `VSCode connection server is running on port ${vscodeConnection.getPort()}\nWaiting for VSCode extension to connect...`
|
|
11
|
+
alreadyConnected: true,
|
|
12
|
+
message: `Already connected to IDE (port ${vscodeConnection.getPort()})`
|
|
16
13
|
};
|
|
17
14
|
}
|
|
18
|
-
//
|
|
15
|
+
// Try to connect to IDE plugin server
|
|
19
16
|
try {
|
|
20
17
|
await vscodeConnection.start();
|
|
21
18
|
return {
|
|
22
19
|
success: true,
|
|
23
20
|
action: 'info',
|
|
24
|
-
message: `
|
|
21
|
+
message: `Connected to IDE on port ${vscodeConnection.getPort()}\nMake sure your IDE plugin (VSCode/JetBrains) is active and running.`
|
|
25
22
|
};
|
|
26
23
|
}
|
|
27
24
|
catch (error) {
|
|
28
|
-
// Handle EADDRINUSE error specifically
|
|
29
|
-
if (error instanceof Error && 'code' in error && error.code === 'EADDRINUSE') {
|
|
30
|
-
// Port is in use by another Snow CLI process - this is OK!
|
|
31
|
-
// Return success and indicate already connected
|
|
32
|
-
return {
|
|
33
|
-
success: true,
|
|
34
|
-
action: 'info',
|
|
35
|
-
alreadyConnected: true, // Treat as already connected since another terminal has the connection
|
|
36
|
-
message: `VSCode connection is already active in another Snow CLI terminal.\nNo additional connection needed - context will be shared through VSCode extension.`
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
25
|
return {
|
|
40
26
|
success: false,
|
|
41
|
-
message: error instanceof Error
|
|
27
|
+
message: error instanceof Error
|
|
28
|
+
? `Failed to connect to IDE: ${error.message}\nMake sure your IDE plugin is installed and active.`
|
|
29
|
+
: 'Failed to connect to IDE. Make sure your IDE plugin is installed and active.'
|
|
42
30
|
};
|
|
43
31
|
}
|
|
44
32
|
}
|
|
@@ -16,26 +16,48 @@ interface Diagnostic {
|
|
|
16
16
|
code?: string | number;
|
|
17
17
|
}
|
|
18
18
|
declare class VSCodeConnectionManager {
|
|
19
|
-
private
|
|
20
|
-
private
|
|
19
|
+
private client;
|
|
20
|
+
private reconnectTimer;
|
|
21
|
+
private reconnectAttempts;
|
|
22
|
+
private readonly MAX_RECONNECT_ATTEMPTS;
|
|
23
|
+
private readonly BASE_RECONNECT_DELAY;
|
|
24
|
+
private readonly MAX_RECONNECT_DELAY;
|
|
25
|
+
private readonly VSCODE_BASE_PORT;
|
|
26
|
+
private readonly VSCODE_MAX_PORT;
|
|
27
|
+
private readonly JETBRAINS_BASE_PORT;
|
|
28
|
+
private readonly JETBRAINS_MAX_PORT;
|
|
21
29
|
private port;
|
|
22
30
|
private editorContext;
|
|
23
31
|
private listeners;
|
|
32
|
+
private currentWorkingDirectory;
|
|
24
33
|
start(): Promise<void>;
|
|
34
|
+
/**
|
|
35
|
+
* Find the correct port for the current workspace
|
|
36
|
+
*/
|
|
37
|
+
private findPortForWorkspace;
|
|
38
|
+
/**
|
|
39
|
+
* Check if we should handle this message based on workspace folder
|
|
40
|
+
*/
|
|
41
|
+
private shouldHandleMessage;
|
|
42
|
+
private scheduleReconnect;
|
|
25
43
|
stop(): void;
|
|
26
44
|
isConnected(): boolean;
|
|
27
|
-
|
|
45
|
+
isClientRunning(): boolean;
|
|
28
46
|
getContext(): EditorContext;
|
|
29
47
|
onContextUpdate(listener: (context: EditorContext) => void): () => void;
|
|
30
48
|
private handleMessage;
|
|
31
49
|
private notifyListeners;
|
|
32
50
|
getPort(): number;
|
|
33
51
|
/**
|
|
34
|
-
* Request diagnostics for a specific file from
|
|
52
|
+
* Request diagnostics for a specific file from IDE
|
|
35
53
|
* @param filePath - The file path to get diagnostics for
|
|
36
54
|
* @returns Promise that resolves with diagnostics array
|
|
37
55
|
*/
|
|
38
56
|
requestDiagnostics(filePath: string): Promise<Diagnostic[]>;
|
|
57
|
+
/**
|
|
58
|
+
* Reset reconnection attempts (e.g., when user manually triggers reconnect)
|
|
59
|
+
*/
|
|
60
|
+
resetReconnectAttempts(): void;
|
|
39
61
|
}
|
|
40
62
|
export declare const vscodeConnection: VSCodeConnectionManager;
|
|
41
63
|
export type { EditorContext, Diagnostic };
|
|
@@ -1,17 +1,69 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { WebSocket } from 'ws';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import * as os from 'os';
|
|
2
5
|
class VSCodeConnectionManager {
|
|
3
6
|
constructor() {
|
|
4
|
-
Object.defineProperty(this, "
|
|
7
|
+
Object.defineProperty(this, "client", {
|
|
5
8
|
enumerable: true,
|
|
6
9
|
configurable: true,
|
|
7
10
|
writable: true,
|
|
8
11
|
value: null
|
|
9
12
|
});
|
|
10
|
-
Object.defineProperty(this, "
|
|
13
|
+
Object.defineProperty(this, "reconnectTimer", {
|
|
11
14
|
enumerable: true,
|
|
12
15
|
configurable: true,
|
|
13
16
|
writable: true,
|
|
14
|
-
value:
|
|
17
|
+
value: null
|
|
18
|
+
});
|
|
19
|
+
Object.defineProperty(this, "reconnectAttempts", {
|
|
20
|
+
enumerable: true,
|
|
21
|
+
configurable: true,
|
|
22
|
+
writable: true,
|
|
23
|
+
value: 0
|
|
24
|
+
});
|
|
25
|
+
Object.defineProperty(this, "MAX_RECONNECT_ATTEMPTS", {
|
|
26
|
+
enumerable: true,
|
|
27
|
+
configurable: true,
|
|
28
|
+
writable: true,
|
|
29
|
+
value: 10
|
|
30
|
+
});
|
|
31
|
+
Object.defineProperty(this, "BASE_RECONNECT_DELAY", {
|
|
32
|
+
enumerable: true,
|
|
33
|
+
configurable: true,
|
|
34
|
+
writable: true,
|
|
35
|
+
value: 2000
|
|
36
|
+
}); // 2 seconds
|
|
37
|
+
Object.defineProperty(this, "MAX_RECONNECT_DELAY", {
|
|
38
|
+
enumerable: true,
|
|
39
|
+
configurable: true,
|
|
40
|
+
writable: true,
|
|
41
|
+
value: 30000
|
|
42
|
+
}); // 30 seconds
|
|
43
|
+
// Port ranges: VSCode uses 9527-9537, JetBrains uses 9538-9548
|
|
44
|
+
Object.defineProperty(this, "VSCODE_BASE_PORT", {
|
|
45
|
+
enumerable: true,
|
|
46
|
+
configurable: true,
|
|
47
|
+
writable: true,
|
|
48
|
+
value: 9527
|
|
49
|
+
});
|
|
50
|
+
Object.defineProperty(this, "VSCODE_MAX_PORT", {
|
|
51
|
+
enumerable: true,
|
|
52
|
+
configurable: true,
|
|
53
|
+
writable: true,
|
|
54
|
+
value: 9537
|
|
55
|
+
});
|
|
56
|
+
Object.defineProperty(this, "JETBRAINS_BASE_PORT", {
|
|
57
|
+
enumerable: true,
|
|
58
|
+
configurable: true,
|
|
59
|
+
writable: true,
|
|
60
|
+
value: 9538
|
|
61
|
+
});
|
|
62
|
+
Object.defineProperty(this, "JETBRAINS_MAX_PORT", {
|
|
63
|
+
enumerable: true,
|
|
64
|
+
configurable: true,
|
|
65
|
+
writable: true,
|
|
66
|
+
value: 9548
|
|
15
67
|
});
|
|
16
68
|
Object.defineProperty(this, "port", {
|
|
17
69
|
enumerable: true,
|
|
@@ -31,64 +83,147 @@ class VSCodeConnectionManager {
|
|
|
31
83
|
writable: true,
|
|
32
84
|
value: []
|
|
33
85
|
});
|
|
86
|
+
Object.defineProperty(this, "currentWorkingDirectory", {
|
|
87
|
+
enumerable: true,
|
|
88
|
+
configurable: true,
|
|
89
|
+
writable: true,
|
|
90
|
+
value: process.cwd()
|
|
91
|
+
});
|
|
34
92
|
}
|
|
35
93
|
async start() {
|
|
36
|
-
// If already
|
|
37
|
-
if (this.
|
|
94
|
+
// If already connected, just return success
|
|
95
|
+
if (this.client?.readyState === WebSocket.OPEN) {
|
|
38
96
|
return Promise.resolve();
|
|
39
97
|
}
|
|
98
|
+
// Try to find the correct port for this workspace
|
|
99
|
+
const targetPort = this.findPortForWorkspace();
|
|
40
100
|
return new Promise((resolve, reject) => {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
this.
|
|
44
|
-
//
|
|
45
|
-
this.
|
|
46
|
-
|
|
101
|
+
const tryConnect = (port) => {
|
|
102
|
+
// Check both VSCode and JetBrains port ranges
|
|
103
|
+
if (port > this.VSCODE_MAX_PORT && port < this.JETBRAINS_BASE_PORT) {
|
|
104
|
+
// Jump from VSCode range to JetBrains range
|
|
105
|
+
tryConnect(this.JETBRAINS_BASE_PORT);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (port > this.JETBRAINS_MAX_PORT) {
|
|
109
|
+
reject(new Error(`Failed to connect: no IDE server found on ports ${this.VSCODE_BASE_PORT}-${this.VSCODE_MAX_PORT} or ${this.JETBRAINS_BASE_PORT}-${this.JETBRAINS_MAX_PORT}`));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
this.client = new WebSocket(`ws://localhost:${port}`);
|
|
114
|
+
this.client.on('open', () => {
|
|
115
|
+
// Reset reconnect attempts on successful connection
|
|
116
|
+
this.reconnectAttempts = 0;
|
|
117
|
+
this.port = port;
|
|
118
|
+
resolve();
|
|
119
|
+
});
|
|
120
|
+
this.client.on('message', message => {
|
|
47
121
|
try {
|
|
48
122
|
const data = JSON.parse(message.toString());
|
|
49
|
-
|
|
123
|
+
// Filter messages by workspace folder
|
|
124
|
+
if (this.shouldHandleMessage(data)) {
|
|
125
|
+
this.handleMessage(data);
|
|
126
|
+
}
|
|
50
127
|
}
|
|
51
128
|
catch (error) {
|
|
52
129
|
// Ignore invalid JSON
|
|
53
130
|
}
|
|
54
131
|
});
|
|
55
|
-
|
|
56
|
-
this.
|
|
132
|
+
this.client.on('close', () => {
|
|
133
|
+
this.client = null;
|
|
134
|
+
this.scheduleReconnect();
|
|
57
135
|
});
|
|
58
|
-
|
|
59
|
-
//
|
|
60
|
-
this.
|
|
136
|
+
this.client.on('error', _error => {
|
|
137
|
+
// On initial connection, try next port
|
|
138
|
+
if (this.reconnectAttempts === 0) {
|
|
139
|
+
this.client = null;
|
|
140
|
+
tryConnect(port + 1);
|
|
141
|
+
}
|
|
142
|
+
// For reconnections, silently handle and let close event trigger reconnect
|
|
61
143
|
});
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
catch (error) {
|
|
71
|
-
reject(error);
|
|
72
|
-
}
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
tryConnect(port + 1);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
tryConnect(targetPort);
|
|
73
150
|
});
|
|
74
151
|
}
|
|
152
|
+
/**
|
|
153
|
+
* Find the correct port for the current workspace
|
|
154
|
+
*/
|
|
155
|
+
findPortForWorkspace() {
|
|
156
|
+
try {
|
|
157
|
+
const portInfoPath = path.join(os.tmpdir(), 'snow-cli-ports.json');
|
|
158
|
+
if (fs.existsSync(portInfoPath)) {
|
|
159
|
+
const portInfo = JSON.parse(fs.readFileSync(portInfoPath, 'utf8'));
|
|
160
|
+
// Try to match current working directory
|
|
161
|
+
const cwd = this.currentWorkingDirectory;
|
|
162
|
+
// Direct match
|
|
163
|
+
if (portInfo[cwd]) {
|
|
164
|
+
return portInfo[cwd];
|
|
165
|
+
}
|
|
166
|
+
// Check if cwd is within any of the workspace folders
|
|
167
|
+
for (const [workspace, port] of Object.entries(portInfo)) {
|
|
168
|
+
if (cwd.startsWith(workspace)) {
|
|
169
|
+
return port;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
// Ignore errors, will fall back to VSCODE_BASE_PORT
|
|
176
|
+
}
|
|
177
|
+
// Start with VSCode port range by default
|
|
178
|
+
return this.VSCODE_BASE_PORT;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Check if we should handle this message based on workspace folder
|
|
182
|
+
*/
|
|
183
|
+
shouldHandleMessage(data) {
|
|
184
|
+
// If no workspace folder in message, accept it (backwards compatibility)
|
|
185
|
+
if (!data.workspaceFolder) {
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
// Check if message's workspace folder matches our current working directory
|
|
189
|
+
const cwd = this.currentWorkingDirectory;
|
|
190
|
+
// Bidirectional check: either cwd is within IDE workspace, or IDE workspace is within cwd
|
|
191
|
+
const cwdInWorkspace = cwd.startsWith(data.workspaceFolder);
|
|
192
|
+
const workspaceInCwd = data.workspaceFolder.startsWith(cwd);
|
|
193
|
+
const matches = cwdInWorkspace || workspaceInCwd;
|
|
194
|
+
return matches;
|
|
195
|
+
}
|
|
196
|
+
scheduleReconnect() {
|
|
197
|
+
if (this.reconnectTimer) {
|
|
198
|
+
clearTimeout(this.reconnectTimer);
|
|
199
|
+
}
|
|
200
|
+
this.reconnectAttempts++;
|
|
201
|
+
if (this.reconnectAttempts >= this.MAX_RECONNECT_ATTEMPTS) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const delay = Math.min(this.BASE_RECONNECT_DELAY * Math.pow(1.5, this.reconnectAttempts - 1), this.MAX_RECONNECT_DELAY);
|
|
205
|
+
this.reconnectTimer = setTimeout(() => {
|
|
206
|
+
this.start().catch(() => {
|
|
207
|
+
// Silently handle reconnection failures
|
|
208
|
+
});
|
|
209
|
+
}, delay);
|
|
210
|
+
}
|
|
75
211
|
stop() {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
212
|
+
if (this.reconnectTimer) {
|
|
213
|
+
clearTimeout(this.reconnectTimer);
|
|
214
|
+
this.reconnectTimer = null;
|
|
79
215
|
}
|
|
80
|
-
this.
|
|
81
|
-
|
|
82
|
-
this.
|
|
83
|
-
this.server = null;
|
|
216
|
+
if (this.client) {
|
|
217
|
+
this.client.close();
|
|
218
|
+
this.client = null;
|
|
84
219
|
}
|
|
220
|
+
this.reconnectAttempts = 0;
|
|
85
221
|
}
|
|
86
222
|
isConnected() {
|
|
87
|
-
return
|
|
88
|
-
Array.from(this.clients).some(client => client.readyState === WebSocket.OPEN));
|
|
223
|
+
return this.client?.readyState === WebSocket.OPEN;
|
|
89
224
|
}
|
|
90
|
-
|
|
91
|
-
return this.
|
|
225
|
+
isClientRunning() {
|
|
226
|
+
return this.client !== null;
|
|
92
227
|
}
|
|
93
228
|
getContext() {
|
|
94
229
|
return { ...this.editorContext };
|
|
@@ -119,15 +254,13 @@ class VSCodeConnectionManager {
|
|
|
119
254
|
return this.port;
|
|
120
255
|
}
|
|
121
256
|
/**
|
|
122
|
-
* Request diagnostics for a specific file from
|
|
257
|
+
* Request diagnostics for a specific file from IDE
|
|
123
258
|
* @param filePath - The file path to get diagnostics for
|
|
124
259
|
* @returns Promise that resolves with diagnostics array
|
|
125
260
|
*/
|
|
126
261
|
async requestDiagnostics(filePath) {
|
|
127
262
|
return new Promise(resolve => {
|
|
128
|
-
|
|
129
|
-
const client = Array.from(this.clients).find(c => c.readyState === WebSocket.OPEN);
|
|
130
|
-
if (!client) {
|
|
263
|
+
if (!this.client || this.client.readyState !== WebSocket.OPEN) {
|
|
131
264
|
resolve([]); // Return empty array if not connected
|
|
132
265
|
return;
|
|
133
266
|
}
|
|
@@ -150,15 +283,21 @@ class VSCodeConnectionManager {
|
|
|
150
283
|
};
|
|
151
284
|
const cleanup = () => {
|
|
152
285
|
clearTimeout(timeout);
|
|
153
|
-
client?.removeListener('message', handler);
|
|
286
|
+
this.client?.removeListener('message', handler);
|
|
154
287
|
};
|
|
155
|
-
client.on('message', handler);
|
|
156
|
-
client.send(JSON.stringify({
|
|
288
|
+
this.client.on('message', handler);
|
|
289
|
+
this.client.send(JSON.stringify({
|
|
157
290
|
type: 'getDiagnostics',
|
|
158
291
|
requestId,
|
|
159
292
|
filePath,
|
|
160
293
|
}));
|
|
161
294
|
});
|
|
162
295
|
}
|
|
296
|
+
/**
|
|
297
|
+
* Reset reconnection attempts (e.g., when user manually triggers reconnect)
|
|
298
|
+
*/
|
|
299
|
+
resetReconnectAttempts() {
|
|
300
|
+
this.reconnectAttempts = 0;
|
|
301
|
+
}
|
|
163
302
|
}
|
|
164
303
|
export const vscodeConnection = new VSCodeConnectionManager();
|