agentgui 1.0.582 → 1.0.583

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/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">
@@ -0,0 +1,225 @@
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.583",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "server.js",
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() {
@@ -1317,11 +1317,11 @@ class AgentGUIClient {
1317
1317
  this.enableControls();
1318
1318
  }
1319
1319
 
1320
- handleAllConversationsDeleted(data) {
1320
+ async handleAllConversationsDeleted(data) {
1321
1321
  this.state.currentConversation = null;
1322
1322
  window.dispatchEvent(new CustomEvent('conversation-deselected'));
1323
1323
  if (window.conversationManager) {
1324
- window.conversationManager.loadConversations();
1324
+ await window.conversationManager.loadConversations();
1325
1325
  }
1326
1326
  }
1327
1327