@gricha/perry 0.3.0 → 0.3.2
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/README.md +1 -0
- package/dist/client/docker-proxy.js +2 -16
- package/dist/client/port-forward.js +23 -0
- package/dist/client/proxy.js +2 -16
- package/dist/config/loader.js +2 -6
- package/dist/index.js +14 -15
- package/dist/perry-worker +0 -0
- package/dist/sessions/agents/utils.js +6 -2
- package/dist/sessions/parser.js +1 -11
- package/dist/shared/format-utils.js +15 -0
- package/dist/shared/path-utils.js +8 -0
- package/dist/ssh/sync.js +1 -8
- package/dist/update-checker.js +1 -4
- package/package.json +4 -7
- package/dist/chat/base-chat-websocket.js +0 -86
- package/dist/chat/base-claude-session.js +0 -215
- package/dist/chat/base-opencode-session.js +0 -181
- package/dist/chat/handler.js +0 -47
- package/dist/chat/host-handler.js +0 -41
- package/dist/chat/host-opencode-handler.js +0 -144
- package/dist/chat/index.js +0 -2
- package/dist/chat/opencode-handler.js +0 -100
- package/dist/chat/opencode-server.js +0 -502
- package/dist/chat/opencode-websocket.js +0 -31
- package/dist/chat/session-monitor.js +0 -186
- package/dist/chat/session-utils.js +0 -155
- package/dist/chat/websocket.js +0 -33
|
@@ -1,502 +0,0 @@
|
|
|
1
|
-
import { execInContainer } from '../docker';
|
|
2
|
-
// Configuration for connection management
|
|
3
|
-
const HEARTBEAT_TIMEOUT_MS = 45000; // Expect heartbeat every 30s, allow 15s grace
|
|
4
|
-
const MESSAGE_SEND_TIMEOUT_MS = 30000; // Timeout for sending a message
|
|
5
|
-
const SSE_STREAM_TIMEOUT_MS = 120000; // Overall timeout for SSE stream
|
|
6
|
-
const SSE_READY_TIMEOUT_MS = 5000; // Timeout waiting for SSE to become ready
|
|
7
|
-
const serverPorts = new Map();
|
|
8
|
-
const serverStarting = new Map();
|
|
9
|
-
async function findAvailablePort(containerName) {
|
|
10
|
-
const script = `import socket; s=socket.socket(); s.bind(('', 0)); print(s.getsockname()[1]); s.close()`;
|
|
11
|
-
const result = await execInContainer(containerName, ['python3', '-c', script], {
|
|
12
|
-
user: 'workspace',
|
|
13
|
-
});
|
|
14
|
-
return parseInt(result.stdout.trim(), 10);
|
|
15
|
-
}
|
|
16
|
-
async function isServerRunning(containerName, port) {
|
|
17
|
-
try {
|
|
18
|
-
const result = await execInContainer(containerName, ['curl', '-s', '-o', '/dev/null', '-w', '%{http_code}', `http://localhost:${port}/session`], { user: 'workspace' });
|
|
19
|
-
return result.stdout.trim() === '200';
|
|
20
|
-
}
|
|
21
|
-
catch {
|
|
22
|
-
return false;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
async function startServer(containerName) {
|
|
26
|
-
const existing = serverPorts.get(containerName);
|
|
27
|
-
if (existing && (await isServerRunning(containerName, existing))) {
|
|
28
|
-
return existing;
|
|
29
|
-
}
|
|
30
|
-
const starting = serverStarting.get(containerName);
|
|
31
|
-
if (starting) {
|
|
32
|
-
return starting;
|
|
33
|
-
}
|
|
34
|
-
const startPromise = (async () => {
|
|
35
|
-
const port = await findAvailablePort(containerName);
|
|
36
|
-
console.log(`[opencode-server] Starting server on port ${port} in ${containerName}`);
|
|
37
|
-
await execInContainer(containerName, [
|
|
38
|
-
'sh',
|
|
39
|
-
'-c',
|
|
40
|
-
`nohup opencode serve --port ${port} --hostname 127.0.0.1 > /tmp/opencode-server.log 2>&1 &`,
|
|
41
|
-
], { user: 'workspace' });
|
|
42
|
-
for (let i = 0; i < 30; i++) {
|
|
43
|
-
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
44
|
-
if (await isServerRunning(containerName, port)) {
|
|
45
|
-
console.log(`[opencode-server] Server started on port ${port}`);
|
|
46
|
-
serverPorts.set(containerName, port);
|
|
47
|
-
serverStarting.delete(containerName);
|
|
48
|
-
return port;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
serverStarting.delete(containerName);
|
|
52
|
-
throw new Error('Failed to start OpenCode server');
|
|
53
|
-
})();
|
|
54
|
-
serverStarting.set(containerName, startPromise);
|
|
55
|
-
return startPromise;
|
|
56
|
-
}
|
|
57
|
-
export class OpenCodeServerSession {
|
|
58
|
-
containerName;
|
|
59
|
-
workDir;
|
|
60
|
-
sessionId;
|
|
61
|
-
model;
|
|
62
|
-
sessionModel;
|
|
63
|
-
onMessage;
|
|
64
|
-
sseProcess = null;
|
|
65
|
-
responseComplete = false;
|
|
66
|
-
seenToolUse = new Set();
|
|
67
|
-
seenToolResult = new Set();
|
|
68
|
-
lastHeartbeat = 0;
|
|
69
|
-
heartbeatTimer = null;
|
|
70
|
-
streamError = null;
|
|
71
|
-
constructor(options, onMessage) {
|
|
72
|
-
this.containerName = options.containerName;
|
|
73
|
-
this.workDir = options.workDir || '/home/workspace';
|
|
74
|
-
this.sessionId = options.sessionId;
|
|
75
|
-
this.model = options.model;
|
|
76
|
-
this.sessionModel = options.model;
|
|
77
|
-
this.onMessage = onMessage;
|
|
78
|
-
}
|
|
79
|
-
/**
|
|
80
|
-
* Check session status from OpenCode server.
|
|
81
|
-
* Returns the session status or null if session doesn't exist.
|
|
82
|
-
*/
|
|
83
|
-
async getSessionStatus(port) {
|
|
84
|
-
if (!this.sessionId)
|
|
85
|
-
return null;
|
|
86
|
-
try {
|
|
87
|
-
const result = await execInContainer(this.containerName, ['curl', '-s', '--max-time', '5', `http://localhost:${port}/session/status`], { user: 'workspace' });
|
|
88
|
-
const statuses = JSON.parse(result.stdout);
|
|
89
|
-
const status = statuses[this.sessionId];
|
|
90
|
-
return status || { type: 'idle' };
|
|
91
|
-
}
|
|
92
|
-
catch {
|
|
93
|
-
return null;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
/**
|
|
97
|
-
* Verify session exists before attempting to use it.
|
|
98
|
-
*/
|
|
99
|
-
async verifySession(port) {
|
|
100
|
-
if (!this.sessionId)
|
|
101
|
-
return true; // No session to verify
|
|
102
|
-
try {
|
|
103
|
-
const result = await execInContainer(this.containerName, [
|
|
104
|
-
'curl',
|
|
105
|
-
'-s',
|
|
106
|
-
'-o',
|
|
107
|
-
'/dev/null',
|
|
108
|
-
'-w',
|
|
109
|
-
'%{http_code}',
|
|
110
|
-
'--max-time',
|
|
111
|
-
'5',
|
|
112
|
-
`http://localhost:${port}/session/${this.sessionId}`,
|
|
113
|
-
], { user: 'workspace' });
|
|
114
|
-
return result.stdout.trim() === '200';
|
|
115
|
-
}
|
|
116
|
-
catch {
|
|
117
|
-
return false;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
async sendMessage(userMessage) {
|
|
121
|
-
const port = await startServer(this.containerName);
|
|
122
|
-
const baseUrl = `http://localhost:${port}`;
|
|
123
|
-
// Reset error state for new message
|
|
124
|
-
this.streamError = null;
|
|
125
|
-
this.onMessage({
|
|
126
|
-
type: 'system',
|
|
127
|
-
content: 'Processing your message...',
|
|
128
|
-
timestamp: new Date().toISOString(),
|
|
129
|
-
});
|
|
130
|
-
try {
|
|
131
|
-
// If resuming an existing session, verify it still exists
|
|
132
|
-
if (this.sessionId) {
|
|
133
|
-
const sessionExists = await this.verifySession(port);
|
|
134
|
-
if (!sessionExists) {
|
|
135
|
-
console.log(`[opencode-server] Session ${this.sessionId} no longer exists, creating new one`);
|
|
136
|
-
this.sessionId = undefined;
|
|
137
|
-
this.onMessage({
|
|
138
|
-
type: 'system',
|
|
139
|
-
content: 'Previous session expired, starting new session...',
|
|
140
|
-
timestamp: new Date().toISOString(),
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
else {
|
|
144
|
-
// Check if session is busy (another client using it)
|
|
145
|
-
const status = await this.getSessionStatus(port);
|
|
146
|
-
if (status?.type === 'busy') {
|
|
147
|
-
this.onMessage({
|
|
148
|
-
type: 'system',
|
|
149
|
-
content: 'Session is currently busy, waiting for it to become available...',
|
|
150
|
-
timestamp: new Date().toISOString(),
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
if (!this.sessionId) {
|
|
156
|
-
const sessionPayload = this.model ? JSON.stringify({ model: this.model }) : '{}';
|
|
157
|
-
const createResult = await execInContainer(this.containerName, [
|
|
158
|
-
'curl',
|
|
159
|
-
'-s',
|
|
160
|
-
'--max-time',
|
|
161
|
-
String(MESSAGE_SEND_TIMEOUT_MS / 1000),
|
|
162
|
-
'-X',
|
|
163
|
-
'POST',
|
|
164
|
-
`${baseUrl}/session`,
|
|
165
|
-
'-H',
|
|
166
|
-
'Content-Type: application/json',
|
|
167
|
-
'-d',
|
|
168
|
-
sessionPayload,
|
|
169
|
-
], { user: 'workspace' });
|
|
170
|
-
if (createResult.exitCode !== 0) {
|
|
171
|
-
throw new Error(`Failed to create session: ${createResult.stderr || 'Unknown error'}`);
|
|
172
|
-
}
|
|
173
|
-
try {
|
|
174
|
-
const session = JSON.parse(createResult.stdout);
|
|
175
|
-
this.sessionId = session.id;
|
|
176
|
-
}
|
|
177
|
-
catch {
|
|
178
|
-
throw new Error(`Invalid response from OpenCode server: ${createResult.stdout}`);
|
|
179
|
-
}
|
|
180
|
-
this.sessionModel = this.model;
|
|
181
|
-
this.onMessage({
|
|
182
|
-
type: 'system',
|
|
183
|
-
content: `Session started ${this.sessionId}`,
|
|
184
|
-
timestamp: new Date().toISOString(),
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
this.responseComplete = false;
|
|
188
|
-
this.seenToolUse.clear();
|
|
189
|
-
this.seenToolResult.clear();
|
|
190
|
-
// Start SSE stream with timeout
|
|
191
|
-
const { ready, done } = await this.startSSEStream(port);
|
|
192
|
-
// Wait for SSE stream to be ready with timeout
|
|
193
|
-
const readyTimeout = new Promise((_, reject) => {
|
|
194
|
-
setTimeout(() => reject(new Error('Timeout waiting for connection to OpenCode server')), SSE_READY_TIMEOUT_MS);
|
|
195
|
-
});
|
|
196
|
-
try {
|
|
197
|
-
await Promise.race([ready, readyTimeout]);
|
|
198
|
-
}
|
|
199
|
-
catch (err) {
|
|
200
|
-
this.cleanup();
|
|
201
|
-
throw err;
|
|
202
|
-
}
|
|
203
|
-
// Now send the message - THIS IS AWAITED now!
|
|
204
|
-
const messagePayload = JSON.stringify({
|
|
205
|
-
parts: [{ type: 'text', text: userMessage }],
|
|
206
|
-
});
|
|
207
|
-
const sendResult = await execInContainer(this.containerName, [
|
|
208
|
-
'curl',
|
|
209
|
-
'-s',
|
|
210
|
-
'--max-time',
|
|
211
|
-
String(MESSAGE_SEND_TIMEOUT_MS / 1000),
|
|
212
|
-
'-w',
|
|
213
|
-
'\n%{http_code}',
|
|
214
|
-
'-X',
|
|
215
|
-
'POST',
|
|
216
|
-
`${baseUrl}/session/${this.sessionId}/message`,
|
|
217
|
-
'-H',
|
|
218
|
-
'Content-Type: application/json',
|
|
219
|
-
'-d',
|
|
220
|
-
messagePayload,
|
|
221
|
-
], { user: 'workspace' });
|
|
222
|
-
// Parse the HTTP status code from the last line
|
|
223
|
-
const lines = sendResult.stdout.trim().split('\n');
|
|
224
|
-
const httpStatus = lines.pop();
|
|
225
|
-
if (sendResult.exitCode !== 0) {
|
|
226
|
-
this.cleanup();
|
|
227
|
-
throw new Error(`Failed to send message: ${sendResult.stderr || 'Connection failed'}`);
|
|
228
|
-
}
|
|
229
|
-
if (httpStatus && !httpStatus.startsWith('2')) {
|
|
230
|
-
this.cleanup();
|
|
231
|
-
const errorBody = lines.join('\n');
|
|
232
|
-
throw new Error(`OpenCode server error (HTTP ${httpStatus}): ${errorBody || 'Unknown error'}`);
|
|
233
|
-
}
|
|
234
|
-
// Wait for the response stream to complete
|
|
235
|
-
await done;
|
|
236
|
-
// Check if there was a stream error during processing
|
|
237
|
-
if (this.streamError) {
|
|
238
|
-
throw this.streamError;
|
|
239
|
-
}
|
|
240
|
-
this.onMessage({
|
|
241
|
-
type: 'done',
|
|
242
|
-
content: 'Response complete',
|
|
243
|
-
timestamp: new Date().toISOString(),
|
|
244
|
-
});
|
|
245
|
-
}
|
|
246
|
-
catch (err) {
|
|
247
|
-
console.error('[opencode-server] Error:', err);
|
|
248
|
-
this.cleanup();
|
|
249
|
-
this.onMessage({
|
|
250
|
-
type: 'error',
|
|
251
|
-
content: err.message,
|
|
252
|
-
timestamp: new Date().toISOString(),
|
|
253
|
-
});
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
/**
|
|
257
|
-
* Clean up resources (timers, processes)
|
|
258
|
-
*/
|
|
259
|
-
cleanup() {
|
|
260
|
-
this.stopHeartbeatMonitor();
|
|
261
|
-
if (this.sseProcess) {
|
|
262
|
-
this.sseProcess.kill();
|
|
263
|
-
this.sseProcess = null;
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
async startSSEStream(port) {
|
|
267
|
-
let resolveReady;
|
|
268
|
-
let rejectReady;
|
|
269
|
-
let resolveDone;
|
|
270
|
-
let rejectDone;
|
|
271
|
-
let readyResolved = false;
|
|
272
|
-
let doneResolved = false;
|
|
273
|
-
const ready = new Promise((resolve, reject) => {
|
|
274
|
-
resolveReady = () => {
|
|
275
|
-
if (!readyResolved) {
|
|
276
|
-
readyResolved = true;
|
|
277
|
-
resolve();
|
|
278
|
-
}
|
|
279
|
-
};
|
|
280
|
-
rejectReady = (err) => {
|
|
281
|
-
if (!readyResolved) {
|
|
282
|
-
readyResolved = true;
|
|
283
|
-
reject(err);
|
|
284
|
-
}
|
|
285
|
-
};
|
|
286
|
-
});
|
|
287
|
-
const done = new Promise((resolve, reject) => {
|
|
288
|
-
resolveDone = () => {
|
|
289
|
-
if (!doneResolved) {
|
|
290
|
-
doneResolved = true;
|
|
291
|
-
this.stopHeartbeatMonitor();
|
|
292
|
-
resolve();
|
|
293
|
-
}
|
|
294
|
-
};
|
|
295
|
-
rejectDone = (err) => {
|
|
296
|
-
if (!doneResolved) {
|
|
297
|
-
doneResolved = true;
|
|
298
|
-
this.stopHeartbeatMonitor();
|
|
299
|
-
reject(err);
|
|
300
|
-
}
|
|
301
|
-
};
|
|
302
|
-
});
|
|
303
|
-
const proc = Bun.spawn([
|
|
304
|
-
'docker',
|
|
305
|
-
'exec',
|
|
306
|
-
'-i',
|
|
307
|
-
this.containerName,
|
|
308
|
-
'curl',
|
|
309
|
-
'-s',
|
|
310
|
-
'-N',
|
|
311
|
-
'--max-time',
|
|
312
|
-
String(SSE_STREAM_TIMEOUT_MS / 1000),
|
|
313
|
-
`http://localhost:${port}/event`,
|
|
314
|
-
], {
|
|
315
|
-
stdin: 'ignore',
|
|
316
|
-
stdout: 'pipe',
|
|
317
|
-
stderr: 'pipe',
|
|
318
|
-
});
|
|
319
|
-
this.sseProcess = proc;
|
|
320
|
-
const decoder = new TextDecoder();
|
|
321
|
-
let buffer = '';
|
|
322
|
-
let hasReceivedData = false;
|
|
323
|
-
// Start heartbeat monitoring once we receive data
|
|
324
|
-
const startHeartbeatMonitor = () => {
|
|
325
|
-
this.lastHeartbeat = Date.now();
|
|
326
|
-
this.heartbeatTimer = setInterval(() => {
|
|
327
|
-
const timeSinceLastHeartbeat = Date.now() - this.lastHeartbeat;
|
|
328
|
-
if (timeSinceLastHeartbeat > HEARTBEAT_TIMEOUT_MS) {
|
|
329
|
-
console.error(`[opencode-server] No heartbeat received for ${timeSinceLastHeartbeat}ms, connection may be lost`);
|
|
330
|
-
this.streamError = new Error('Connection to OpenCode server lost. Please try again.');
|
|
331
|
-
proc.kill();
|
|
332
|
-
resolveDone();
|
|
333
|
-
}
|
|
334
|
-
}, HEARTBEAT_TIMEOUT_MS / 2);
|
|
335
|
-
};
|
|
336
|
-
const processChunk = (chunk) => {
|
|
337
|
-
buffer += decoder.decode(chunk);
|
|
338
|
-
if (!hasReceivedData) {
|
|
339
|
-
hasReceivedData = true;
|
|
340
|
-
startHeartbeatMonitor();
|
|
341
|
-
resolveReady();
|
|
342
|
-
}
|
|
343
|
-
const lines = buffer.split('\n');
|
|
344
|
-
buffer = lines.pop() || '';
|
|
345
|
-
for (const line of lines) {
|
|
346
|
-
if (!line.startsWith('data: '))
|
|
347
|
-
continue;
|
|
348
|
-
const data = line.slice(6).trim();
|
|
349
|
-
if (!data)
|
|
350
|
-
continue;
|
|
351
|
-
try {
|
|
352
|
-
const event = JSON.parse(data);
|
|
353
|
-
// Update heartbeat timestamp for any valid event (including heartbeats)
|
|
354
|
-
this.lastHeartbeat = Date.now();
|
|
355
|
-
// Handle heartbeat events silently
|
|
356
|
-
if (event.type === 'server.heartbeat' || event.type === 'server.connected') {
|
|
357
|
-
continue;
|
|
358
|
-
}
|
|
359
|
-
this.handleEvent(event);
|
|
360
|
-
if (event.type === 'session.idle') {
|
|
361
|
-
this.responseComplete = true;
|
|
362
|
-
proc.kill();
|
|
363
|
-
resolveDone();
|
|
364
|
-
return;
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
catch {
|
|
368
|
-
continue;
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
};
|
|
372
|
-
// Process stdout stream
|
|
373
|
-
(async () => {
|
|
374
|
-
if (!proc.stdout) {
|
|
375
|
-
rejectReady(new Error('Failed to start SSE stream: no stdout'));
|
|
376
|
-
rejectDone(new Error('Failed to start SSE stream: no stdout'));
|
|
377
|
-
return;
|
|
378
|
-
}
|
|
379
|
-
try {
|
|
380
|
-
for await (const chunk of proc.stdout) {
|
|
381
|
-
processChunk(chunk);
|
|
382
|
-
if (this.responseComplete)
|
|
383
|
-
break;
|
|
384
|
-
}
|
|
385
|
-
// Stream ended - check if it was expected
|
|
386
|
-
if (!this.responseComplete && !doneResolved) {
|
|
387
|
-
// Stream ended without session.idle - could be connection loss
|
|
388
|
-
console.warn('[opencode-server] SSE stream ended unexpectedly');
|
|
389
|
-
this.streamError = new Error('Connection to OpenCode server closed unexpectedly');
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
catch (err) {
|
|
393
|
-
console.error('[opencode-server] SSE stream error:', err);
|
|
394
|
-
this.streamError = err;
|
|
395
|
-
}
|
|
396
|
-
resolveDone();
|
|
397
|
-
})();
|
|
398
|
-
// Capture stderr for diagnostics
|
|
399
|
-
(async () => {
|
|
400
|
-
if (!proc.stderr)
|
|
401
|
-
return;
|
|
402
|
-
const stderrDecoder = new TextDecoder();
|
|
403
|
-
let stderr = '';
|
|
404
|
-
for await (const chunk of proc.stderr) {
|
|
405
|
-
stderr += stderrDecoder.decode(chunk);
|
|
406
|
-
}
|
|
407
|
-
if (stderr && !this.responseComplete) {
|
|
408
|
-
console.error('[opencode-server] SSE stderr:', stderr);
|
|
409
|
-
}
|
|
410
|
-
})();
|
|
411
|
-
// Timeout for initial ready state
|
|
412
|
-
setTimeout(() => {
|
|
413
|
-
if (!hasReceivedData && !readyResolved) {
|
|
414
|
-
rejectReady(new Error('Timeout connecting to OpenCode server event stream'));
|
|
415
|
-
}
|
|
416
|
-
}, SSE_READY_TIMEOUT_MS);
|
|
417
|
-
// Overall stream timeout
|
|
418
|
-
setTimeout(() => {
|
|
419
|
-
if (!this.responseComplete && !doneResolved) {
|
|
420
|
-
console.warn(`[opencode-server] SSE stream timeout after ${SSE_STREAM_TIMEOUT_MS}ms`);
|
|
421
|
-
this.streamError = new Error('Request timed out. Please try again or check if OpenCode is responding.');
|
|
422
|
-
proc.kill();
|
|
423
|
-
resolveDone();
|
|
424
|
-
}
|
|
425
|
-
}, SSE_STREAM_TIMEOUT_MS);
|
|
426
|
-
return { ready, done };
|
|
427
|
-
}
|
|
428
|
-
/**
|
|
429
|
-
* Stop the heartbeat monitor timer
|
|
430
|
-
*/
|
|
431
|
-
stopHeartbeatMonitor() {
|
|
432
|
-
if (this.heartbeatTimer) {
|
|
433
|
-
clearInterval(this.heartbeatTimer);
|
|
434
|
-
this.heartbeatTimer = null;
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
handleEvent(event) {
|
|
438
|
-
const timestamp = new Date().toISOString();
|
|
439
|
-
if (event.type === 'message.part.updated' && event.properties.part) {
|
|
440
|
-
const part = event.properties.part;
|
|
441
|
-
if (part.type === 'text' && event.properties.delta) {
|
|
442
|
-
this.onMessage({
|
|
443
|
-
type: 'assistant',
|
|
444
|
-
content: event.properties.delta,
|
|
445
|
-
timestamp,
|
|
446
|
-
});
|
|
447
|
-
}
|
|
448
|
-
else if (part.type === 'tool' && part.tool) {
|
|
449
|
-
const state = part.state;
|
|
450
|
-
const partId = part.id;
|
|
451
|
-
if (!this.seenToolUse.has(partId)) {
|
|
452
|
-
this.seenToolUse.add(partId);
|
|
453
|
-
this.onMessage({
|
|
454
|
-
type: 'tool_use',
|
|
455
|
-
content: JSON.stringify(state?.input, null, 2),
|
|
456
|
-
toolName: state?.title || part.tool,
|
|
457
|
-
toolId: partId,
|
|
458
|
-
timestamp,
|
|
459
|
-
});
|
|
460
|
-
}
|
|
461
|
-
if (state?.status === 'completed' && state?.output && !this.seenToolResult.has(partId)) {
|
|
462
|
-
this.seenToolResult.add(partId);
|
|
463
|
-
this.onMessage({
|
|
464
|
-
type: 'tool_result',
|
|
465
|
-
content: state.output,
|
|
466
|
-
toolId: partId,
|
|
467
|
-
timestamp,
|
|
468
|
-
});
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
async interrupt() {
|
|
474
|
-
if (this.sseProcess || this.heartbeatTimer) {
|
|
475
|
-
this.cleanup();
|
|
476
|
-
this.onMessage({
|
|
477
|
-
type: 'system',
|
|
478
|
-
content: 'Chat interrupted',
|
|
479
|
-
timestamp: new Date().toISOString(),
|
|
480
|
-
});
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
setModel(model) {
|
|
484
|
-
if (this.model !== model) {
|
|
485
|
-
this.model = model;
|
|
486
|
-
if (this.sessionModel !== model) {
|
|
487
|
-
this.sessionId = undefined;
|
|
488
|
-
this.onMessage({
|
|
489
|
-
type: 'system',
|
|
490
|
-
content: `Switching to model: ${model}`,
|
|
491
|
-
timestamp: new Date().toISOString(),
|
|
492
|
-
});
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
getSessionId() {
|
|
497
|
-
return this.sessionId;
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
export function createOpenCodeServerSession(options, onMessage) {
|
|
501
|
-
return new OpenCodeServerSession(options, onMessage);
|
|
502
|
-
}
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { BaseChatWebSocketServer, } from './base-chat-websocket';
|
|
2
|
-
import { createHostOpencodeSession } from './host-opencode-handler';
|
|
3
|
-
import { createOpenCodeServerSession } from './opencode-server';
|
|
4
|
-
export class OpencodeWebSocketServer extends BaseChatWebSocketServer {
|
|
5
|
-
agentType = 'opencode';
|
|
6
|
-
getConfig;
|
|
7
|
-
constructor(options) {
|
|
8
|
-
super(options);
|
|
9
|
-
this.getConfig = options.getConfig;
|
|
10
|
-
}
|
|
11
|
-
createConnection(ws, workspaceName) {
|
|
12
|
-
return {
|
|
13
|
-
ws,
|
|
14
|
-
session: null,
|
|
15
|
-
workspaceName,
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
createHostSession(sessionId, onMessage, messageModel, _projectPath) {
|
|
19
|
-
const model = messageModel || this.getConfig?.()?.agents?.opencode?.model;
|
|
20
|
-
return createHostOpencodeSession({ sessionId, model }, onMessage);
|
|
21
|
-
}
|
|
22
|
-
createContainerSession(containerName, sessionId, onMessage, messageModel) {
|
|
23
|
-
const model = messageModel || this.getConfig?.()?.agents?.opencode?.model;
|
|
24
|
-
return createOpenCodeServerSession({
|
|
25
|
-
containerName,
|
|
26
|
-
workDir: '/home/workspace',
|
|
27
|
-
sessionId,
|
|
28
|
-
model,
|
|
29
|
-
}, onMessage);
|
|
30
|
-
}
|
|
31
|
-
}
|