agentgui 1.0.401 → 1.0.402

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.
@@ -1,7 +1,7 @@
1
1
  import fs from 'fs';
2
2
  import os from 'os';
3
3
  import path from 'path';
4
- import { execSync } from 'child_process';
4
+ import { execSync, spawn } from 'child_process';
5
5
 
6
6
  function err(code, message) { const e = new Error(message); e.code = code; throw e; }
7
7
 
@@ -9,7 +9,7 @@ export function register(router, deps) {
9
9
  const { queries, wsOptimizer, modelDownloadState, ensureModelsDownloaded,
10
10
  broadcastSync, getSpeech, getProviderConfigs, saveProviderConfig,
11
11
  startGeminiOAuth, exchangeGeminiOAuthCode, geminiOAuthState,
12
- STARTUP_CWD } = deps;
12
+ STARTUP_CWD, activeScripts } = deps;
13
13
 
14
14
  router.handle('home', () => ({ home: os.homedir(), cwd: STARTUP_CWD }));
15
15
 
@@ -183,4 +183,52 @@ export function register(router, deps) {
183
183
  });
184
184
 
185
185
  router.handle('ws.stats', () => wsOptimizer.getStats());
186
+
187
+ router.handle('conv.scripts', (p) => {
188
+ const conv = queries.getConversation(p.id);
189
+ if (!conv) err(404, 'Not found');
190
+ const wd = conv.workingDirectory || STARTUP_CWD;
191
+ let hasStart = false, hasDev = false;
192
+ try {
193
+ const pkg = JSON.parse(fs.readFileSync(path.join(wd, 'package.json'), 'utf-8'));
194
+ const scripts = pkg.scripts || {};
195
+ hasStart = !!scripts.start;
196
+ hasDev = !!scripts.dev;
197
+ } catch {}
198
+ const running = activeScripts.has(p.id);
199
+ const runningScript = running ? activeScripts.get(p.id).script : null;
200
+ return { hasStart, hasDev, running, runningScript };
201
+ });
202
+
203
+ router.handle('conv.run-script', (p) => {
204
+ const conv = queries.getConversation(p.id);
205
+ if (!conv) err(404, 'Not found');
206
+ if (activeScripts.has(p.id)) err(409, 'Script already running');
207
+ const script = p.script;
208
+ if (script !== 'start' && script !== 'dev') err(400, 'Invalid script');
209
+ const wd = conv.workingDirectory || STARTUP_CWD;
210
+ try {
211
+ const pkg = JSON.parse(fs.readFileSync(path.join(wd, 'package.json'), 'utf-8'));
212
+ if (!pkg.scripts || !pkg.scripts[script]) err(400, `Script "${script}" not found`);
213
+ } catch (e) { if (e.code) throw e; err(400, 'No package.json'); }
214
+ const childEnv = { ...process.env, FORCE_COLOR: '1' };
215
+ delete childEnv.PORT; delete childEnv.BASE_URL; delete childEnv.HOT_RELOAD;
216
+ const isWindows = os.platform() === 'win32';
217
+ const child = spawn('npm', ['run', script], { cwd: wd, stdio: ['ignore', 'pipe', 'pipe'], detached: true, env: childEnv, shell: isWindows });
218
+ activeScripts.set(p.id, { process: child, script, startTime: Date.now() });
219
+ broadcastSync({ type: 'script_started', conversationId: p.id, script, timestamp: Date.now() });
220
+ const onData = (stream) => (chunk) => broadcastSync({ type: 'script_output', conversationId: p.id, data: chunk.toString(), stream, timestamp: Date.now() });
221
+ child.stdout.on('data', onData('stdout'));
222
+ child.stderr.on('data', onData('stderr'));
223
+ child.on('error', (e) => { activeScripts.delete(p.id); broadcastSync({ type: 'script_stopped', conversationId: p.id, code: 1, error: e.message, timestamp: Date.now() }); });
224
+ child.on('close', (code) => { activeScripts.delete(p.id); broadcastSync({ type: 'script_stopped', conversationId: p.id, code: code || 0, timestamp: Date.now() }); });
225
+ return { ok: true, script, pid: child.pid };
226
+ });
227
+
228
+ router.handle('conv.stop-script', (p) => {
229
+ const entry = activeScripts.get(p.id);
230
+ if (!entry) err(404, 'No running script');
231
+ try { process.kill(-entry.process.pid, 'SIGTERM'); } catch { try { entry.process.kill('SIGTERM'); } catch {} }
232
+ return { ok: true };
233
+ });
186
234
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.401",
3
+ "version": "1.0.402",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "server.js",
@@ -22,6 +22,9 @@
22
22
  },
