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/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
- try {
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 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