agentgui 1.0.583 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.583",
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
@@ -4144,7 +4144,7 @@ wss.on('connection', (ws, req) => {
4144
4144
 
4145
4145
  const BROADCAST_TYPES = new Set([
4146
4146
  'message_created', 'conversation_created', 'conversation_updated',
4147
- 'conversations_updated', 'conversation_deleted', 'queue_status', 'queue_updated',
4147
+ 'conversations_updated', 'conversation_deleted', 'all_conversations_deleted', 'queue_status', 'queue_updated',
4148
4148
  'rate_limit_hit', 'rate_limit_clear',
4149
4149
  'script_started', 'script_stopped', 'script_output',
4150
4150
  'model_download_progress', 'stt_progress', 'tts_setup_progress', 'voice_list',
@@ -1319,10 +1319,17 @@ class AgentGUIClient {
1319
1319
 
1320
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) {
1329
+ this.state.currentConversation = null;
1324
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,164 +0,0 @@
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,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
- }