agentgui 1.0.397 → 1.0.399
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/lib/ws-events.js +20 -0
- package/lib/ws-handlers-conv.js +183 -0
- package/lib/ws-handlers-run.js +157 -0
- package/lib/ws-handlers-session.js +165 -0
- package/lib/ws-handlers-util.js +186 -0
- package/lib/ws-optimizer.js +2 -2
- package/lib/ws-protocol.js +81 -0
- package/package.json +3 -2
- package/scripts/patch-fsbrowse.js +88 -0
- package/server.js +285 -93
- package/static/app.js +7 -17
- package/static/index.html +19 -18
- package/static/js/client.js +4 -13
- package/static/js/conversations.js +12 -51
- package/static/js/features.js +12 -11
- package/static/js/voice.js +69 -64
- package/static/js/ws-client.js +80 -0
package/server.js
CHANGED
|
@@ -17,6 +17,11 @@ import { runClaudeWithStreaming } from './lib/claude-runner.js';
|
|
|
17
17
|
import { initializeDescriptors, getAgentDescriptor } from './lib/agent-descriptors.js';
|
|
18
18
|
import { SSEStreamManager } from './lib/sse-stream.js';
|
|
19
19
|
import { WSOptimizer } from './lib/ws-optimizer.js';
|
|
20
|
+
import { WsRouter } from './lib/ws-protocol.js';
|
|
21
|
+
import { register as registerConvHandlers } from './lib/ws-handlers-conv.js';
|
|
22
|
+
import { register as registerSessionHandlers } from './lib/ws-handlers-session.js';
|
|
23
|
+
import { register as registerRunHandlers } from './lib/ws-handlers-run.js';
|
|
24
|
+
import { register as registerUtilHandlers } from './lib/ws-handlers-util.js';
|
|
20
25
|
|
|
21
26
|
const ttsTextAccumulators = new Map();
|
|
22
27
|
|
|
@@ -295,8 +300,10 @@ expressApp.use(BASE_URL + '/files/:conversationId', (req, res, next) => {
|
|
|
295
300
|
if (!conv || !conv.workingDirectory) {
|
|
296
301
|
return res.status(404).json({ error: 'Conversation not found or no working directory' });
|
|
297
302
|
}
|
|
303
|
+
// Normalize the working directory path to avoid Windows path duplication issues
|
|
304
|
+
const normalizedWorkingDir = path.resolve(conv.workingDirectory);
|
|
298
305
|
// Create a fresh fsbrowse router for this conversation's directory
|
|
299
|
-
const router = fsbrowse({ baseDir:
|
|
306
|
+
const router = fsbrowse({ baseDir: normalizedWorkingDir, name: 'Files' });
|
|
300
307
|
// Strip the conversationId param from the path before passing to fsbrowse
|
|
301
308
|
req.baseUrl = BASE_URL + '/files/' + req.params.conversationId;
|
|
302
309
|
router(req, res, next);
|
|
@@ -1073,7 +1080,9 @@ const server = http.createServer(async (req, res) => {
|
|
|
1073
1080
|
|
|
1074
1081
|
if (pathOnly === '/api/conversations' && req.method === 'POST') {
|
|
1075
1082
|
const body = await parseBody(req);
|
|
1076
|
-
|
|
1083
|
+
// Normalize working directory to avoid Windows path issues
|
|
1084
|
+
const normalizedWorkingDir = body.workingDirectory ? path.resolve(body.workingDirectory) : null;
|
|
1085
|
+
const conversation = queries.createConversation(body.agentId, body.title, normalizedWorkingDir, body.model || null);
|
|
1077
1086
|
queries.createEvent('conversation.created', { agentId: body.agentId, workingDirectory: conversation.workingDirectory, model: conversation.model }, conversation.id);
|
|
1078
1087
|
broadcastSync({ type: 'conversation_created', conversation });
|
|
1079
1088
|
sendJSON(req, res, 201, { conversation });
|
|
@@ -1099,6 +1108,10 @@ const server = http.createServer(async (req, res) => {
|
|
|
1099
1108
|
|
|
1100
1109
|
if (req.method === 'POST' || req.method === 'PUT') {
|
|
1101
1110
|
const body = await parseBody(req);
|
|
1111
|
+
// Normalize working directory if present to avoid Windows path issues
|
|
1112
|
+
if (body.workingDirectory) {
|
|
1113
|
+
body.workingDirectory = path.resolve(body.workingDirectory);
|
|
1114
|
+
}
|
|
1102
1115
|
const conv = queries.updateConversation(convMatch[1], body);
|
|
1103
1116
|
if (!conv) { sendJSON(req, res, 404, { error: 'Conversation not found' }); return; }
|
|
1104
1117
|
queries.createEvent('conversation.updated', body, convMatch[1]);
|
|
@@ -3292,6 +3305,82 @@ async function processMessageWithStreaming(conversationId, messageId, sessionId,
|
|
|
3292
3305
|
});
|
|
3293
3306
|
|
|
3294
3307
|
if (block.type === 'text' && block.text) {
|
|
3308
|
+
// Check for rate limit message in text content
|
|
3309
|
+
const rateLimitTextMatch = block.text.match(/you'?ve hit your limit|rate limit exceeded/i);
|
|
3310
|
+
if (rateLimitTextMatch) {
|
|
3311
|
+
debugLog(`[rate-limit] Detected rate limit message in stream for conv ${conversationId}`);
|
|
3312
|
+
|
|
3313
|
+
// Extract reset time from message
|
|
3314
|
+
let retryAfterSec = 300; // default 5 minutes
|
|
3315
|
+
const resetTimeMatch = block.text.match(/resets?\s+(?:at\s+)?(\d{1,2})(?::(\d{2}))?\s*(am|pm)?\s*\(?(UTC|[A-Z]{2,4})\)?/i);
|
|
3316
|
+
if (resetTimeMatch) {
|
|
3317
|
+
let hours = parseInt(resetTimeMatch[1], 10);
|
|
3318
|
+
const minutes = resetTimeMatch[2] ? parseInt(resetTimeMatch[2], 10) : 0;
|
|
3319
|
+
const period = resetTimeMatch[3]?.toLowerCase();
|
|
3320
|
+
|
|
3321
|
+
if (period === 'pm' && hours !== 12) hours += 12;
|
|
3322
|
+
if (period === 'am' && hours === 12) hours = 0;
|
|
3323
|
+
|
|
3324
|
+
const now = new Date();
|
|
3325
|
+
const resetTime = new Date(now);
|
|
3326
|
+
resetTime.setUTCHours(hours, minutes, 0, 0);
|
|
3327
|
+
|
|
3328
|
+
if (resetTime <= now) {
|
|
3329
|
+
resetTime.setUTCDate(resetTime.getUTCDate() + 1);
|
|
3330
|
+
}
|
|
3331
|
+
|
|
3332
|
+
retryAfterSec = Math.max(60, Math.ceil((resetTime.getTime() - now.getTime()) / 1000));
|
|
3333
|
+
debugLog(`[rate-limit] Parsed reset time: ${resetTime.toISOString()}, retry in ${retryAfterSec}s`);
|
|
3334
|
+
}
|
|
3335
|
+
|
|
3336
|
+
// Kill the running process
|
|
3337
|
+
const entry = activeExecutions.get(conversationId);
|
|
3338
|
+
if (entry && entry.pid) {
|
|
3339
|
+
try {
|
|
3340
|
+
process.kill(entry.pid);
|
|
3341
|
+
debugLog(`[rate-limit] Killed process ${entry.pid} for conv ${conversationId}`);
|
|
3342
|
+
} catch (e) {
|
|
3343
|
+
debugLog(`[rate-limit] Failed to kill process: ${e.message}`);
|
|
3344
|
+
}
|
|
3345
|
+
}
|
|
3346
|
+
|
|
3347
|
+
// Set flag to stop processing and trigger retry
|
|
3348
|
+
rateLimitState.set(conversationId, {
|
|
3349
|
+
retryAt: Date.now() + (retryAfterSec * 1000),
|
|
3350
|
+
cooldownMs: retryAfterSec * 1000,
|
|
3351
|
+
retryCount: 0,
|
|
3352
|
+
isStreamDetected: true
|
|
3353
|
+
});
|
|
3354
|
+
|
|
3355
|
+
// Broadcast rate limit event
|
|
3356
|
+
broadcastSync({
|
|
3357
|
+
type: 'rate_limit_hit',
|
|
3358
|
+
sessionId,
|
|
3359
|
+
conversationId,
|
|
3360
|
+
retryAfterMs: retryAfterSec * 1000,
|
|
3361
|
+
retryAt: Date.now() + (retryAfterSec * 1000),
|
|
3362
|
+
retryCount: 1,
|
|
3363
|
+
timestamp: Date.now()
|
|
3364
|
+
});
|
|
3365
|
+
|
|
3366
|
+
batcher.drain();
|
|
3367
|
+
activeExecutions.delete(conversationId);
|
|
3368
|
+
queries.setIsStreaming(conversationId, false);
|
|
3369
|
+
|
|
3370
|
+
// Schedule retry
|
|
3371
|
+
setTimeout(() => {
|
|
3372
|
+
rateLimitState.delete(conversationId);
|
|
3373
|
+
broadcastSync({
|
|
3374
|
+
type: 'rate_limit_clear',
|
|
3375
|
+
conversationId,
|
|
3376
|
+
timestamp: Date.now()
|
|
3377
|
+
});
|
|
3378
|
+
scheduleRetry(conversationId, messageId, content, agentId, model);
|
|
3379
|
+
}, retryAfterSec * 1000);
|
|
3380
|
+
|
|
3381
|
+
return; // Stop processing events
|
|
3382
|
+
}
|
|
3383
|
+
|
|
3295
3384
|
eagerTTS(block.text, conversationId, sessionId);
|
|
3296
3385
|
}
|
|
3297
3386
|
}
|
|
@@ -3346,6 +3435,74 @@ async function processMessageWithStreaming(conversationId, messageId, sessionId,
|
|
|
3346
3435
|
|
|
3347
3436
|
if (parsed.result) {
|
|
3348
3437
|
const resultText = typeof parsed.result === 'string' ? parsed.result : JSON.stringify(parsed.result);
|
|
3438
|
+
|
|
3439
|
+
// Check for rate limit message in result
|
|
3440
|
+
const rateLimitResultMatch = resultText.match(/you'?ve hit your limit|rate limit exceeded/i);
|
|
3441
|
+
if (rateLimitResultMatch) {
|
|
3442
|
+
debugLog(`[rate-limit] Detected rate limit in result for conv ${conversationId}`);
|
|
3443
|
+
|
|
3444
|
+
let retryAfterSec = 300;
|
|
3445
|
+
const resetTimeMatch = resultText.match(/resets?\s+(?:at\s+)?(\d{1,2})(?::(\d{2}))?\s*(am|pm)?\s*\(?(UTC|[A-Z]{2,4})\)?/i);
|
|
3446
|
+
if (resetTimeMatch) {
|
|
3447
|
+
let hours = parseInt(resetTimeMatch[1], 10);
|
|
3448
|
+
const minutes = resetTimeMatch[2] ? parseInt(resetTimeMatch[2], 10) : 0;
|
|
3449
|
+
const period = resetTimeMatch[3]?.toLowerCase();
|
|
3450
|
+
|
|
3451
|
+
if (period === 'pm' && hours !== 12) hours += 12;
|
|
3452
|
+
if (period === 'am' && hours === 12) hours = 0;
|
|
3453
|
+
|
|
3454
|
+
const now = new Date();
|
|
3455
|
+
const resetTime = new Date(now);
|
|
3456
|
+
resetTime.setUTCHours(hours, minutes, 0, 0);
|
|
3457
|
+
|
|
3458
|
+
if (resetTime <= now) {
|
|
3459
|
+
resetTime.setUTCDate(resetTime.getUTCDate() + 1);
|
|
3460
|
+
}
|
|
3461
|
+
|
|
3462
|
+
retryAfterSec = Math.max(60, Math.ceil((resetTime.getTime() - now.getTime()) / 1000));
|
|
3463
|
+
}
|
|
3464
|
+
|
|
3465
|
+
const entry = activeExecutions.get(conversationId);
|
|
3466
|
+
if (entry && entry.pid) {
|
|
3467
|
+
try {
|
|
3468
|
+
process.kill(entry.pid);
|
|
3469
|
+
} catch (e) {}
|
|
3470
|
+
}
|
|
3471
|
+
|
|
3472
|
+
rateLimitState.set(conversationId, {
|
|
3473
|
+
retryAt: Date.now() + (retryAfterSec * 1000),
|
|
3474
|
+
cooldownMs: retryAfterSec * 1000,
|
|
3475
|
+
retryCount: 0,
|
|
3476
|
+
isStreamDetected: true
|
|
3477
|
+
});
|
|
3478
|
+
|
|
3479
|
+
broadcastSync({
|
|
3480
|
+
type: 'rate_limit_hit',
|
|
3481
|
+
sessionId,
|
|
3482
|
+
conversationId,
|
|
3483
|
+
retryAfterMs: retryAfterSec * 1000,
|
|
3484
|
+
retryAt: Date.now() + (retryAfterSec * 1000),
|
|
3485
|
+
retryCount: 1,
|
|
3486
|
+
timestamp: Date.now()
|
|
3487
|
+
});
|
|
3488
|
+
|
|
3489
|
+
batcher.drain();
|
|
3490
|
+
activeExecutions.delete(conversationId);
|
|
3491
|
+
queries.setIsStreaming(conversationId, false);
|
|
3492
|
+
|
|
3493
|
+
setTimeout(() => {
|
|
3494
|
+
rateLimitState.delete(conversationId);
|
|
3495
|
+
broadcastSync({
|
|
3496
|
+
type: 'rate_limit_clear',
|
|
3497
|
+
conversationId,
|
|
3498
|
+
timestamp: Date.now()
|
|
3499
|
+
});
|
|
3500
|
+
scheduleRetry(conversationId, messageId, content, agentId, model);
|
|
3501
|
+
}, retryAfterSec * 1000);
|
|
3502
|
+
|
|
3503
|
+
return;
|
|
3504
|
+
}
|
|
3505
|
+
|
|
3349
3506
|
if (resultText) eagerTTS(resultText, conversationId, sessionId);
|
|
3350
3507
|
}
|
|
3351
3508
|
|
|
@@ -3412,6 +3569,13 @@ async function processMessageWithStreaming(conversationId, messageId, sessionId,
|
|
|
3412
3569
|
};
|
|
3413
3570
|
|
|
3414
3571
|
const { outputs, sessionId: claudeSessionId } = await runClaudeWithStreaming(content, cwd, agentId || 'claude-code', config);
|
|
3572
|
+
|
|
3573
|
+
// Check if rate limit was already handled in stream detection
|
|
3574
|
+
if (rateLimitState.get(conversationId)?.isStreamDetected) {
|
|
3575
|
+
debugLog(`[rate-limit] Rate limit already handled in stream for conv ${conversationId}, skipping success handler`);
|
|
3576
|
+
return;
|
|
3577
|
+
}
|
|
3578
|
+
|
|
3415
3579
|
activeExecutions.delete(conversationId);
|
|
3416
3580
|
batcher.drain();
|
|
3417
3581
|
debugLog(`[stream] Claude returned ${outputs.length} outputs, sessionId=${claudeSessionId}`);
|
|
@@ -3443,6 +3607,13 @@ async function processMessageWithStreaming(conversationId, messageId, sessionId,
|
|
|
3443
3607
|
const elapsed = Date.now() - startTime;
|
|
3444
3608
|
debugLog(`[stream] Error after ${elapsed}ms: ${error.message}`);
|
|
3445
3609
|
|
|
3610
|
+
// Check if rate limit was already handled in stream detection
|
|
3611
|
+
const existingState = rateLimitState.get(conversationId);
|
|
3612
|
+
if (existingState?.isStreamDetected) {
|
|
3613
|
+
debugLog(`[rate-limit] Rate limit already handled in stream for conv ${conversationId}, skipping catch handler`);
|
|
3614
|
+
return;
|
|
3615
|
+
}
|
|
3616
|
+
|
|
3446
3617
|
const isRateLimit = error.rateLimited ||
|
|
3447
3618
|
/rate.?limit|429|too many requests|overloaded|throttl/i.test(error.message);
|
|
3448
3619
|
|
|
@@ -3641,97 +3812,7 @@ wss.on('connection', (ws, req) => {
|
|
|
3641
3812
|
}));
|
|
3642
3813
|
|
|
3643
3814
|
ws.on('message', (msg) => {
|
|
3644
|
-
|
|
3645
|
-
const data = JSON.parse(msg);
|
|
3646
|
-
if (data.type === 'subscribe') {
|
|
3647
|
-
if (data.sessionId) {
|
|
3648
|
-
ws.subscriptions.add(data.sessionId);
|
|
3649
|
-
if (!subscriptionIndex.has(data.sessionId)) subscriptionIndex.set(data.sessionId, new Set());
|
|
3650
|
-
subscriptionIndex.get(data.sessionId).add(ws);
|
|
3651
|
-
}
|
|
3652
|
-
if (data.conversationId) {
|
|
3653
|
-
const key = `conv-${data.conversationId}`;
|
|
3654
|
-
ws.subscriptions.add(key);
|
|
3655
|
-
if (!subscriptionIndex.has(key)) subscriptionIndex.set(key, new Set());
|
|
3656
|
-
subscriptionIndex.get(key).add(ws);
|
|
3657
|
-
}
|
|
3658
|
-
const subTarget = data.sessionId || data.conversationId;
|
|
3659
|
-
debugLog(`[WebSocket] Client ${ws.clientId} subscribed to ${subTarget}`);
|
|
3660
|
-
ws.send(JSON.stringify({
|
|
3661
|
-
type: 'subscription_confirmed',
|
|
3662
|
-
sessionId: data.sessionId,
|
|
3663
|
-
conversationId: data.conversationId,
|
|
3664
|
-
timestamp: Date.now()
|
|
3665
|
-
}));
|
|
3666
|
-
} else if (data.type === 'unsubscribe') {
|
|
3667
|
-
if (data.sessionId) {
|
|
3668
|
-
ws.subscriptions.delete(data.sessionId);
|
|
3669
|
-
const idx = subscriptionIndex.get(data.sessionId);
|
|
3670
|
-
if (idx) { idx.delete(ws); if (idx.size === 0) subscriptionIndex.delete(data.sessionId); }
|
|
3671
|
-
}
|
|
3672
|
-
if (data.conversationId) {
|
|
3673
|
-
const key = `conv-${data.conversationId}`;
|
|
3674
|
-
ws.subscriptions.delete(key);
|
|
3675
|
-
const idx = subscriptionIndex.get(key);
|
|
3676
|
-
if (idx) { idx.delete(ws); if (idx.size === 0) subscriptionIndex.delete(key); }
|
|
3677
|
-
}
|
|
3678
|
-
debugLog(`[WebSocket] Client ${ws.clientId} unsubscribed from ${data.sessionId || data.conversationId}`);
|
|
3679
|
-
} else if (data.type === 'get_subscriptions') {
|
|
3680
|
-
ws.send(JSON.stringify({
|
|
3681
|
-
type: 'subscriptions',
|
|
3682
|
-
subscriptions: Array.from(ws.subscriptions),
|
|
3683
|
-
timestamp: Date.now()
|
|
3684
|
-
}));
|
|
3685
|
-
} else if (data.type === 'set_voice') {
|
|
3686
|
-
ws.ttsVoiceId = data.voiceId || 'default';
|
|
3687
|
-
} else if (data.type === 'latency_report') {
|
|
3688
|
-
ws.latencyTier = data.quality || 'good';
|
|
3689
|
-
ws.latencyAvg = data.avg || 0;
|
|
3690
|
-
ws.latencyTrend = data.trend || 'stable';
|
|
3691
|
-
} else if (data.type === 'ping') {
|
|
3692
|
-
ws.send(JSON.stringify({
|
|
3693
|
-
type: 'pong',
|
|
3694
|
-
requestId: data.requestId,
|
|
3695
|
-
timestamp: Date.now()
|
|
3696
|
-
}));
|
|
3697
|
-
} else if (data.type === 'terminal_start') {
|
|
3698
|
-
if (ws.terminalProc) {
|
|
3699
|
-
try { ws.terminalProc.kill(); } catch(e) {}
|
|
3700
|
-
}
|
|
3701
|
-
const { spawn } = require('child_process');
|
|
3702
|
-
const shell = process.env.SHELL || '/bin/bash';
|
|
3703
|
-
const cwd = data.cwd || process.env.STARTUP_CWD || process.env.HOME || '/';
|
|
3704
|
-
const proc = spawn(shell, [], { cwd, env: { ...process.env, TERM: 'xterm-256color', COLORTERM: 'truecolor' }, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
3705
|
-
ws.terminalProc = proc;
|
|
3706
|
-
proc.stdout.on('data', (chunk) => {
|
|
3707
|
-
if (ws.readyState === 1) ws.send(JSON.stringify({ type: 'terminal_output', data: chunk.toString('base64'), encoding: 'base64' }));
|
|
3708
|
-
});
|
|
3709
|
-
proc.stderr.on('data', (chunk) => {
|
|
3710
|
-
if (ws.readyState === 1) ws.send(JSON.stringify({ type: 'terminal_output', data: chunk.toString('base64'), encoding: 'base64' }));
|
|
3711
|
-
});
|
|
3712
|
-
proc.on('exit', (code) => {
|
|
3713
|
-
if (ws.readyState === 1) ws.send(JSON.stringify({ type: 'terminal_exit', code }));
|
|
3714
|
-
ws.terminalProc = null;
|
|
3715
|
-
});
|
|
3716
|
-
ws.send(JSON.stringify({ type: 'terminal_started', timestamp: Date.now() }));
|
|
3717
|
-
} else if (data.type === 'terminal_input') {
|
|
3718
|
-
if (ws.terminalProc && ws.terminalProc.stdin.writable) {
|
|
3719
|
-
ws.terminalProc.stdin.write(Buffer.from(data.data, 'base64'));
|
|
3720
|
-
}
|
|
3721
|
-
} else if (data.type === 'terminal_stop') {
|
|
3722
|
-
if (ws.terminalProc) {
|
|
3723
|
-
try { ws.terminalProc.kill(); } catch(e) {}
|
|
3724
|
-
ws.terminalProc = null;
|
|
3725
|
-
}
|
|
3726
|
-
}
|
|
3727
|
-
} catch (e) {
|
|
3728
|
-
console.error('WebSocket message parse error:', e.message);
|
|
3729
|
-
ws.send(JSON.stringify({
|
|
3730
|
-
type: 'error',
|
|
3731
|
-
error: 'Invalid message format',
|
|
3732
|
-
timestamp: Date.now()
|
|
3733
|
-
}));
|
|
3734
|
-
}
|
|
3815
|
+
wsRouter.onMessage(ws, msg);
|
|
3735
3816
|
});
|
|
3736
3817
|
|
|
3737
3818
|
ws.on('pong', () => { ws.isAlive = true; });
|
|
@@ -3787,6 +3868,117 @@ function broadcastSync(event) {
|
|
|
3787
3868
|
}
|
|
3788
3869
|
}
|
|
3789
3870
|
|
|
3871
|
+
// WebSocket protocol router
|
|
3872
|
+
const wsRouter = new WsRouter();
|
|
3873
|
+
|
|
3874
|
+
registerConvHandlers(wsRouter, {
|
|
3875
|
+
queries, activeExecutions, messageQueues, rateLimitState,
|
|
3876
|
+
broadcastSync, processMessageWithStreaming
|
|
3877
|
+
});
|
|
3878
|
+
|
|
3879
|
+
registerSessionHandlers(wsRouter, {
|
|
3880
|
+
db: queries, discoveredAgents, getModelsForAgent, modelCache,
|
|
3881
|
+
getAgentDescriptor, activeScripts, broadcastSync,
|
|
3882
|
+
startGeminiOAuth, geminiOAuthState: () => geminiOAuthState
|
|
3883
|
+
});
|
|
3884
|
+
|
|
3885
|
+
registerRunHandlers(wsRouter, {
|
|
3886
|
+
queries, discoveredAgents, activeExecutions, activeProcessesByRunId,
|
|
3887
|
+
broadcastSync, processMessageWithStreaming
|
|
3888
|
+
});
|
|
3889
|
+
|
|
3890
|
+
registerUtilHandlers(wsRouter, {
|
|
3891
|
+
queries, wsOptimizer, modelDownloadState, ensureModelsDownloaded,
|
|
3892
|
+
broadcastSync, getSpeech, getProviderConfigs, saveProviderConfig,
|
|
3893
|
+
startGeminiOAuth, exchangeGeminiOAuthCode,
|
|
3894
|
+
geminiOAuthState: () => geminiOAuthState,
|
|
3895
|
+
STARTUP_CWD
|
|
3896
|
+
});
|
|
3897
|
+
|
|
3898
|
+
wsRouter.onLegacy((data, ws) => {
|
|
3899
|
+
if (data.type === 'subscribe') {
|
|
3900
|
+
if (data.sessionId) {
|
|
3901
|
+
ws.subscriptions.add(data.sessionId);
|
|
3902
|
+
if (!subscriptionIndex.has(data.sessionId)) subscriptionIndex.set(data.sessionId, new Set());
|
|
3903
|
+
subscriptionIndex.get(data.sessionId).add(ws);
|
|
3904
|
+
}
|
|
3905
|
+
if (data.conversationId) {
|
|
3906
|
+
const key = `conv-${data.conversationId}`;
|
|
3907
|
+
ws.subscriptions.add(key);
|
|
3908
|
+
if (!subscriptionIndex.has(key)) subscriptionIndex.set(key, new Set());
|
|
3909
|
+
subscriptionIndex.get(key).add(ws);
|
|
3910
|
+
}
|
|
3911
|
+
const subTarget = data.sessionId || data.conversationId;
|
|
3912
|
+
debugLog(`[WebSocket] Client ${ws.clientId} subscribed to ${subTarget}`);
|
|
3913
|
+
ws.send(JSON.stringify({
|
|
3914
|
+
type: 'subscription_confirmed',
|
|
3915
|
+
sessionId: data.sessionId,
|
|
3916
|
+
conversationId: data.conversationId,
|
|
3917
|
+
timestamp: Date.now()
|
|
3918
|
+
}));
|
|
3919
|
+
} else if (data.type === 'unsubscribe') {
|
|
3920
|
+
if (data.sessionId) {
|
|
3921
|
+
ws.subscriptions.delete(data.sessionId);
|
|
3922
|
+
const idx = subscriptionIndex.get(data.sessionId);
|
|
3923
|
+
if (idx) { idx.delete(ws); if (idx.size === 0) subscriptionIndex.delete(data.sessionId); }
|
|
3924
|
+
}
|
|
3925
|
+
if (data.conversationId) {
|
|
3926
|
+
const key = `conv-${data.conversationId}`;
|
|
3927
|
+
ws.subscriptions.delete(key);
|
|
3928
|
+
const idx = subscriptionIndex.get(key);
|
|
3929
|
+
if (idx) { idx.delete(ws); if (idx.size === 0) subscriptionIndex.delete(key); }
|
|
3930
|
+
}
|
|
3931
|
+
debugLog(`[WebSocket] Client ${ws.clientId} unsubscribed from ${data.sessionId || data.conversationId}`);
|
|
3932
|
+
} else if (data.type === 'get_subscriptions') {
|
|
3933
|
+
ws.send(JSON.stringify({
|
|
3934
|
+
type: 'subscriptions',
|
|
3935
|
+
subscriptions: Array.from(ws.subscriptions),
|
|
3936
|
+
timestamp: Date.now()
|
|
3937
|
+
}));
|
|
3938
|
+
} else if (data.type === 'set_voice') {
|
|
3939
|
+
ws.ttsVoiceId = data.voiceId || 'default';
|
|
3940
|
+
} else if (data.type === 'latency_report') {
|
|
3941
|
+
ws.latencyTier = data.quality || 'good';
|
|
3942
|
+
ws.latencyAvg = data.avg || 0;
|
|
3943
|
+
ws.latencyTrend = data.trend || 'stable';
|
|
3944
|
+
} else if (data.type === 'ping') {
|
|
3945
|
+
ws.send(JSON.stringify({
|
|
3946
|
+
type: 'pong',
|
|
3947
|
+
requestId: data.requestId,
|
|
3948
|
+
timestamp: Date.now()
|
|
3949
|
+
}));
|
|
3950
|
+
} else if (data.type === 'terminal_start') {
|
|
3951
|
+
if (ws.terminalProc) {
|
|
3952
|
+
try { ws.terminalProc.kill(); } catch(e) {}
|
|
3953
|
+
}
|
|
3954
|
+
const { spawn } = require('child_process');
|
|
3955
|
+
const shell = process.env.SHELL || '/bin/bash';
|
|
3956
|
+
const cwd = data.cwd || process.env.STARTUP_CWD || process.env.HOME || '/';
|
|
3957
|
+
const proc = spawn(shell, [], { cwd, env: { ...process.env, TERM: 'xterm-256color', COLORTERM: 'truecolor' }, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
3958
|
+
ws.terminalProc = proc;
|
|
3959
|
+
proc.stdout.on('data', (chunk) => {
|
|
3960
|
+
if (ws.readyState === 1) ws.send(JSON.stringify({ type: 'terminal_output', data: chunk.toString('base64'), encoding: 'base64' }));
|
|
3961
|
+
});
|
|
3962
|
+
proc.stderr.on('data', (chunk) => {
|
|
3963
|
+
if (ws.readyState === 1) ws.send(JSON.stringify({ type: 'terminal_output', data: chunk.toString('base64'), encoding: 'base64' }));
|
|
3964
|
+
});
|
|
3965
|
+
proc.on('exit', (code) => {
|
|
3966
|
+
if (ws.readyState === 1) ws.send(JSON.stringify({ type: 'terminal_exit', code }));
|
|
3967
|
+
ws.terminalProc = null;
|
|
3968
|
+
});
|
|
3969
|
+
ws.send(JSON.stringify({ type: 'terminal_started', timestamp: Date.now() }));
|
|
3970
|
+
} else if (data.type === 'terminal_input') {
|
|
3971
|
+
if (ws.terminalProc && ws.terminalProc.stdin.writable) {
|
|
3972
|
+
ws.terminalProc.stdin.write(Buffer.from(data.data, 'base64'));
|
|
3973
|
+
}
|
|
3974
|
+
} else if (data.type === 'terminal_stop') {
|
|
3975
|
+
if (ws.terminalProc) {
|
|
3976
|
+
try { ws.terminalProc.kill(); } catch(e) {}
|
|
3977
|
+
ws.terminalProc = null;
|
|
3978
|
+
}
|
|
3979
|
+
}
|
|
3980
|
+
});
|
|
3981
|
+
|
|
3790
3982
|
// Heartbeat interval to detect stale connections
|
|
3791
3983
|
const heartbeatInterval = setInterval(() => {
|
|
3792
3984
|
syncClients.forEach(ws => {
|
package/static/app.js
CHANGED
|
@@ -107,8 +107,7 @@ class GMGUIApp {
|
|
|
107
107
|
|
|
108
108
|
async fetchAgents() {
|
|
109
109
|
try {
|
|
110
|
-
const
|
|
111
|
-
const data = await res.json();
|
|
110
|
+
const data = await window.wsClient.rpc('agent.ls');
|
|
112
111
|
for (const agent of data.agents || []) {
|
|
113
112
|
this.agents.set(agent.id, agent);
|
|
114
113
|
}
|
|
@@ -119,8 +118,7 @@ class GMGUIApp {
|
|
|
119
118
|
|
|
120
119
|
async fetchConversations() {
|
|
121
120
|
try {
|
|
122
|
-
const
|
|
123
|
-
const data = await res.json();
|
|
121
|
+
const data = await window.wsClient.rpc('conv.ls');
|
|
124
122
|
this.conversations.clear();
|
|
125
123
|
for (const conv of data.conversations || []) {
|
|
126
124
|
this.conversations.set(conv.id, conv);
|
|
@@ -315,19 +313,11 @@ class GMGUIApp {
|
|
|
315
313
|
}
|
|
316
314
|
|
|
317
315
|
try {
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
if (res.ok) {
|
|
325
|
-
const data = await res.json();
|
|
326
|
-
await this.fetchConversations();
|
|
327
|
-
this.renderChatHistory();
|
|
328
|
-
if (data.conversation) {
|
|
329
|
-
this.selectConversation(data.conversation.id);
|
|
330
|
-
}
|
|
316
|
+
const data = await window.wsClient.rpc('conv.new', { title, agentId: this.selectedAgent });
|
|
317
|
+
await this.fetchConversations();
|
|
318
|
+
this.renderChatHistory();
|
|
319
|
+
if (data.conversation) {
|
|
320
|
+
this.selectConversation(data.conversation.id);
|
|
331
321
|
}
|
|
332
322
|
} catch (e) {
|
|
333
323
|
console.error('[APP] Error creating conversation:', e);
|
package/static/index.html
CHANGED
|
@@ -2086,24 +2086,24 @@
|
|
|
2086
2086
|
html.dark .tool-color-search { background: #0a1c0e; }
|
|
2087
2087
|
.tool-color-search > .folded-tool-body { border-top-color: #bbf7d0; }
|
|
2088
2088
|
html.dark .tool-color-search > .folded-tool-body { border-top-color: #16a34a; }
|
|
2089
|
-
|
|
2090
|
-
/* Skill - Yellow/Amber */
|
|
2091
|
-
.tool-color-skill.folded-tool > .folded-tool-bar { background: #d9f99d; }
|
|
2092
|
-
html.dark .tool-color-skill.folded-tool > .folded-tool-bar { background: #65a30d; }
|
|
2093
|
-
.tool-color-skill.folded-tool > .folded-tool-bar:hover { background: #fde68a; }
|
|
2094
|
-
html.dark .tool-color-skill.folded-tool > .folded-tool-bar:hover { background: #78350f; }
|
|
2095
|
-
.tool-color-skill.folded-tool > .folded-tool-bar::before { color: #d97706; }
|
|
2096
|
-
html.dark .tool-color-skill.folded-tool > .folded-tool-bar::before { color: #fbbf24; }
|
|
2097
|
-
.tool-color-skill .folded-tool-icon { color: #d97706; }
|
|
2098
|
-
html.dark .tool-color-skill .folded-tool-icon { color: #fbbf24; }
|
|
2099
|
-
.tool-color-skill .folded-tool-name { color: #92400e; }
|
|
2100
|
-
html.dark .tool-color-skill .folded-tool-name { color: #fcd34d; }
|
|
2101
|
-
.tool-color-skill .folded-tool-desc { color: #b45309; }
|
|
2102
|
-
html.dark .tool-color-skill .folded-tool-desc { color: #fbbf24; }
|
|
2103
|
-
.tool-color-skill { background: #fffbeb; }
|
|
2104
|
-
html.dark .tool-color-skill { background: #1c1507; }
|
|
2105
|
-
.tool-color-skill > .folded-tool-body { border-top-color: #fde68a; }
|
|
2106
|
-
html.dark .tool-color-skill > .folded-tool-body { border-top-color: #78350f; }
|
|
2089
|
+
|
|
2090
|
+
/* Skill - Yellow/Amber */
|
|
2091
|
+
.tool-color-skill.folded-tool > .folded-tool-bar { background: #d9f99d; }
|
|
2092
|
+
html.dark .tool-color-skill.folded-tool > .folded-tool-bar { background: #65a30d; }
|
|
2093
|
+
.tool-color-skill.folded-tool > .folded-tool-bar:hover { background: #fde68a; }
|
|
2094
|
+
html.dark .tool-color-skill.folded-tool > .folded-tool-bar:hover { background: #78350f; }
|
|
2095
|
+
.tool-color-skill.folded-tool > .folded-tool-bar::before { color: #d97706; }
|
|
2096
|
+
html.dark .tool-color-skill.folded-tool > .folded-tool-bar::before { color: #fbbf24; }
|
|
2097
|
+
.tool-color-skill .folded-tool-icon { color: #d97706; }
|
|
2098
|
+
html.dark .tool-color-skill .folded-tool-icon { color: #fbbf24; }
|
|
2099
|
+
.tool-color-skill .folded-tool-name { color: #92400e; }
|
|
2100
|
+
html.dark .tool-color-skill .folded-tool-name { color: #fcd34d; }
|
|
2101
|
+
.tool-color-skill .folded-tool-desc { color: #b45309; }
|
|
2102
|
+
html.dark .tool-color-skill .folded-tool-desc { color: #fbbf24; }
|
|
2103
|
+
.tool-color-skill { background: #fffbeb; }
|
|
2104
|
+
html.dark .tool-color-skill { background: #1c1507; }
|
|
2105
|
+
.tool-color-skill > .folded-tool-body { border-top-color: #fde68a; }
|
|
2106
|
+
html.dark .tool-color-skill > .folded-tool-body { border-top-color: #78350f; }
|
|
2107
2107
|
|
|
2108
2108
|
.block-type-tool_result { background: #f3f4f6; }
|
|
2109
2109
|
html.dark .block-type-tool_result { background: #1f2937; }
|
|
@@ -3221,6 +3221,7 @@
|
|
|
3221
3221
|
<script defer src="/gm/js/kalman-filter.js"></script>
|
|
3222
3222
|
<script defer src="/gm/js/event-consolidator.js"></script>
|
|
3223
3223
|
<script defer src="/gm/js/websocket-manager.js"></script>
|
|
3224
|
+
<script defer src="/gm/js/ws-client.js"></script>
|
|
3224
3225
|
<script defer src="/gm/js/event-filter.js"></script>
|
|
3225
3226
|
<script defer src="/gm/js/syntax-highlighter.js"></script>
|
|
3226
3227
|
<script defer src="/gm/js/dialogs.js"></script>
|
package/static/js/client.js
CHANGED
|
@@ -384,10 +384,7 @@ class AgentGUIClient {
|
|
|
384
384
|
this.ui.stopButton.addEventListener('click', async () => {
|
|
385
385
|
if (!this.state.currentConversation) return;
|
|
386
386
|
try {
|
|
387
|
-
const
|
|
388
|
-
method: 'POST'
|
|
389
|
-
});
|
|
390
|
-
const data = await resp.json();
|
|
387
|
+
const data = await window.wsClient.rpc('conv.cancel', { id: this.state.currentConversation.id });
|
|
391
388
|
console.log('Stop response:', data);
|
|
392
389
|
} catch (err) {
|
|
393
390
|
console.error('Failed to stop:', err);
|
|
@@ -401,12 +398,7 @@ class AgentGUIClient {
|
|
|
401
398
|
const instructions = await window.UIDialog.prompt('Enter instructions to inject into the running agent:', '', 'Inject Instructions');
|
|
402
399
|
if (!instructions) return;
|
|
403
400
|
try {
|
|
404
|
-
const
|
|
405
|
-
method: 'POST',
|
|
406
|
-
headers: { 'Content-Type': 'application/json' },
|
|
407
|
-
body: JSON.stringify({ content: instructions })
|
|
408
|
-
});
|
|
409
|
-
const data = await resp.json();
|
|
401
|
+
const data = await window.wsClient.rpc('conv.inject', { id: this.state.currentConversation.id, content: instructions });
|
|
410
402
|
console.log('Inject response:', data);
|
|
411
403
|
} catch (err) {
|
|
412
404
|
console.error('Failed to inject:', err);
|
|
@@ -603,9 +595,8 @@ class AgentGUIClient {
|
|
|
603
595
|
`;
|
|
604
596
|
messagesEl = outputEl.querySelector('.conversation-messages');
|
|
605
597
|
try {
|
|
606
|
-
const
|
|
607
|
-
if (
|
|
608
|
-
const fullData = await fullResp.json();
|
|
598
|
+
const fullData = await window.wsClient.rpc('conv.full', { id: data.conversationId });
|
|
599
|
+
if (fullData) {
|
|
609
600
|
const priorChunks = (fullData.chunks || []).map(c => ({
|
|
610
601
|
...c,
|
|
611
602
|
block: typeof c.data === 'string' ? JSON.parse(c.data) : c.data
|