agentgui 1.0.582 → 1.0.584

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/CLAUDE.md CHANGED
@@ -350,3 +350,39 @@ queries.updateToolStatus(toolId, { status: 'installed', version, installed_at: D
350
350
  - Database persists across page reload ✓
351
351
  - Frontend shows "Up-to-date" or "Update available" ✓
352
352
  - Tool install history records the action ✓
353
+
354
+ ---
355
+
356
+ ## ACP SDK Integration
357
+
358
+ ### Current Status
359
+ - **@agentclientprotocol/sdk** (`^0.4.1`) has been added to dependencies
360
+ - The SDK is positioned as the main protocol for client-server and server-ACP tools communication
361
+
362
+ ### Clear All Conversations Fix
363
+
364
+ **Issue:** After clicking "Clear All Conversations", the conversation threads would reappear in the sidebar.
365
+
366
+ **Root Cause:** The `all_conversations_deleted` broadcast event was being sent by the server (in `lib/ws-handlers-conv.js`), but:
367
+ 1. The event type was not in the `BROADCAST_TYPES` set in `server.js`, so it wasn't being broadcast to all clients
368
+ 2. The conversation manager (`static/js/conversations.js`) had no handler for this event type
369
+ 3. Client cleanup in `handleAllConversationsDeleted` was incomplete
370
+
371
+ **Solution Applied:**
372
+ 1. Added `'all_conversations_deleted'` to `BROADCAST_TYPES` set (server.js:4147)
373
+ 2. Added event handler in conversation manager to clear all local state (conversations.js:573-577)
374
+ 3. Enhanced client cleanup to clear all caches and state before reloading (client.js:1321-1330)
375
+
376
+ **Files Modified:**
377
+ - `server.js`: Added `all_conversations_deleted` to BROADCAST_TYPES
378
+ - `static/js/conversations.js`: Added handler for all_conversations_deleted event
379
+ - `static/js/client.js`: Enhanced handleAllConversationsDeleted with complete state cleanup
380
+
381
+ ### Next Steps for Full ACP SDK Integration
382
+ The ACP SDK dependency has been added. Full integration would involve:
383
+ 1. Replacing custom WebSocket protocol with ACP SDK's RPC/messaging layer
384
+ 2. Updating `lib/acp-manager.js` to use ACP SDK for ACP tool communication
385
+ 3. Migrating `lib/ws-protocol.js` handlers to use ACP SDK message types
386
+ 4. Updating client-side WebSocket handlers to work with ACP SDK events
387
+
388
+ This refactoring is optional and can be done incrementally as needed.
package/docs/index.html CHANGED
@@ -364,6 +364,31 @@
364
364
  </div>
365
365
  </section>
366
366
 
367
+ <!-- Why AgentGUI -->
368
+ <section class="section">
369
+ <div class="container">
370
+ <h2 class="section-title">Why AgentGUI?</h2>
371
+ <p class="section-subtitle">Stop juggling multiple terminal windows. Work with all your AI agents in one unified interface.</p>
372
+
373
+ <div class="features-grid">
374
+ <div class="feature-card">
375
+ <h3>📊 Compare Side-by-Side</h3>
376
+ <p>Test the same prompt across Claude Code, Gemini CLI, OpenCode, and others to find the best approach for your task.</p>
377
+ </div>
378
+
379
+ <div class="feature-card">
380
+ <h3>💾 Never Lose Context</h3>
381
+ <p>Every conversation, file change, and terminal output is automatically saved. Resume interrupted work exactly where you left off.</p>
382
+ </div>
383
+
384
+ <div class="feature-card">
385
+ <h3>👁️ See Everything</h3>
386
+ <p>Watch streaming responses, file changes, and tool calls in real-time instead of parsing raw JSON in your terminal.</p>
387
+ </div>
388
+ </div>
389
+ </div>
390
+ </section>
391
+
367
392
  <!-- Features -->
368
393
  <section class="section" style="background: #f9fafb;">
369
394
  <div class="container">
@@ -450,6 +475,46 @@
450
475
  </div>
451
476
  </section>
452
477
 
478
+ <!-- Use Cases -->
479
+ <section class="section">
480
+ <div class="container">
481
+ <h2 class="section-title">Use Cases</h2>
482
+ <p class="section-subtitle">Practical scenarios where AgentGUI shines</p>
483
+
484
+ <div class="features-grid">
485
+ <div class="feature-card">
486
+ <h3>Multi-Agent Comparison</h3>
487
+ <p>Run the same task through different agents to compare approaches, code quality, and execution speed.</p>
488
+ </div>
489
+
490
+ <div class="feature-card">
491
+ <h3>Long-Running Projects</h3>
492
+ <p>Build complex features across multiple sessions without losing context or conversation history.</p>
493
+ </div>
494
+
495
+ <div class="feature-card">
496
+ <h3>Team Collaboration</h3>
497
+ <p>Share conversation URLs and working directories for pair programming with AI agents.</p>
498
+ </div>
499
+
500
+ <div class="feature-card">
501
+ <h3>Agent Development</h3>
502
+ <p>Test and debug custom agents with full visibility into streaming events and tool calls.</p>
503
+ </div>
504
+
505
+ <div class="feature-card">
506
+ <h3>Offline Speech</h3>
507
+ <p>Use local speech-to-text and text-to-speech without API costs or internet dependency.</p>
508
+ </div>
509
+
510
+ <div class="feature-card">
511
+ <h3>Code Review Sessions</h3>
512
+ <p>Review AI-generated changes in a visual diff view with file browser integration.</p>
513
+ </div>
514
+ </div>
515
+ </div>
516
+ </section>
517
+
453
518
  <!-- Architecture -->
454
519
  <section class="section" style="background: #f9fafb;">
455
520
  <div class="container">
@@ -1,164 +1,225 @@
1
- import { startProcess as startProc, scheduleRestart as scheduleRestart, MAX_RESTARTS, RESTART_WINDOW_MS, IDLE_TIMEOUT_MS } from './acp-process-lifecycle.js';
2
-
3
- const ACP_TOOLS = [
4
- { id: 'opencode', cmd: 'opencode', args: ['acp'], port: 18100, npxPkg: 'opencode-ai' },
5
- { id: 'kilo', cmd: 'kilo', args: ['acp'], port: 18101, npxPkg: '@kilocode/cli' },
6
- ];
7
-
8
- const HEALTH_INTERVAL_MS = 30000;
9
- const STARTUP_GRACE_MS = 5000;
10
- const processes = new Map();
11
- let healthTimer = null;
12
- let shuttingDown = false;
13
-
14
- function log(msg) { console.log('[ACP] ' + msg); }
15
-
16
- function startProcess(tool) {
17
- if (shuttingDown) return null;
18
- const existing = processes.get(tool.id);
19
- if (existing?.process && !existing.process.killed) return existing;
20
-
21
- const entry = startProc(tool, log);
22
- if (!entry) return null;
23
-
24
- entry.process.on('close', (code) => {
25
- entry.healthy = false;
26
- if (shuttingDown || entry._stopping) return;
27
- log(tool.id + ' exited code ' + code);
28
- scheduleRestart(tool, entry.restarts, log, startProcess, () => shuttingDown);
29
- });
30
-
31
- processes.set(tool.id, entry);
32
- log(tool.id + ' started port ' + tool.port + ' pid ' + entry.process.pid);
33
- setTimeout(() => checkHealth(tool.id), STARTUP_GRACE_MS);
34
- resetIdleTimer(tool.id);
35
- return entry;
36
- }
37
-
38
- function resetIdleTimer(toolId) {
39
- const entry = processes.get(toolId);
40
- if (!entry) return;
41
- entry.lastUsed = Date.now();
42
- if (entry.idleTimer) clearTimeout(entry.idleTimer);
43
- entry.idleTimer = setTimeout(() => stopTool(toolId), IDLE_TIMEOUT_MS);
44
- }
45
-
46
- function stopTool(toolId) {
47
- const entry = processes.get(toolId);
48
- if (!entry) return;
49
- log(toolId + ' idle, stopping to free RAM');
50
- entry._stopping = true;
51
- if (entry.idleTimer) clearTimeout(entry.idleTimer);
52
- try { entry.process.kill('SIGTERM'); } catch (_) {}
53
- setTimeout(() => { try { entry.process.kill('SIGKILL'); } catch (_) {} }, 5000);
54
- processes.delete(toolId);
55
- }
56
-
57
- async function checkHealth(toolId) {
58
- const entry = processes.get(toolId);
59
- if (!entry || shuttingDown) return;
60
-
61
- const { fetchACPProvider } = await import('./acp-http-client.js');
62
- const result = await fetchACPProvider('http://127.0.0.1', entry.port);
63
-
64
- entry.healthy = result.ok;
65
- entry.lastHealthCheck = Date.now();
66
-
67
- if (result.data) {
68
- entry.providerInfo = result.data;
69
- }
70
- }
71
-
72
- export async function ensureRunning(agentId) {
73
- const tool = ACP_TOOLS.find(t => t.id === agentId);
74
- if (!tool) return null;
75
- let entry = processes.get(agentId);
76
- if (entry?.healthy) { resetIdleTimer(agentId); return entry.port; }
77
- if (!entry || entry._stopping) {
78
- entry = startProcess(tool);
79
- if (!entry) return null;
80
- }
81
- for (let i = 0; i < 20; i++) {
82
- await new Promise(r => setTimeout(r, 500));
83
- await checkHealth(agentId);
84
- if (processes.get(agentId)?.healthy) { resetIdleTimer(agentId); return tool.port; }
85
- }
86
- return null;
87
- }
88
-
89
- export function touch(agentId) {
90
- const entry = processes.get(agentId);
91
- if (entry) resetIdleTimer(agentId);
92
- }
93
-
94
- export async function startAll() {
95
- log('ACP tools available (on-demand start)');
96
- healthTimer = setInterval(() => {
97
- for (const [id] of processes) checkHealth(id);
98
- }, HEALTH_INTERVAL_MS);
99
- }
100
-
101
- export async function stopAll() {
102
- shuttingDown = true;
103
- if (healthTimer) { clearInterval(healthTimer); healthTimer = null; }
104
- const kills = [];
105
- for (const [id, entry] of processes) {
106
- if (entry.idleTimer) clearTimeout(entry.idleTimer);
107
- log('stopping ' + id + ' pid ' + entry.pid);
108
- kills.push(new Promise(resolve => {
109
- const t = setTimeout(() => { try { entry.process.kill('SIGKILL'); } catch (_) {} resolve(); }, 5000);
110
- entry.process.on('close', () => { clearTimeout(t); resolve(); });
111
- try { entry.process.kill('SIGTERM'); } catch (_) {}
112
- }));
113
- }
114
- await Promise.all(kills);
115
- processes.clear();
116
- log('all stopped');
117
- }
118
-
119
- export function getStatus() {
120
- return ACP_TOOLS.map(tool => {
121
- const e = processes.get(tool.id);
122
- return {
123
- id: tool.id, port: tool.port, running: !!e, healthy: e?.healthy || false,
124
- pid: e?.pid, uptime: e ? Date.now() - e.startedAt : 0,
125
- restartCount: e?.restarts.length || 0, idleMs: e ? Date.now() - e.lastUsed : 0,
126
- providerInfo: e?.providerInfo || null,
127
- };
128
- });
129
- }
130
-
131
- export function getPort(agentId) {
132
- const e = processes.get(agentId);
133
- return e?.healthy ? e.port : null;
134
- }
135
-
136
- export function getRunningPorts() {
137
- const ports = {};
138
- for (const [id, e] of processes) if (e.healthy) ports[id] = e.port;
139
- return ports;
140
- }
141
-
142
- export async function restart(agentId) {
143
- const tool = ACP_TOOLS.find(t => t.id === agentId);
144
- if (!tool) return false;
145
- stopTool(agentId);
146
- startProcess(tool);
147
- return true;
148
- }
149
-
150
- export async function queryModels(agentId) {
151
- const port = await ensureRunning(agentId);
152
- if (!port) return [];
153
- try {
154
- const res = await fetch('http://127.0.0.1:' + port + '/models');
155
- if (!res.ok) return [];
156
- const data = await res.json();
157
- return data.models || [];
158
- } catch (_) { return []; }
159
- }
160
-
161
- export function isAvailable(agentId) {
162
- const tool = ACP_TOOLS.find(t => t.id === agentId);
163
- return !!tool;
164
- }
1
+ import { spawn } from 'child_process';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import fs from 'fs';
5
+ import { fileURLToPath } from 'url';
6
+
7
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
+ const projectRoot = path.resolve(__dirname, '..');
9
+ const isWindows = os.platform() === 'win32';
10
+
11
+ const ACP_TOOLS = [
12
+ { id: 'opencode', cmd: 'opencode', args: ['acp'], port: 18100, npxPkg: 'opencode-ai' },
13
+ { id: 'kilo', cmd: 'kilo', args: ['acp'], port: 18101, npxPkg: '@kilocode/cli' },
14
+ ];
15
+
16
+ const HEALTH_INTERVAL_MS = 30000;
17
+ const STARTUP_GRACE_MS = 5000;
18
+ const MAX_RESTARTS = 10;
19
+ const RESTART_WINDOW_MS = 300000;
20
+ const IDLE_TIMEOUT_MS = 120000;
21
+
22
+ const processes = new Map();
23
+ let healthTimer = null;
24
+ let shuttingDown = false;
25
+
26
+ function log(msg) { console.log('[ACP-SDK] ' + msg); }
27
+
28
+ function resolveBinary(cmd) {
29
+ const ext = isWindows ? '.cmd' : '';
30
+ const localBin = path.join(projectRoot, 'node_modules', '.bin', cmd + ext);
31
+ if (fs.existsSync(localBin)) return localBin;
32
+ return cmd;
33
+ }
34
+
35
+ function startProcess(tool) {
36
+ if (shuttingDown) return null;
37
+ const existing = processes.get(tool.id);
38
+ if (existing?.process && !existing.process.killed) return existing;
39
+
40
+ const cmd = resolveBinary(tool.cmd);
41
+ const entry = {
42
+ id: tool.id,
43
+ port: tool.port,
44
+ startedAt: Date.now(),
45
+ lastUsed: Date.now(),
46
+ lastHealthCheck: 0,
47
+ healthy: false,
48
+ process: null,
49
+ pid: null,
50
+ restarts: [],
51
+ idleTimer: null,
52
+ providerInfo: null,
53
+ _stopping: false
54
+ };
55
+
56
+ try {
57
+ entry.process = spawn(cmd, tool.args, {
58
+ stdio: ['ignore', 'pipe', 'pipe'],
59
+ detached: false
60
+ });
61
+ entry.pid = entry.process.pid;
62
+
63
+ entry.process.on('close', (code) => {
64
+ entry.healthy = false;
65
+ if (shuttingDown || entry._stopping) return;
66
+ log(tool.id + ' exited code ' + code);
67
+ const window = Date.now() - RESTART_WINDOW_MS;
68
+ entry.restarts = entry.restarts.filter(t => t > window);
69
+ if (entry.restarts.length < MAX_RESTARTS) {
70
+ const delay = Math.min(1000 * Math.pow(2, entry.restarts.length), 30000);
71
+ entry.restarts.push(Date.now());
72
+ setTimeout(() => startProcess(tool), delay);
73
+ } else {
74
+ log(tool.id + ' max restarts reached');
75
+ }
76
+ });
77
+
78
+ processes.set(tool.id, entry);
79
+ log(tool.id + ' started port ' + tool.port + ' pid ' + entry.pid);
80
+ setTimeout(() => checkHealth(tool.id), STARTUP_GRACE_MS);
81
+ resetIdleTimer(tool.id);
82
+ } catch (err) {
83
+ log(tool.id + ' spawn failed: ' + err.message);
84
+ }
85
+
86
+ return entry;
87
+ }
88
+
89
+ function resetIdleTimer(toolId) {
90
+ const entry = processes.get(toolId);
91
+ if (!entry) return;
92
+ entry.lastUsed = Date.now();
93
+ if (entry.idleTimer) clearTimeout(entry.idleTimer);
94
+ entry.idleTimer = setTimeout(() => stopTool(toolId), IDLE_TIMEOUT_MS);
95
+ }
96
+
97
+ function stopTool(toolId) {
98
+ const entry = processes.get(toolId);
99
+ if (!entry) return;
100
+ log(toolId + ' idle, stopping to free RAM');
101
+ entry._stopping = true;
102
+ if (entry.idleTimer) clearTimeout(entry.idleTimer);
103
+ try { entry.process.kill('SIGTERM'); } catch (_) {}
104
+ setTimeout(() => { try { entry.process.kill('SIGKILL'); } catch (_) {} }, 5000);
105
+ processes.delete(toolId);
106
+ }
107
+
108
+ async function checkHealth(toolId) {
109
+ const entry = processes.get(toolId);
110
+ if (!entry || shuttingDown) return;
111
+
112
+ try {
113
+ const res = await fetch('http://127.0.0.1:' + entry.port + '/provider', {
114
+ signal: AbortSignal.timeout(3000)
115
+ });
116
+ entry.healthy = res.ok;
117
+ if (res.ok) {
118
+ entry.providerInfo = await res.json();
119
+ }
120
+ } catch (_) {
121
+ entry.healthy = false;
122
+ }
123
+ entry.lastHealthCheck = Date.now();
124
+ }
125
+
126
+ export async function ensureRunning(agentId) {
127
+ const tool = ACP_TOOLS.find(t => t.id === agentId);
128
+ if (!tool) return null;
129
+ let entry = processes.get(agentId);
130
+ if (entry?.healthy) { resetIdleTimer(agentId); return entry.port; }
131
+ if (!entry || entry._stopping) {
132
+ entry = startProcess(tool);
133
+ if (!entry) return null;
134
+ }
135
+ for (let i = 0; i < 20; i++) {
136
+ await new Promise(r => setTimeout(r, 500));
137
+ await checkHealth(agentId);
138
+ if (processes.get(agentId)?.healthy) { resetIdleTimer(agentId); return tool.port; }
139
+ }
140
+ return null;
141
+ }
142
+
143
+ export function touch(agentId) {
144
+ const entry = processes.get(agentId);
145
+ if (entry) resetIdleTimer(agentId);
146
+ }
147
+
148
+ export async function startAll() {
149
+ log('ACP tools available (on-demand start)');
150
+ healthTimer = setInterval(() => {
151
+ for (const [id] of processes) checkHealth(id);
152
+ }, HEALTH_INTERVAL_MS);
153
+ }
154
+
155
+ export async function stopAll() {
156
+ shuttingDown = true;
157
+ if (healthTimer) { clearInterval(healthTimer); healthTimer = null; }
158
+ const kills = [];
159
+ for (const [id, entry] of processes) {
160
+ if (entry.idleTimer) clearTimeout(entry.idleTimer);
161
+ log('stopping ' + id + ' pid ' + entry.pid);
162
+ kills.push(new Promise(resolve => {
163
+ const t = setTimeout(() => { try { entry.process.kill('SIGKILL'); } catch (_) {} resolve(); }, 5000);
164
+ entry.process.on('close', () => { clearTimeout(t); resolve(); });
165
+ try { entry.process.kill('SIGTERM'); } catch (_) {}
166
+ }));
167
+ }
168
+ await Promise.all(kills);
169
+ processes.clear();
170
+ log('all stopped');
171
+ }
172
+
173
+ export function getStatus() {
174
+ return ACP_TOOLS.map(tool => {
175
+ const e = processes.get(tool.id);
176
+ return {
177
+ id: tool.id,
178
+ port: tool.port,
179
+ running: !!e,
180
+ healthy: e?.healthy || false,
181
+ pid: e?.pid,
182
+ uptime: e ? Date.now() - e.startedAt : 0,
183
+ restartCount: e?.restarts.length || 0,
184
+ idleMs: e ? Date.now() - e.lastUsed : 0,
185
+ providerInfo: e?.providerInfo || null,
186
+ };
187
+ });
188
+ }
189
+
190
+ export function getPort(agentId) {
191
+ const e = processes.get(agentId);
192
+ return e?.healthy ? e.port : null;
193
+ }
194
+
195
+ export function getRunningPorts() {
196
+ const ports = {};
197
+ for (const [id, e] of processes) if (e.healthy) ports[id] = e.port;
198
+ return ports;
199
+ }
200
+
201
+ export async function restart(agentId) {
202
+ const tool = ACP_TOOLS.find(t => t.id === agentId);
203
+ if (!tool) return false;
204
+ stopTool(agentId);
205
+ startProcess(tool);
206
+ return true;
207
+ }
208
+
209
+ export async function queryModels(agentId) {
210
+ const port = await ensureRunning(agentId);
211
+ if (!port) return [];
212
+ try {
213
+ const res = await fetch('http://127.0.0.1:' + port + '/models', {
214
+ signal: AbortSignal.timeout(3000)
215
+ });
216
+ if (!res.ok) return [];
217
+ const data = await res.json();
218
+ return data.models || [];
219
+ } catch (_) { return []; }
220
+ }
221
+
222
+ export function isAvailable(agentId) {
223
+ const tool = ACP_TOOLS.find(t => t.id === agentId);
224
+ return !!tool;
225
+ }
@@ -2,7 +2,7 @@ import fs from 'fs';
2
2
  import os from 'os';
3
3
  import path from 'path';
4
4
  import { execSync, spawn } from 'child_process';
5
- import { ensureRunning, touch, queryModels } from './acp-manager.js';
5
+ import { ensureRunning, touch, queryModels } from './acp-sdk-manager.js';
6
6
 
7
7
  function spawnScript(cmd, args, convId, scriptName, agentId, deps) {
8
8
  const { activeScripts, broadcastSync, modelCache } = deps;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.582",
3
+ "version": "1.0.584",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "server.js",
@@ -21,6 +21,7 @@
21
21
  "postinstall": "node scripts/patch-fsbrowse.js"
22
22
  },
23
23
  "dependencies": {
24
+ "@agentclientprotocol/sdk": "^0.4.1",
24
25
  "@anthropic-ai/claude-code": "^2.1.37",
25
26
  "@google/gemini-cli": "latest",
26
27
  "@huggingface/transformers": "^3.8.1",
package/server.js CHANGED
@@ -22,7 +22,7 @@ import { register as registerConvHandlers } from './lib/ws-handlers-conv.js';
22
22
  import { register as registerSessionHandlers } from './lib/ws-handlers-session.js';
23
23
  import { register as registerRunHandlers } from './lib/ws-handlers-run.js';
24
24
  import { register as registerUtilHandlers } from './lib/ws-handlers-util.js';
25
- import { startAll as startACPTools, stopAll as stopACPTools, getStatus as getACPStatus, getPort as getACPPort, ensureRunning, queryModels as queryACPModels, touch as touchACP } from './lib/acp-manager.js';
25
+ import { startAll as startACPTools, stopAll as stopACPTools, getStatus as getACPStatus, getPort as getACPPort, ensureRunning, queryModels as queryACPModels, touch as touchACP } from './lib/acp-sdk-manager.js';
26
26
  import { installGMAgentConfigs } from './lib/gm-agent-configs.js';
27
27
  import * as toolManager from './lib/tool-manager.js';
28
28
  import { pm2Manager } from './lib/pm2-manager.js';
@@ -398,27 +398,54 @@ function findCommand(cmd) {
398
398
  }
399
399
 
400
400
  async function queryACPServerAgents(baseUrl) {
401
- const { fetchACPAgents, extractCompleteAgentData } = await import('./lib/acp-http-client.js');
402
-
403
- const result = await fetchACPAgents(baseUrl);
404
-
405
- if (!result.ok) {
406
- console.error(`Failed to query ACP agents from ${baseUrl}: ${result.status} ${result.error || ''}`);
407
- return [];
408
- }
409
-
410
- if (!result.data?.agents || !Array.isArray(result.data.agents)) {
411
- console.error(`Invalid agents response from ${baseUrl}`);
401
+ const endpoint = baseUrl.endsWith('/') ? baseUrl + 'agents/search' : baseUrl + '/agents/search';
402
+ try {
403
+ const response = await fetch(endpoint, {
404
+ method: 'POST',
405
+ headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
406
+ body: JSON.stringify({}),
407
+ signal: AbortSignal.timeout(5000)
408
+ });
409
+ if (!response.ok) {
410
+ console.error(`Failed to query ACP agents from ${baseUrl}: ${response.status}`);
411
+ return [];
412
+ }
413
+ const data = await response.json();
414
+ if (!data?.agents || !Array.isArray(data.agents)) {
415
+ console.error(`Invalid agents response from ${baseUrl}`);
416
+ return [];
417
+ }
418
+ return data.agents.map(agent => ({
419
+ id: agent.agent_id || agent.id,
420
+ name: agent.metadata?.ref?.name || agent.name || 'Unknown Agent',
421
+ metadata: {
422
+ ref: {
423
+ name: agent.metadata?.ref?.name,
424
+ version: agent.metadata?.ref?.version,
425
+ url: agent.metadata?.ref?.url,
426
+ tags: agent.metadata?.ref?.tags
427
+ },
428
+ description: agent.metadata?.description,
429
+ author: agent.metadata?.author,
430
+ license: agent.metadata?.license
431
+ },
432
+ specs: agent.specs ? {
433
+ capabilities: agent.specs.capabilities,
434
+ input_schema: agent.specs.input_schema || agent.specs.input,
435
+ output_schema: agent.specs.output_schema || agent.specs.output,
436
+ thread_state_schema: agent.specs.thread_state_schema || agent.specs.thread_state,
437
+ config_schema: agent.specs.config_schema || agent.specs.config,
438
+ custom_streaming_update_schema: agent.specs.custom_streaming_update_schema || agent.specs.custom_streaming_update
439
+ } : null,
440
+ custom_data: agent.custom_data,
441
+ icon: agent.metadata?.ref?.name?.charAt(0) || 'A',
442
+ protocol: 'acp',
443
+ path: baseUrl
444
+ }));
445
+ } catch (error) {
446
+ console.error(`ACP agents query failed for ${baseUrl}: ${error.message}`);
412
447
  return [];
413
448
  }
414
-
415
- return result.data.agents.map(agent => {
416
- const complete = extractCompleteAgentData(agent);
417
- return {
418
- ...complete,
419
- path: baseUrl
420
- };
421
- });
422
449
  }
423
450
 
424
451
  function discoverAgents() {
@@ -4117,7 +4144,7 @@ wss.on('connection', (ws, req) => {
4117
4144
 
4118
4145
  const BROADCAST_TYPES = new Set([
4119
4146
  'message_created', 'conversation_created', 'conversation_updated',
4120
- 'conversations_updated', 'conversation_deleted', 'queue_status', 'queue_updated',
4147
+ 'conversations_updated', 'conversation_deleted', 'all_conversations_deleted', 'queue_status', 'queue_updated',
4121
4148
  'rate_limit_hit', 'rate_limit_clear',
4122
4149
  'script_started', 'script_stopped', 'script_output',
4123
4150
  'model_download_progress', 'stt_progress', 'tts_setup_progress', 'voice_list',
@@ -1317,12 +1317,19 @@ class AgentGUIClient {
1317
1317
  this.enableControls();
1318
1318
  }
1319
1319
 
1320
- handleAllConversationsDeleted(data) {
1320
+ async handleAllConversationsDeleted(data) {
1321
1321
  this.state.currentConversation = null;
1322
+ this.state.conversations = [];
1323
+ this.state.sessionEvents = [];
1324
+ this.conversationCache.clear();
1325
+ this.conversationListCache = { data: [], timestamp: 0, ttl: 30000 };
1326
+ this.draftPrompts.clear();
1322
1327
  window.dispatchEvent(new CustomEvent('conversation-deselected'));
1323
1328
  if (window.conversationManager) {
1324
- window.conversationManager.loadConversations();
1329
+ this.state.currentConversation = null;
1330
+ await window.conversationManager.loadConversations();
1325
1331
  }
1332
+ this.clearOutput();
1326
1333
  }
1327
1334
 
1328
1335
  isHtmlContent(text) {
@@ -568,6 +568,11 @@ class ConversationManager {
568
568
  this.updateConversation(msg.conversation.id, msg.conversation);
569
569
  } else if (msg.type === 'conversation_deleted') {
570
570
  this.deleteConversation(msg.conversationId);
571
+ } else if (msg.type === 'all_conversations_deleted') {
572
+ this.conversations = [];
573
+ this.activeId = null;
574
+ this.streamingConversations.clear();
575
+ this.showEmpty('No conversations yet');
571
576
  } else if (msg.type === 'streaming_start' && msg.conversationId) {
572
577
  this.streamingConversations.add(msg.conversationId);
573
578
  this.render();
@@ -1,125 +0,0 @@
1
- /**
2
- * ACP HTTP Client with comprehensive request/response logging
3
- */
4
-
5
- function logACPCall(method, url, requestData, responseData, error = null) {
6
- const timestamp = new Date().toISOString();
7
- const logEntry = {
8
- timestamp,
9
- method,
10
- url,
11
- request: requestData,
12
- response: responseData,
13
- error: error ? error.message : null
14
- };
15
-
16
- console.log('[ACP-HTTP]', JSON.stringify(logEntry, null, 2));
17
- return logEntry;
18
- }
19
-
20
- export async function fetchACPProvider(baseUrl, port) {
21
- const url = baseUrl + ':' + port + '/provider';
22
- const startTime = Date.now();
23
-
24
- try {
25
- console.log('[ACP-HTTP] → GET ' + url);
26
-
27
- const response = await fetch(url, {
28
- method: 'GET',
29
- headers: { 'Accept': 'application/json' },
30
- signal: AbortSignal.timeout(3000)
31
- });
32
-
33
- const data = response.ok ? await response.json() : null;
34
- const duration = Date.now() - startTime;
35
-
36
- logACPCall('GET', url, {
37
- headers: { 'Accept': 'application/json' },
38
- timeout: 3000
39
- }, {
40
- status: response.status,
41
- statusText: response.statusText,
42
- headers: Object.fromEntries(response.headers.entries()),
43
- body: data,
44
- duration_ms: duration
45
- });
46
-
47
- return { ok: response.ok, status: response.status, data };
48
- } catch (error) {
49
- logACPCall('GET', url, { headers: { 'Accept': 'application/json' } }, null, error);
50
- return { ok: false, status: 0, data: null, error: error.message };
51
- }
52
- }
53
-
54
- export async function fetchACPAgents(baseUrl) {
55
- const endpoint = baseUrl.endsWith('/') ? baseUrl + 'agents/search' : baseUrl + '/agents/search';
56
- const requestBody = {};
57
- const startTime = Date.now();
58
-
59
- try {
60
- console.log('[ACP-HTTP] → POST ' + endpoint);
61
- console.log('[ACP-HTTP] Request body: ' + JSON.stringify(requestBody));
62
-
63
- const response = await fetch(endpoint, {
64
- method: 'POST',
65
- headers: {
66
- 'Content-Type': 'application/json',
67
- 'Accept': 'application/json'
68
- },
69
- body: JSON.stringify(requestBody),
70
- signal: AbortSignal.timeout(5000)
71
- });
72
-
73
- const data = response.ok ? await response.json() : null;
74
- const duration = Date.now() - startTime;
75
-
76
- logACPCall('POST', endpoint, {
77
- headers: {
78
- 'Content-Type': 'application/json',
79
- 'Accept': 'application/json'
80
- },
81
- body: requestBody,
82
- timeout: 5000
83
- }, {
84
- status: response.status,
85
- statusText: response.statusText,
86
- headers: Object.fromEntries(response.headers.entries()),
87
- body: data,
88
- duration_ms: duration
89
- });
90
-
91
- return { ok: response.ok, status: response.status, data };
92
- } catch (error) {
93
- logACPCall('POST', endpoint, { body: requestBody }, null, error);
94
- return { ok: false, status: 0, data: null, error: error.message };
95
- }
96
- }
97
-
98
- export function extractCompleteAgentData(agent) {
99
- return {
100
- id: agent.agent_id || agent.id,
101
- name: agent.metadata?.ref?.name || agent.name || 'Unknown Agent',
102
- metadata: {
103
- ref: {
104
- name: agent.metadata?.ref?.name,
105
- version: agent.metadata?.ref?.version,
106
- url: agent.metadata?.ref?.url,
107
- tags: agent.metadata?.ref?.tags
108
- },
109
- description: agent.metadata?.description,
110
- author: agent.metadata?.author,
111
- license: agent.metadata?.license
112
- },
113
- specs: agent.specs ? {
114
- capabilities: agent.specs.capabilities,
115
- input_schema: agent.specs.input_schema || agent.specs.input,
116
- output_schema: agent.specs.output_schema || agent.specs.output,
117
- thread_state_schema: agent.specs.thread_state_schema || agent.specs.thread_state,
118
- config_schema: agent.specs.config_schema || agent.specs.config,
119
- custom_streaming_update_schema: agent.specs.custom_streaming_update_schema || agent.specs.custom_streaming_update
120
- } : null,
121
- custom_data: agent.custom_data,
122
- icon: agent.metadata?.ref?.name?.charAt(0) || 'A',
123
- protocol: 'acp'
124
- };
125
- }
@@ -1,65 +0,0 @@
1
- import { spawn } from 'child_process';
2
- import path from 'path';
3
- import os from 'os';
4
- import fs from 'fs';
5
- import { fileURLToPath } from 'url';
6
-
7
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
- const projectRoot = path.resolve(__dirname, '..');
9
- const isWindows = os.platform() === 'win32';
10
-
11
- export const MAX_RESTARTS = 10;
12
- export const RESTART_WINDOW_MS = 300000;
13
- export const IDLE_TIMEOUT_MS = 120000;
14
-
15
- export function resolveBinary(cmd) {
16
- const ext = isWindows ? '.cmd' : '';
17
- const localBin = path.join(projectRoot, 'node_modules', '.bin', cmd + ext);
18
- if (fs.existsSync(localBin)) return localBin;
19
- return cmd;
20
- }
21
-
22
- export function startProcess(tool, log) {
23
- const bin = resolveBinary(tool.cmd);
24
- const args = [...tool.args, '--port', String(tool.port)];
25
- const opts = { stdio: ['pipe', 'pipe', 'pipe'], cwd: process.cwd() };
26
- if (isWindows) opts.shell = true;
27
-
28
- let proc;
29
- try { proc = spawn(bin, args, opts); }
30
- catch (err) { log(tool.id + ' spawn failed: ' + err.message); return null; }
31
-
32
- const entry = {
33
- id: tool.id, port: tool.port, process: proc, pid: proc.pid,
34
- startedAt: Date.now(), restarts: [], healthy: false, lastHealthCheck: 0,
35
- lastUsed: Date.now(), idleTimer: null,
36
- };
37
-
38
- proc.stdout.on('data', () => {});
39
- proc.stderr.on('data', (d) => {
40
- const t = d.toString().trim();
41
- if (t) log(tool.id + ': ' + t.substring(0, 200));
42
- });
43
- proc.stdout.on('error', () => {});
44
- proc.stderr.on('error', () => {});
45
- proc.on('error', (err) => { log(tool.id + ' error: ' + err.message); entry.healthy = false; });
46
-
47
- return entry;
48
- }
49
-
50
- export function scheduleRestart(tool, prevRestarts, log, startProcessFn, shuttingDown) {
51
- if (shuttingDown()) return;
52
- const now = Date.now();
53
- const recent = prevRestarts.filter(t => now - t < RESTART_WINDOW_MS);
54
- if (recent.length >= MAX_RESTARTS) {
55
- log(tool.id + ' exceeded restart limit, giving up');
56
- return null;
57
- }
58
- const delay = Math.min(1000 * Math.pow(2, recent.length), 30000);
59
- log(tool.id + ' restarting in ' + delay + 'ms');
60
- setTimeout(() => {
61
- if (shuttingDown()) return;
62
- const entry = startProcessFn(tool);
63
- if (entry) entry.restarts = [...recent, Date.now()];
64
- }, delay);
65
- }