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/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: conv.workingDirectory, name: 'Files' });
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
- const conversation = queries.createConversation(body.agentId, body.title, body.workingDirectory || null, body.model || null);
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
- try {
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 res = await fetch(BASE_URL + '/api/agents');
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 res = await fetch(BASE_URL + '/api/conversations');
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 res = await fetch(BASE_URL + '/api/conversations', {
319
- method: 'POST',
320
- headers: { 'Content-Type': 'application/json' },
321
- body: JSON.stringify({ title, agentId: this.selectedAgent })
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>
@@ -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 resp = await fetch(`${window.__BASE_URL}/api/conversations/${this.state.currentConversation.id}/cancel`, {
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 resp = await fetch(`${window.__BASE_URL}/api/conversations/${this.state.currentConversation.id}/inject`, {
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 fullResp = await fetch(window.__BASE_URL + `/api/conversations/${data.conversationId}/full`);
607
- if (fullResp.ok) {
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