agentgui 1.0.398 → 1.0.400
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 +1 -1
- package/server.js +275 -91
- package/static/app.js +7 -17
- package/static/index.html +19 -18
- package/static/js/client.js +4 -13
- package/static/js/conversations.js +19 -54
- 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
|
|
|
@@ -3300,6 +3305,82 @@ async function processMessageWithStreaming(conversationId, messageId, sessionId,
|
|
|
3300
3305
|
});
|
|
3301
3306
|
|
|
3302
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
|
+
|
|
3303
3384
|
eagerTTS(block.text, conversationId, sessionId);
|
|
3304
3385
|
}
|
|
3305
3386
|
}
|
|
@@ -3354,6 +3435,74 @@ async function processMessageWithStreaming(conversationId, messageId, sessionId,
|
|
|
3354
3435
|
|
|
3355
3436
|
if (parsed.result) {
|
|
3356
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
|
+
|
|
3357
3506
|
if (resultText) eagerTTS(resultText, conversationId, sessionId);
|
|
3358
3507
|
}
|
|
3359
3508
|
|
|
@@ -3420,6 +3569,13 @@ async function processMessageWithStreaming(conversationId, messageId, sessionId,
|
|
|
3420
3569
|
};
|
|
3421
3570
|
|
|
3422
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
|
+
|
|
3423
3579
|
activeExecutions.delete(conversationId);
|
|
3424
3580
|
batcher.drain();
|
|
3425
3581
|
debugLog(`[stream] Claude returned ${outputs.length} outputs, sessionId=${claudeSessionId}`);
|
|
@@ -3451,6 +3607,13 @@ async function processMessageWithStreaming(conversationId, messageId, sessionId,
|
|
|
3451
3607
|
const elapsed = Date.now() - startTime;
|
|
3452
3608
|
debugLog(`[stream] Error after ${elapsed}ms: ${error.message}`);
|
|
3453
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
|
+
|
|
3454
3617
|
const isRateLimit = error.rateLimited ||
|
|
3455
3618
|
/rate.?limit|429|too many requests|overloaded|throttl/i.test(error.message);
|
|
3456
3619
|
|
|
@@ -3649,97 +3812,7 @@ wss.on('connection', (ws, req) => {
|
|
|
3649
3812
|
}));
|
|
3650
3813
|
|
|
3651
3814
|
ws.on('message', (msg) => {
|
|
3652
|
-
|
|
3653
|
-
const data = JSON.parse(msg);
|
|
3654
|
-
if (data.type === 'subscribe') {
|
|
3655
|
-
if (data.sessionId) {
|
|
3656
|
-
ws.subscriptions.add(data.sessionId);
|
|
3657
|
-
if (!subscriptionIndex.has(data.sessionId)) subscriptionIndex.set(data.sessionId, new Set());
|
|
3658
|
-
subscriptionIndex.get(data.sessionId).add(ws);
|
|
3659
|
-
}
|
|
3660
|
-
if (data.conversationId) {
|
|
3661
|
-
const key = `conv-${data.conversationId}`;
|
|
3662
|
-
ws.subscriptions.add(key);
|
|
3663
|
-
if (!subscriptionIndex.has(key)) subscriptionIndex.set(key, new Set());
|
|
3664
|
-
subscriptionIndex.get(key).add(ws);
|
|
3665
|
-
}
|
|
3666
|
-
const subTarget = data.sessionId || data.conversationId;
|
|
3667
|
-
debugLog(`[WebSocket] Client ${ws.clientId} subscribed to ${subTarget}`);
|
|
3668
|
-
ws.send(JSON.stringify({
|
|
3669
|
-
type: 'subscription_confirmed',
|
|
3670
|
-
sessionId: data.sessionId,
|
|
3671
|
-
conversationId: data.conversationId,
|
|
3672
|
-
timestamp: Date.now()
|
|
3673
|
-
}));
|
|
3674
|
-
} else if (data.type === 'unsubscribe') {
|
|
3675
|
-
if (data.sessionId) {
|
|
3676
|
-
ws.subscriptions.delete(data.sessionId);
|
|
3677
|
-
const idx = subscriptionIndex.get(data.sessionId);
|
|
3678
|
-
if (idx) { idx.delete(ws); if (idx.size === 0) subscriptionIndex.delete(data.sessionId); }
|
|
3679
|
-
}
|
|
3680
|
-
if (data.conversationId) {
|
|
3681
|
-
const key = `conv-${data.conversationId}`;
|
|
3682
|
-
ws.subscriptions.delete(key);
|
|
3683
|
-
const idx = subscriptionIndex.get(key);
|
|
3684
|
-
if (idx) { idx.delete(ws); if (idx.size === 0) subscriptionIndex.delete(key); }
|
|
3685
|
-
}
|
|
3686
|
-
debugLog(`[WebSocket] Client ${ws.clientId} unsubscribed from ${data.sessionId || data.conversationId}`);
|
|
3687
|
-
} else if (data.type === 'get_subscriptions') {
|
|
3688
|
-
ws.send(JSON.stringify({
|
|
3689
|
-
type: 'subscriptions',
|
|
3690
|
-
subscriptions: Array.from(ws.subscriptions),
|
|
3691
|
-
timestamp: Date.now()
|
|
3692
|
-
}));
|
|
3693
|
-
} else if (data.type === 'set_voice') {
|
|
3694
|
-
ws.ttsVoiceId = data.voiceId || 'default';
|
|
3695
|
-
} else if (data.type === 'latency_report') {
|
|
3696
|
-
ws.latencyTier = data.quality || 'good';
|
|
3697
|
-
ws.latencyAvg = data.avg || 0;
|
|
3698
|
-
ws.latencyTrend = data.trend || 'stable';
|
|
3699
|
-
} else if (data.type === 'ping') {
|
|
3700
|
-
ws.send(JSON.stringify({
|
|
3701
|
-
type: 'pong',
|
|
3702
|
-
requestId: data.requestId,
|
|
3703
|
-
timestamp: Date.now()
|
|
3704
|
-
}));
|
|
3705
|
-
} else if (data.type === 'terminal_start') {
|
|
3706
|
-
if (ws.terminalProc) {
|
|
3707
|
-
try { ws.terminalProc.kill(); } catch(e) {}
|
|
3708
|
-
}
|
|
3709
|
-
const { spawn } = require('child_process');
|
|
3710
|
-
const shell = process.env.SHELL || '/bin/bash';
|
|
3711
|
-
const cwd = data.cwd || process.env.STARTUP_CWD || process.env.HOME || '/';
|
|
3712
|
-
const proc = spawn(shell, [], { cwd, env: { ...process.env, TERM: 'xterm-256color', COLORTERM: 'truecolor' }, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
3713
|
-
ws.terminalProc = proc;
|
|
3714
|
-
proc.stdout.on('data', (chunk) => {
|
|
3715
|
-
if (ws.readyState === 1) ws.send(JSON.stringify({ type: 'terminal_output', data: chunk.toString('base64'), encoding: 'base64' }));
|
|
3716
|
-
});
|
|
3717
|
-
proc.stderr.on('data', (chunk) => {
|
|
3718
|
-
if (ws.readyState === 1) ws.send(JSON.stringify({ type: 'terminal_output', data: chunk.toString('base64'), encoding: 'base64' }));
|
|
3719
|
-
});
|
|
3720
|
-
proc.on('exit', (code) => {
|
|
3721
|
-
if (ws.readyState === 1) ws.send(JSON.stringify({ type: 'terminal_exit', code }));
|
|
3722
|
-
ws.terminalProc = null;
|
|
3723
|
-
});
|
|
3724
|
-
ws.send(JSON.stringify({ type: 'terminal_started', timestamp: Date.now() }));
|
|
3725
|
-
} else if (data.type === 'terminal_input') {
|
|
3726
|
-
if (ws.terminalProc && ws.terminalProc.stdin.writable) {
|
|
3727
|
-
ws.terminalProc.stdin.write(Buffer.from(data.data, 'base64'));
|
|
3728
|
-
}
|
|
3729
|
-
} else if (data.type === 'terminal_stop') {
|
|
3730
|
-
if (ws.terminalProc) {
|
|
3731
|
-
try { ws.terminalProc.kill(); } catch(e) {}
|
|
3732
|
-
ws.terminalProc = null;
|
|
3733
|
-
}
|
|
3734
|
-
}
|
|
3735
|
-
} catch (e) {
|
|
3736
|
-
console.error('WebSocket message parse error:', e.message);
|
|
3737
|
-
ws.send(JSON.stringify({
|
|
3738
|
-
type: 'error',
|
|
3739
|
-
error: 'Invalid message format',
|
|
3740
|
-
timestamp: Date.now()
|
|
3741
|
-
}));
|
|
3742
|
-
}
|
|
3815
|
+
wsRouter.onMessage(ws, msg);
|
|
3743
3816
|
});
|
|
3744
3817
|
|
|
3745
3818
|
ws.on('pong', () => { ws.isAlive = true; });
|
|
@@ -3795,6 +3868,117 @@ function broadcastSync(event) {
|
|
|
3795
3868
|
}
|
|
3796
3869
|
}
|
|
3797
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
|
+
|
|
3798
3982
|
// Heartbeat interval to detect stale connections
|
|
3799
3983
|
const heartbeatInterval = setInterval(() => {
|
|
3800
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
|