23
23
  "dependencies": {
24
24
  "@anthropic-ai/claude-code": "^2.1.37",
25
+ "@google/gemini-cli": "latest",
26
+ "@kilocode/cli": "latest",
27
+ "opencode-ai": "latest",
25
28
  "@huggingface/transformers": "^3.8.1",
26
29
  "audio-decode": "^2.2.3",
27
30
  "better-sqlite3": "^12.6.2",
package/server.js CHANGED
@@ -311,6 +311,8 @@ expressApp.use(BASE_URL + '/files/:conversationId', (req, res, next) => {
311
311
 
312
312
  function findCommand(cmd) {
313
313
  const isWindows = os.platform() === 'win32';
314
+ const localBin = path.join(path.dirname(fileURLToPath(import.meta.url)), 'node_modules', '.bin', isWindows ? cmd + '.cmd' : cmd);
315
+ if (fs.existsSync(localBin)) return localBin;
314
316
  try {
315
317
  if (isWindows) {
316
318
  const result = execSync(`where ${cmd}`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] }).trim();
@@ -3892,7 +3894,7 @@ registerUtilHandlers(wsRouter, {
3892
3894
  broadcastSync, getSpeech, getProviderConfigs, saveProviderConfig,
3893
3895
  startGeminiOAuth, exchangeGeminiOAuthCode,
3894
3896
  geminiOAuthState: () => geminiOAuthState,
3895
- STARTUP_CWD
3897
+ STARTUP_CWD, activeScripts
3896
3898
  });
3897
3899
 
3898
3900
  wsRouter.onLegacy((data, ws) => {
package/static/app.js CHANGED
@@ -131,8 +131,7 @@ class GMGUIApp {
131
131
 
132
132
  async fetchMessages(conversationId) {
133
133
  try {
134
- const res = await fetch(BASE_URL + `/api/conversations/${conversationId}/messages`);
135
- const data = await res.json();
134
+ const data = await window.wsClient.rpc('msg.ls', { id: conversationId });
136
135
  return data.messages || [];
137
136
  } catch (e) {
138
137
  console.error('[APP] Error fetching messages:', e);
@@ -289,18 +288,8 @@ class GMGUIApp {
289
288
  if (!content || !this.currentConversation || !this.selectedAgent) return;
290
289
 
291
290
  try {
292
- const res = await fetch(BASE_URL + `/api/conversations/${this.currentConversation}/messages`, {
293
- method: 'POST',
294
- headers: { 'Content-Type': 'application/json' },
295
- body: JSON.stringify({
296
- content,
297
- agentId: this.selectedAgent
298
- })
299
- });
300
-
301
- if (res.ok) {
302
- input.value = '';
303
- }
291
+ await window.wsClient.rpc('msg.send', { id: this.currentConversation, content, agentId: this.selectedAgent });
292
+ input.value = '';
304
293
  } catch (e) {
305
294
  console.error('[APP] Error sending message:', e);
306
295
  }
@@ -1,5 +1,4 @@
1
1
  (function() {
2
- var BASE = window.__BASE_URL || '';
3
2
  var btn = document.getElementById('agentAuthBtn');
4
3
  var dropdown = document.getElementById('agentAuthDropdown');
5
4
  var agents = [], providers = {}, authRunning = false, editingProvider = null;
@@ -20,13 +19,13 @@
20
19
  function refresh() { fetchAuthStatus(); fetchProviderConfigs(); }
21
20
 
22
21
  function fetchAuthStatus() {
23
- fetch(BASE + '/api/agents/auth-status').then(function(r) { return r.json(); })
22
+ window.wsClient.rpc('agent.authstat')
24
23
  .then(function(data) { agents = data.agents || []; updateButton(); renderDropdown(); })
25
24
  .catch(function() {});
26
25
  }
27
26
 
28
27
  function fetchProviderConfigs() {
29
- fetch(BASE + '/api/auth/configs').then(function(r) { return r.json(); })
28
+ window.wsClient.rpc('auth.configs')
30
29
  .then(function(data) { providers = data || {}; updateButton(); renderDropdown(); })
31
30
  .catch(function() {});
32
31
  }
@@ -114,12 +113,10 @@
114
113
  function toggleEdit(pid) { editingProvider = editingProvider === pid ? null : pid; renderDropdown(); }
115
114
 
116
115
  function saveProviderKey(providerId, apiKey) {
117
- fetch(BASE + '/api/auth/save-config', {
118
- method: 'POST', headers: { 'Content-Type': 'application/json' },
119
- body: JSON.stringify({ providerId: providerId, apiKey: apiKey, defaultModel: '' })
120
- }).then(function(r) { return r.json(); }).then(function(data) {
121
- if (data.success) { editingProvider = null; fetchProviderConfigs(); }
122
- }).catch(function() { editingProvider = null; renderDropdown(); });
116
+ window.wsClient.rpc('auth.save', { providerId: providerId, apiKey: apiKey, defaultModel: '' })
117
+ .then(function(data) {
118
+ if (data.success) { editingProvider = null; fetchProviderConfigs(); }
119
+ }).catch(function() { editingProvider = null; renderDropdown(); });
123
120
  }
124
121
 
125
122
  function toggleDropdown(e) {
@@ -201,64 +198,62 @@
201
198
  if (submitBtn) { submitBtn.disabled = true; submitBtn.textContent = 'Verifying...'; }
202
199
  if (errorEl) errorEl.style.display = 'none';
203
200
 
204
- fetch(BASE + '/api/gemini-oauth/complete', {
205
- method: 'POST', headers: { 'Content-Type': 'application/json' },
206
- body: JSON.stringify({ url: url })
207
- }).then(function(r) { return r.json(); }).then(function(data) {
208
- if (data.success) {
209
- cleanupOAuthPolling();
210
- authRunning = false;
211
- removeOAuthModal();
212
- refresh();
213
- } else {
214
- if (errorEl) { errorEl.textContent = data.error || 'Failed to complete authentication.'; errorEl.style.display = 'block'; }
201
+ window.wsClient.rpc('gemini.complete', { url: url })
202
+ .then(function(data) {
203
+ if (data.success) {
204
+ cleanupOAuthPolling();
205
+ authRunning = false;
206
+ removeOAuthModal();
207
+ refresh();
208
+ } else {
209
+ if (errorEl) { errorEl.textContent = data.error || 'Failed to complete authentication.'; errorEl.style.display = 'block'; }
210
+ if (submitBtn) { submitBtn.disabled = false; submitBtn.textContent = 'Complete Sign-In'; }
211
+ }
212
+ }).catch(function(e) {
213
+ if (errorEl) { errorEl.textContent = e.message; errorEl.style.display = 'block'; }
215
214
  if (submitBtn) { submitBtn.disabled = false; submitBtn.textContent = 'Complete Sign-In'; }
216
- }
217
- }).catch(function(e) {
218
- if (errorEl) { errorEl.textContent = e.message; errorEl.style.display = 'block'; }
219
- if (submitBtn) { submitBtn.disabled = false; submitBtn.textContent = 'Complete Sign-In'; }
220
- });
215
+ });
221
216
  }
222
217
 
223
218
  function triggerAuth(agentId) {
224
219
  if (authRunning) return;
225
- fetch(BASE + '/api/agents/' + agentId + '/auth', {
226
- method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}'
227
- }).then(function(r) { return r.json(); }).then(function(data) {
228
- if (data.ok) {
229
- authRunning = true; showTerminalTab(); switchToTerminalView();
230
- var term = getTerminal();
231
- if (term) { term.clear(); term.writeln('\x1b[36m[authenticating ' + agentId + ']\x1b[0m\r\n'); }
232
- if (data.authUrl) {
233
- window.open(data.authUrl, '_blank');
234
- if (agentId === 'gemini') {
235
- showOAuthWaitingModal();
236
- cleanupOAuthPolling();
237
- oauthPollInterval = setInterval(function() {
238
- fetch(BASE + '/api/gemini-oauth/status').then(function(r) { return r.json(); }).then(function(status) {
239
- if (status.status === 'success') {
240
- cleanupOAuthPolling();
241
- authRunning = false;
242
- removeOAuthModal();
243
- refresh();
244
- } else if (status.status === 'error') {
245
- cleanupOAuthPolling();
246
- authRunning = false;
247
- removeOAuthModal();
248
- }
249
- }).catch(function() {});
250
- }, 1500);
251
- oauthFallbackTimer = setTimeout(function() {
252
- if (authRunning) showOAuthPasteFallback();
253
- }, 30000);
254
- oauthPollTimeout = setTimeout(function() {
220
+ window.wsClient.rpc('agent.auth', { id: agentId })
221
+ .then(function(data) {
222
+ if (data.ok) {
223
+ authRunning = true; showTerminalTab(); switchToTerminalView();
224
+ var term = getTerminal();
225
+ if (term) { term.clear(); term.writeln('\x1b[36m[authenticating ' + agentId + ']\x1b[0m\r\n'); }
226
+ if (data.authUrl) {
227
+ window.open(data.authUrl, '_blank');
228
+ if (agentId === 'gemini') {
229
+ showOAuthWaitingModal();
255
230
  cleanupOAuthPolling();
256
- if (authRunning) { authRunning = false; removeOAuthModal(); }
257
- }, 5 * 60 * 1000);
231
+ oauthPollInterval = setInterval(function() {
232
+ window.wsClient.rpc('gemini.status')
233
+ .then(function(status) {
234
+ if (status.status === 'success') {
235
+ cleanupOAuthPolling();
236
+ authRunning = false;
237
+ removeOAuthModal();
238
+ refresh();
239
+ } else if (status.status === 'error') {
240
+ cleanupOAuthPolling();
241
+ authRunning = false;
242
+ removeOAuthModal();
243
+ }
244
+ }).catch(function() {});
245
+ }, 1500);
246
+ oauthFallbackTimer = setTimeout(function() {
247
+ if (authRunning) showOAuthPasteFallback();
248
+ }, 30000);
249
+ oauthPollTimeout = setTimeout(function() {
250
+ cleanupOAuthPolling();
251
+ if (authRunning) { authRunning = false; removeOAuthModal(); }
252
+ }, 5 * 60 * 1000);
253
+ }
258
254
  }
259
255
  }
260
- }
261
- }).catch(function() {});
256
+ }).catch(function() {});
262
257
  }
263
258
 
264
259
  function onWsMessage(e) {
@@ -14,9 +14,10 @@ class AgentGUIClient {
14
14
  ...config
15
15
  };
16
16
 
17
- // Initialize components
17
+ // Initialize components - reuse global wsManager/wsClient if available
18
18
  this.renderer = new StreamingRenderer(config.renderer || {});
19
- this.wsManager = new WebSocketManager(config.websocket || {});
19
+ this.wsManager = window.wsManager || new WebSocketManager(config.websocket || {});
20
+ if (!window.wsManager) window.wsManager = this.wsManager;
20
21
  this.eventProcessor = new EventProcessor(config.eventProcessor || {});
21
22
 
22
23
  // Application state
@@ -881,8 +882,7 @@ class AgentGUIClient {
881
882
 
882
883
  async _promptPushIfWeOwnRemote() {
883
884
  try {
884
- const result = await fetch(window.__BASE_URL + '/api/git/check-remote-ownership');
885
- const { ownsRemote, hasChanges, hasUnpushed, remoteUrl } = await result.json();
885
+ const { ownsRemote, hasChanges, hasUnpushed, remoteUrl } = await window.wsClient.rpc('git.check');
886
886
  if (ownsRemote && (hasChanges || hasUnpushed)) {
887
887
  const conv = this.state.currentConversation;
888
888
  if (conv) {
@@ -987,8 +987,7 @@ class AgentGUIClient {
987
987
  if (!outputEl) return;
988
988
 
989
989
  try {
990
- const response = await fetch(window.__BASE_URL + `/api/conversations/${conversationId}/queue`);
991
- const { queue } = await response.json();
990
+ const { queue } = await window.wsClient.rpc('q.ls', { id: conversationId });
992
991
 
993
992
  let queueEl = outputEl.querySelector('.queue-indicator');
994
993
  if (!queue || queue.length === 0) {
@@ -1015,7 +1014,7 @@ class AgentGUIClient {
1015
1014
  const index = parseInt(e.target.dataset.index);
1016
1015
  const msgId = queue[index].messageId;
1017
1016
  if (await window.UIDialog.confirm('Delete this queued message?', 'Delete Message')) {
1018
- await fetch(window.__BASE_URL + `/api/conversations/${conversationId}/queue/${msgId}`, { method: 'DELETE' });
1017
+ await window.wsClient.rpc('q.del', { id: conversationId, messageId: msgId });
1019
1018
  }
1020
1019
  });
1021
1020
  });
@@ -1026,11 +1025,7 @@ class AgentGUIClient {
1026
1025
  const q = queue[index];
1027
1026
  const newContent = await window.UIDialog.prompt('Edit message:', q.content, 'Edit Queued Message');
1028
1027
  if (newContent !== null && newContent !== q.content) {
1029
- fetch(window.__BASE_URL + `/api/conversations/${conversationId}/queue/${q.messageId}`, {
1030
- method: 'PATCH',
1031
- headers: { 'Content-Type': 'application/json' },
1032
- body: JSON.stringify({ content: newContent })
1033
- });
1028
+ window.wsClient.rpc('q.upd', { id: conversationId, messageId: q.messageId, content: newContent });
1034
1029
  }
1035
1030
  });
1036
1031
  });
@@ -1281,12 +1276,7 @@ class AgentGUIClient {
1281
1276
  } else {
1282
1277
  const body = { agentId, title: savedPrompt.substring(0, 50) };
1283
1278
  if (model) body.model = model;
1284
- const response = await fetch(window.__BASE_URL + '/api/conversations', {
1285
- method: 'POST',
1286
- headers: { 'Content-Type': 'application/json' },
1287
- body: JSON.stringify(body)
1288
- });
1289
- const { conversation } = await response.json();
1279
+ const { conversation } = await window.wsClient.rpc('conv.new', body);
1290
1280
  this.state.currentConversation = conversation;
1291
1281
  this.lockAgentAndModel(agentId, model);
1292
1282
 
@@ -1351,10 +1341,7 @@ class AgentGUIClient {
1351
1341
  if (lastSeq < 0) return;
1352
1342
 
1353
1343
  try {
1354
- const url = `${window.__BASE_URL}/api/sessions/${sessionId}/chunks?sinceSeq=${lastSeq}`;
1355
- const resp = await fetch(url);
1356
- if (!resp.ok) return;
1357
- const { chunks: rawChunks } = await resp.json();
1344
+ const { chunks: rawChunks } = await window.wsClient.rpc('sess.chunks', { id: sessionId, sinceSeq: lastSeq });
1358
1345
  if (!rawChunks || rawChunks.length === 0) return;
1359
1346
 
1360
1347
  const chunks = rawChunks.map(c => ({
@@ -1545,43 +1532,29 @@ class AgentGUIClient {
1545
1532
  this.wsManager.sendMessage({ type: 'subscribe', conversationId });
1546
1533
  }
1547
1534
 
1548
- const streamBody = { content: prompt, agentId };
1535
+ const streamBody = { id: conversationId, content: prompt, agentId };
1549
1536
  if (model) streamBody.model = model;
1550
- const response = await fetch(`${window.__BASE_URL}/api/conversations/${conversationId}/stream`, {
1551
- method: 'POST',
1552
- headers: { 'Content-Type': 'application/json' },
1553
- body: JSON.stringify(streamBody)
1554
- });
1555
-
1556
- if (response.status === 404) {
1557
- console.warn('Conversation not found, recreating:', conversationId);
1558
- const conv = this.state.currentConversation;
1559
- const createBody = {
1560
- agentId,
1561
- title: conv?.title || prompt.substring(0, 50),
1562
- workingDirectory: conv?.workingDirectory || null
1563
- };
1564
- if (model) createBody.model = model;
1565
- const createResp = await fetch(window.__BASE_URL + '/api/conversations', {
1566
- method: 'POST',
1567
- headers: { 'Content-Type': 'application/json' },
1568
- body: JSON.stringify(createBody)
1569
- });
1570
- if (!createResp.ok) throw new Error(`Failed to recreate conversation: HTTP ${createResp.status}`);
1571
- const { conversation: newConv } = await createResp.json();
1572
- this.state.currentConversation = newConv;
1573
- if (window.conversationManager) {
1574
- window.conversationManager.loadConversations();
1575
- window.conversationManager.select(newConv.id);
1537
+ let result;
1538
+ try {
1539
+ result = await window.wsClient.rpc('msg.stream', streamBody);
1540
+ } catch (e) {
1541
+ if (e.code === 404) {
1542
+ console.warn('Conversation not found, recreating:', conversationId);
1543
+ const conv = this.state.currentConversation;
1544
+ const createBody = { agentId, title: conv?.title || prompt.substring(0, 50), workingDirectory: conv?.workingDirectory || null };
1545
+ if (model) createBody.model = model;
1546
+ const { conversation: newConv } = await window.wsClient.rpc('conv.new', createBody);
1547
+ this.state.currentConversation = newConv;
1548
+ if (window.conversationManager) {
1549
+ window.conversationManager.loadConversations();
1550
+ window.conversationManager.select(newConv.id);
1551
+ }
1552
+ this.updateUrlForConversation(newConv.id);
1553
+ return this.streamToConversation(newConv.id, prompt, agentId, model);
1576
1554
  }
1577
- this.updateUrlForConversation(newConv.id);
1578
- return this.streamToConversation(newConv.id, prompt, agentId, model);
1555
+ throw e;
1579
1556
  }
1580
1557
 
1581
- if (!response.ok) throw new Error(`HTTP ${response.status}`);
1582
-
1583
- const result = await response.json();
1584
-
1585
1558
  if (result.queued) {
1586
1559
  console.log('Message queued, position:', result.queuePosition);
1587
1560
  return;
@@ -1607,26 +1580,8 @@ class AgentGUIClient {
1607
1580
  async fetchChunks(conversationId, since = 0) {
1608
1581
  if (!conversationId) return [];
1609
1582
 
1610
- if (this.chunkPollState.abortController) {
1611
- this.chunkPollState.abortController.abort();
1612
- }
1613
- this.chunkPollState.abortController = new AbortController();
1614
- const signal = this.chunkPollState.abortController.signal;
1615
-
1616
1583
  try {
1617
- const params = new URLSearchParams();
1618
- if (since > 0) {
1619
- params.append('since', since.toString());
1620
- }
1621
-
1622
- const url = `${window.__BASE_URL}/api/conversations/${conversationId}/chunks?${params.toString()}`;
1623
- const response = await fetch(url, { signal });
1624
-
1625
- if (!response.ok) {
1626
- throw new Error(`HTTP ${response.status}`);
1627
- }
1628
-
1629
- const data = await response.json();
1584
+ const data = await window.wsClient.rpc('conv.chunks', { id: conversationId, since: since > 0 ? since : 0 });
1630
1585
  if (!data.ok || !Array.isArray(data.chunks)) {
1631
1586
  throw new Error('Invalid chunks response');
1632
1587
  }
@@ -1663,9 +1618,8 @@ class AgentGUIClient {
1663
1618
 
1664
1619
  const checkSessionStatus = async () => {
1665
1620
  if (!this.state.currentSession?.id) return false;
1666
- const sessionResponse = await fetch(`${window.__BASE_URL}/api/sessions/${this.state.currentSession.id}`);
1667
- if (!sessionResponse.ok) return false;
1668
- const { session } = await sessionResponse.json();
1621
+ let session;
1622
+ try { ({ session } = await window.wsClient.rpc('sess.get', { id: this.state.currentSession.id })); } catch { return false; }
1669
1623
  if (session && (session.status === 'complete' || session.status === 'error')) {
1670
1624
  if (session.status === 'complete') {
1671
1625
  this.handleStreamingComplete({ sessionId: session.id, conversationId, timestamp: Date.now() });
@@ -1867,8 +1821,7 @@ class AgentGUIClient {
1867
1821
  async loadAgents() {
1868
1822
  return this._dedupedFetch('loadAgents', async () => {
1869
1823
  try {
1870
- const response = await fetch(window.__BASE_URL + '/api/agents');
1871
- const { agents } = await response.json();
1824
+ const { agents } = await window.wsClient.rpc('agent.ls');
1872
1825
  this.state.agents = agents;
1873
1826
 
1874
1827
  if (this.ui.agentSelector) {
@@ -1891,8 +1844,7 @@ class AgentGUIClient {
1891
1844
 
1892
1845
  async checkSpeechStatus() {
1893
1846
  try {
1894
- const response = await fetch(window.__BASE_URL + '/api/speech-status');
1895
- const status = await response.json();
1847
+ const status = await window.wsClient.rpc('speech.status');
1896
1848
  if (status.modelsComplete) {
1897
1849
  this._modelDownloadProgress = { done: true, complete: true };
1898
1850
  this._modelDownloadInProgress = false;
@@ -1918,8 +1870,7 @@ class AgentGUIClient {
1918
1870
  return;
1919
1871
  }
1920
1872
  try {
1921
- const response = await fetch(window.__BASE_URL + `/api/agents/${agentId}/models`);
1922
- const { models } = await response.json();
1873
+ const { models } = await window.wsClient.rpc('agent.models', { id: agentId });
1923
1874
  this._modelCache.set(agentId, models || []);
1924
1875
  this._populateModelSelector(models || []);
1925
1876
  } catch (error) {
@@ -1990,8 +1941,7 @@ class AgentGUIClient {
1990
1941
  async loadConversations() {
1991
1942
  return this._dedupedFetch('loadConversations', async () => {
1992
1943
  try {
1993
- const response = await fetch(window.__BASE_URL + '/api/conversations');
1994
- const { conversations } = await response.json();
1944
+ const { conversations } = await window.wsClient.rpc('conv.ls');
1995
1945
  this.state.conversations = conversations;
1996
1946
  return conversations;
1997
1947
  } catch (error) {
@@ -2180,17 +2130,7 @@ class AgentGUIClient {
2180
2130
  if (workingDirectory) body.workingDirectory = workingDirectory;
2181
2131
  if (model) body.model = model;
2182
2132
 
2183
- const response = await fetch(window.__BASE_URL + '/api/conversations', {
2184
- method: 'POST',
2185
- headers: { 'Content-Type': 'application/json' },
2186
- body: JSON.stringify(body)
2187
- });
2188
-
2189
- if (!response.ok) {
2190
- throw new Error(`Failed to create conversation: ${response.status}`);
2191
- }
2192
-
2193
- const { conversation } = await response.json();
2133
+ const { conversation } = await window.wsClient.rpc('conv.new', body);
2194
2134
 
2195
2135
  await this.loadConversations();
2196
2136
 
@@ -2282,20 +2222,22 @@ class AgentGUIClient {
2282
2222
 
2283
2223
  this._showSkeletonLoading(conversationId);
2284
2224
 
2285
- const resp = await fetch(window.__BASE_URL + `/api/conversations/${conversationId}/full`, { signal: convSignal });
2286
- if (resp.status === 404) {
2287
- console.warn('Conversation no longer exists:', conversationId);
2288
- this.state.currentConversation = null;
2289
- if (window.conversationManager) {
2290
- window.conversationManager.loadConversations();
2225
+ let fullData;
2226
+ try {
2227
+ fullData = await window.wsClient.rpc('conv.full', { id: conversationId });
2228
+ } catch (e) {
2229
+ if (e.code === 404) {
2230
+ console.warn('Conversation no longer exists:', conversationId);
2231
+ this.state.currentConversation = null;
2232
+ if (window.conversationManager) window.conversationManager.loadConversations();
2233
+ const outputEl = document.getElementById('output');
2234
+ if (outputEl) outputEl.innerHTML = '<p class="text-secondary" style="padding:2rem;text-align:center">Conversation not found. It may have been lost during a server restart.</p>';
2235
+ this.enableControls();
2236
+ return;
2291
2237
  }
2292
- const outputEl = document.getElementById('output');
2293
- if (outputEl) outputEl.innerHTML = '<p class="text-secondary" style="padding:2rem;text-align:center">Conversation not found. It may have been lost during a server restart.</p>';
2294
- this.enableControls();
2295
- return;
2238
+ throw e;
2296
2239
  }
2297
- if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
2298
- const { conversation, isActivelyStreaming, latestSession, chunks: rawChunks, totalChunks, messages: allMessages } = await resp.json();
2240
+ const { conversation, isActivelyStreaming, latestSession, chunks: rawChunks, totalChunks, messages: allMessages } = fullData;
2299
2241
 
2300
2242
  this.state.currentConversation = conversation;
2301
2243
  const hasActivity = (allMessages && allMessages.length > 0) || isActivelyStreaming || latestSession || this.state.streamingConversations.has(conversationId);
@@ -2337,11 +2279,9 @@ class AgentGUIClient {
2337
2279
  loadMoreBtn.disabled = true;
2338
2280
  loadMoreBtn.textContent = 'Loading...';
2339
2281
  try {
2340
- const fullResp = await fetch(window.__BASE_URL + `/api/conversations/${conversationId}/full?allChunks=1`);
2341
- if (fullResp.ok) {
2342
- this.invalidateCache(conversationId);
2343
- await this.loadConversationMessages(conversationId);
2344
- }
2282
+ await window.wsClient.rpc('conv.full', { id: conversationId, allChunks: true });
2283
+ this.invalidateCache(conversationId);
2284
+ await this.loadConversationMessages(conversationId);
2345
2285
  } catch (e) {
2346
2286
  loadMoreBtn.textContent = 'Failed to load. Try again.';
2347
2287
  loadMoreBtn.disabled = false;
@@ -1,5 +1,4 @@
1
1
  (function() {
2
- const BASE = window.__BASE_URL || '';
3
2
  let currentConversationId = null;
4
3
  let currentWorkingDirectory = null;
5
4
  let scriptState = { running: false, script: null, hasStart: false, hasDev: false };
@@ -59,25 +58,18 @@
59
58
 
60
59
  function fetchConversationAndCheckScripts() {
61
60
  if (!currentConversationId) return;
62
-
63
- fetch(BASE + '/api/conversations/' + currentConversationId)
64
- .then(function(r) { return r.json(); })
61
+ window.wsClient.rpc('conv.get', { id: currentConversationId })
65
62
  .then(function(data) {
66
63
  currentWorkingDirectory = data.conversation?.workingDirectory || null;
67
- if (currentWorkingDirectory) {
68
- showTerminalTab();
69
- }
64
+ if (currentWorkingDirectory) showTerminalTab();
70
65
  checkScripts();
71
66
  })
72
- .catch(function() {
73
- checkScripts();
74
- });
67
+ .catch(function() { checkScripts(); });
75
68
  }
76
69
 
77
70
  function checkScripts() {
78
71
  if (!currentConversationId) return;
79
- fetch(BASE + '/api/conversations/' + currentConversationId + '/scripts')
80
- .then(function(r) { return r.json(); })
72
+ window.wsClient.rpc('conv.scripts', { id: currentConversationId })
81
73
  .then(function(data) {
82
74
  scriptState.hasStart = data.hasStart;
83
75
  scriptState.hasDev = data.hasDev;
@@ -115,42 +107,29 @@
115
107
 
116
108
  function runScript(script) {
117
109
  if (!currentConversationId || scriptState.running) return;
118
- fetch(BASE + '/api/conversations/' + currentConversationId + '/run-script', {
119
- method: 'POST',
120
- headers: { 'Content-Type': 'application/json' },
121
- body: JSON.stringify({ script: script })
122
- })
123
- .then(function(r) { return r.json(); })
124
- .then(function(data) {
125
- if (data.ok) {
126
- scriptState.running = true;
127
- scriptState.script = script;
128
- hasTerminalContent = false;
129
- updateButtons();
130
- showTerminalTab();
131
- switchToTerminalView();
132
-
133
- var term = getTerminal();
134
- if (term) {
135
- term.clear();
136
- term.writeln('\x1b[36m[running npm run ' + script + ']\x1b[0m\r\n');
110
+ window.wsClient.rpc('conv.run-script', { id: currentConversationId, script: script })
111
+ .then(function(data) {
112
+ if (data.ok) {
113
+ scriptState.running = true;
114
+ scriptState.script = script;
115
+ hasTerminalContent = false;
116
+ updateButtons();
117
+ showTerminalTab();
118
+ switchToTerminalView();
119
+ var term = getTerminal();
120
+ if (term) {
121
+ term.clear();
122
+ term.writeln('\x1b[36m[running npm run ' + script + ']\x1b[0m\r\n');
123
+ }
137
124
  }
138
- }
139
- })
140
- .catch(function(err) {
141
- console.error('Failed to start script:', err);
142
- });
125
+ })
126
+ .catch(function(err) { console.error('Failed to start script:', err); });
143
127
  }
144
128
 
145
129
  function stopScript() {
146
130
  if (!currentConversationId) return;
147
- fetch(BASE + '/api/conversations/' + currentConversationId + '/stop-script', {
148
- method: 'POST',
149
- headers: { 'Content-Type': 'application/json' },
150
- body: '{}'
151
- }).catch(function(err) {
152
- console.error('Failed to stop script:', err);
153
- });
131
+ window.wsClient.rpc('conv.stop-script', { id: currentConversationId })
132
+ .catch(function(err) { console.error('Failed to stop script:', err); });
154
133
  }
155
134
 
156
135
  function showTerminalTab() {
@@ -78,3 +78,6 @@ class WsClient {
78
78
  }
79
79
 
80
80
  window.WsClient = WsClient;
81
+
82
+ window.wsManager = new WebSocketManager();
83
+ window.wsClient = new WsClient(window.wsManager);