agentgui 1.0.727 → 1.0.729

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
@@ -24,6 +24,7 @@ lib/agent-descriptors.js Data-driven ACP agent descriptor builder
24
24
  lib/acp-sdk-manager.js ACP tool lifecycle - on-demand start opencode/kilo/codex, health checks, idle timeout
25
25
  lib/acp-server-machine.js XState v5 machine per ACP tool: stopped/starting/running/crashed/restarting states
26
26
  lib/execution-machine.js XState v5 machine per conversation: idle/streaming/draining/rate_limited states
27
+ lib/tool-install-machine.js XState v5 machine per tool: unchecked/checking/idle/installing/installed/updating/needs_update/failed states
27
28
  lib/ws-protocol.js WebSocket RPC router (WsRouter class)
28
29
  lib/ws-optimizer.js Per-client priority queue for WS event batching
29
30
  lib/ws-handlers-conv.js Conversation CRUD, chunks, cancel, steer, inject RPC handlers
@@ -53,28 +54,38 @@ static/js/ui-components.js UI component helpers
53
54
  static/js/syntax-highlighter.js Code syntax highlighting
54
55
  static/js/voice.js Voice input/output
55
56
  static/js/features.js View toggle, drag-drop upload, model progress indicator
56
- static/js/tools-manager.js Tool install/update UI
57
+ static/js/tools-manager.js Tool install/update UI orchestrator
58
+ static/js/tools-manager-ui.js Tool card rendering + voice selector helpers (split from tools-manager.js)
57
59
  static/js/ws-machine.js XState v5 WS connection machine: disconnected/connecting/connected/reconnecting
58
60
  static/js/conv-machine.js XState v5 per-conversation UI machine: idle/streaming/queued
61
+ static/js/tool-install-machine.js XState v5 per-tool UI install machine: idle/installing/installed/updating/needs_update/failed
62
+ static/js/voice-machine.js XState v5 voice/TTS machine: idle/queued/speaking/disabled (circuit-breaker)
63
+ static/js/conv-list-machine.js XState v5 conversation list machine: unloaded/loading/loaded/error
64
+ static/js/prompt-machine.js XState v5 prompt area machine: ready/loading/streaming/queued/disabled
59
65
  static/lib/xstate.umd.min.js XState v5 browser bundle (UMD, served locally from node_modules)
60
66
  static/templates/ 31 HTML template fragments for event rendering
61
67
  ```
62
68
 
63
69
  ## XState State Machines
64
70
 
65
- XState v5 machines provide formal state tracking alongside (not replacing) the existing Map-based execution tracking. The machines are authoritative for state validation; the Maps remain as the operational data store.
71
+ XState v5 machines are authoritative for their respective state domains. Ad-hoc Maps/Sets/booleans they replaced have been deleted.
66
72
 
67
73
  **Server machines** (ESM, `lib/`):
68
74
  - `execution-machine.js`: One actor per conversation. States: idle → streaming → draining (queue drain) → streaming → idle. Also rate_limited state. `execMachine.send(convId, event)` API. `conv.get` and `conv.full` WS responses include `executionState` field.
69
75
  - `acp-server-machine.js`: One actor per ACP tool (opencode/kilo/codex). States: stopped → starting → running ↔ crashed → restarting. Used by `acp-sdk-manager.js` to track health and drive restart backoff.
76
+ - `tool-install-machine.js`: One actor per tool ID. States: unchecked → checking → idle/installed/needs_update/installing/updating/failed. Replaces `installLocks` Map in `tool-spawner.js`. Events: CHECK_START, IDLE, INSTALLED, NEEDS_UPDATE, INSTALL_START, INSTALL_COMPLETE, UPDATE_START, UPDATE_COMPLETE, FAILED. API: `getOrCreate(toolId)`, `send(toolId, event)`, `isLocked(toolId)`, `getMachineActors()`. Context: version, error, installedAt, lastCheckedAt. `GET /api/debug/machines` returns all snapshots when `DEBUG=1`.
70
77
 
71
78
  **Client machines** (browser UMD, `static/js/`):
72
79
  - `ws-machine.js`: Wraps WebSocketManager. States: disconnected/connecting/connected/reconnecting. Actor accessible as `wsManager._wsActor`. State readable via `wsManager.connectionState`.
73
80
  - `conv-machine.js`: One actor per conversation. States: idle/streaming/queued. API exposed as `window.convMachineAPI`. All actors in `window.__convMachines` Map for debug.
81
+ - `tool-install-machine.js`: One actor per tool ID. States: idle/installing/installed/updating/needs_update/failed. Replaces `operationInProgress` Set in `tools-manager.js`. Context: version, error, progress, installedVersion, publishedVersion. API: `window.toolInstallMachineAPI`. Actors in `window.__toolInstallMachines`.
82
+ - `voice-machine.js`: Single actor for TTS playback. States: idle/queued/speaking/disabled. Replaces isSpeaking, isPlayingChunk, ttsDisabledUntilReset booleans and ttsConsecutiveFailures counter in `voice.js`. Circuit-breaker trips at 3 consecutive failures (disabled state, RESET to recover). API: `window.voiceMachineAPI`. Actor at `window.__voiceMachine`.
83
+ - `conv-list-machine.js`: Single actor for conversation list. States: unloaded/loading/loaded/error. Context: conversations[], activeId, streamingIds[], version, lastPollAt. Replaces `_conversationVersion`, `_lastMutationSource`, `streamingConversations` Set in `ConversationManager`. All list mutations go through machine events. API: `window.convListMachineAPI`. Actor at `window.__convListMachine`.
84
+ - `prompt-machine.js`: Single actor for prompt area. States: ready/loading/streaming/queued/disabled. Replaces dead `_promptState` string and `_promptStateTransitions` object in `client.js`. Driven by `enableControls()`, `disableControls()`, `handleStreamingStart()`, `handleStreamingComplete()`, `handleStreamingError()`. API: `window.promptMachineAPI`. Actor at `window.__promptMachine`.
74
85
 
75
- **XState browser loading**: UMD bundle at `static/lib/xstate.umd.min.js` (copied from `node_modules/xstate/dist/xstate.umd.min.js` during npm install). Loaded as `defer` script before ws-machine.js and conv-machine.js. Exposes `window.XState` global.
86
+ **XState browser loading**: UMD bundle at `static/lib/xstate.umd.min.js` (copied from `node_modules/xstate/dist/xstate.umd.min.js` during npm install). Loaded as `defer` script. Load order: xstate.umd.min.js → ws-machine.js conv-machine.js → tool-install-machine.js → voice-machine.js → conv-list-machine.js → prompt-machine.js → all other app scripts. Exposes `window.XState` global.
76
87
 
77
- **Integration pattern**: Machines augment existing state (Maps, Set, booleans). They receive events at the same points the Maps are mutated. Machine state does NOT replace Map checks in server logic — both run in parallel, machine is the formal model.
88
+ **Authoritative pattern**: Each machine owns its domain exclusively. No parallel ad-hoc state alongside machines. `window.__*` globals expose all client actors for debug inspection.
78
89
 
79
90
  ## Key Details
80
91
 
@@ -151,7 +162,7 @@ Tool updates are managed through a complete pipeline:
151
162
  3. Tool manager (`lib/tool-manager.js` lines 400-432) executes `bun x <package>` and detects new version
152
163
  4. Version is saved to database: `queries.updateToolStatus(toolId, { version, status: 'installed' })`
153
164
  5. WebSocket broadcasts `tool_update_complete` with version and status data
154
- 6. Frontend updates UI and removes tool from `operationInProgress` set
165
+ 6. Frontend machine transitions to installed/failed via WS event, UI re-renders from machine state
155
166
 
156
167
  **Critical Detail:** When updating tools in batch (`/api/tools/update`), the version parameter MUST be included in the database update call. This ensures database persistence across page reloads.
157
168
 
@@ -190,9 +201,9 @@ Current tools:
190
201
 
191
202
  When user clicks Install/Update button on a tool:
192
203
 
193
- 1. **Frontend** (`static/js/tools-manager.js`): Immediately updates status to 'installing'/'updating', sends POST, adds toolId to `operationInProgress` to prevent duplicates
194
- 2. **Backend** (`server.js`): Updates DB status, sends immediate `{ success: true }`, runs install/update async in background, broadcasts `tool_install_complete` or `tool_install_failed` on completion
195
- 3. **Frontend WebSocket Handler**: Listens for completion events, updates UI, removes from `operationInProgress`
204
+ 1. **Frontend** (`static/js/tools-manager.js`): Sends INSTALL/UPDATE event to `toolInstallMachineAPI`, sends POST. Machine guards duplicate requests via `isLocked()`.
205
+ 2. **Backend** (`server.js`): `tool-install-machine.js` sends INSTALL_START/UPDATE_START, runs async, sends INSTALL_COMPLETE/UPDATE_COMPLETE/FAILED. Broadcasts WS events.
206
+ 3. **Frontend WebSocket Handler**: Sends COMPLETE/FAILED/PROGRESS to machine. UI renders from machine state only.
196
207
 
197
208
  ## WebSocket Protocol
198
209
 
@@ -0,0 +1,157 @@
1
+ import { createMachine, createActor, assign } from 'xstate';
2
+
3
+ const machine = createMachine({
4
+ id: 'tool-install',
5
+ initial: 'unchecked',
6
+ context: {
7
+ version: null,
8
+ error: null,
9
+ installedAt: null,
10
+ lastCheckedAt: null,
11
+ },
12
+ states: {
13
+ unchecked: {
14
+ on: {
15
+ CHECK_START: 'checking',
16
+ INSTALL_START: 'installing',
17
+ },
18
+ },
19
+ checking: {
20
+ entry: assign({ lastCheckedAt: () => Date.now() }),
21
+ on: {
22
+ IDLE: {
23
+ target: 'idle',
24
+ actions: assign(({ event }) => ({
25
+ version: event.version || null,
26
+ lastCheckedAt: Date.now(),
27
+ })),
28
+ },
29
+ INSTALLED: {
30
+ target: 'installed',
31
+ actions: assign(({ event }) => ({
32
+ version: event.version || null,
33
+ installedAt: event.installedAt || Date.now(),
34
+ lastCheckedAt: Date.now(),
35
+ })),
36
+ },
37
+ NEEDS_UPDATE: {
38
+ target: 'needs_update',
39
+ actions: assign(({ event }) => ({
40
+ version: event.version || null,
41
+ lastCheckedAt: Date.now(),
42
+ })),
43
+ },
44
+ FAILED: {
45
+ target: 'failed',
46
+ actions: assign(({ event }) => ({ error: event.error || 'check failed' })),
47
+ },
48
+ },
49
+ },
50
+ idle: {
51
+ on: {
52
+ CHECK_START: 'checking',
53
+ INSTALL_START: 'installing',
54
+ },
55
+ },
56
+ installing: {
57
+ entry: assign({ error: null }),
58
+ on: {
59
+ INSTALL_COMPLETE: {
60
+ target: 'installed',
61
+ actions: assign(({ event }) => ({
62
+ version: event.version || null,
63
+ installedAt: Date.now(),
64
+ error: null,
65
+ })),
66
+ },
67
+ FAILED: {
68
+ target: 'failed',
69
+ actions: assign(({ event }) => ({ error: event.error || 'install failed' })),
70
+ },
71
+ },
72
+ },
73
+ installed: {
74
+ on: {
75
+ CHECK_START: 'checking',
76
+ UPDATE_START: 'updating',
77
+ },
78
+ },
79
+ updating: {
80
+ entry: assign({ error: null }),
81
+ on: {
82
+ UPDATE_COMPLETE: {
83
+ target: 'installed',
84
+ actions: assign(({ event }) => ({
85
+ version: event.version || null,
86
+ error: null,
87
+ })),
88
+ },
89
+ FAILED: {
90
+ target: 'failed',
91
+ actions: assign(({ event }) => ({ error: event.error || 'update failed' })),
92
+ },
93
+ },
94
+ },
95
+ needs_update: {
96
+ on: {
97
+ UPDATE_START: 'updating',
98
+ CHECK_START: 'checking',
99
+ },
100
+ },
101
+ failed: {
102
+ on: {
103
+ CHECK_START: 'checking',
104
+ INSTALL_START: 'installing',
105
+ },
106
+ },
107
+ },
108
+ });
109
+
110
+ const actors = new Map();
111
+
112
+ export function getOrCreate(toolId) {
113
+ if (actors.has(toolId)) return actors.get(toolId);
114
+ const actor = createActor(machine);
115
+ actor.start();
116
+ actors.set(toolId, actor);
117
+ return actor;
118
+ }
119
+
120
+ export function get(toolId) {
121
+ return actors.get(toolId) || null;
122
+ }
123
+
124
+ export function send(toolId, event) {
125
+ const actor = getOrCreate(toolId);
126
+ actor.send(event);
127
+ return actor.getSnapshot();
128
+ }
129
+
130
+ export function snapshot(toolId) {
131
+ const actor = actors.get(toolId);
132
+ return actor ? actor.getSnapshot() : null;
133
+ }
134
+
135
+ export function isLocked(toolId) {
136
+ const s = snapshot(toolId);
137
+ return s ? (s.value === 'installing' || s.value === 'updating') : false;
138
+ }
139
+
140
+ export function getState(toolId) {
141
+ const s = snapshot(toolId);
142
+ return s ? s.value : 'unchecked';
143
+ }
144
+
145
+ export function remove(toolId) {
146
+ const actor = actors.get(toolId);
147
+ if (actor) { actor.stop(); actors.delete(toolId); }
148
+ }
149
+
150
+ export function stopAll() {
151
+ for (const [, actor] of actors) actor.stop();
152
+ actors.clear();
153
+ }
154
+
155
+ export function getMachineActors() {
156
+ return actors;
157
+ }
@@ -17,7 +17,6 @@ const TOOLS = [
17
17
  ];
18
18
 
19
19
  const statusCache = new Map();
20
- const installLocks = new Map();
21
20
 
22
21
  const getTool = (id) => TOOLS.find(t => t.id === id);
23
22
 
@@ -51,7 +50,7 @@ export async function checkForUpdates(toolId) {
51
50
  return { needsUpdate: status.upgradeNeeded && status.installed };
52
51
  }
53
52
 
54
- const { install, update } = createInstaller(getTool, installLocks, statusCache, checkToolStatusAsync);
53
+ const { install, update } = createInstaller(getTool, statusCache, checkToolStatusAsync);
55
54
  export { install, update };
56
55
 
57
56
  export function getAllTools() {
@@ -1,4 +1,5 @@
1
1
  import { checkToolViaBunx } from './tool-version.js';
2
+ import * as toolInstallMachine from './tool-install-machine.js';
2
3
 
3
4
  let updateCheckInterval = null;
4
5
  const UPDATE_CHECK_INTERVAL = 6 * 60 * 60 * 1000;
@@ -8,9 +9,11 @@ export async function autoProvision(TOOLS, statusCache, install, broadcast) {
8
9
  log('Starting background tool provisioning...');
9
10
  for (const tool of TOOLS) {
10
11
  try {
12
+ toolInstallMachine.send(tool.id, { type: 'CHECK_START' });
11
13
  const status = await checkToolViaBunx(tool.pkg, tool.pluginId, tool.category, tool.frameWork, true, TOOLS);
12
14
  statusCache.set(tool.id, { ...status, toolId: tool.id, timestamp: Date.now() });
13
15
  if (!status.installed) {
16
+ toolInstallMachine.send(tool.id, { type: 'IDLE' });
14
17
  log(`${tool.id} not installed, installing...`);
15
18
  broadcast({ type: 'tool_install_started', toolId: tool.id });
16
19
  const result = await install(tool.id, (msg) => {
@@ -24,9 +27,9 @@ export async function autoProvision(TOOLS, statusCache, install, broadcast) {
24
27
  broadcast({ type: 'tool_install_failed', toolId: tool.id, data: result });
25
28
  }
26
29
  } else if (status.upgradeNeeded) {
30
+ toolInstallMachine.send(tool.id, { type: 'NEEDS_UPDATE', version: status.installedVersion });
27
31
  log(`${tool.id} needs update (${status.installedVersion} -> ${status.publishedVersion})`);
28
32
  broadcast({ type: 'tool_install_started', toolId: tool.id });
29
- const { update } = await import('./tool-spawner.js').then(m => m);
30
33
  const result = await install(tool.id, (msg) => {
31
34
  broadcast({ type: 'tool_update_progress', toolId: tool.id, data: msg });
32
35
  });
@@ -38,10 +41,12 @@ export async function autoProvision(TOOLS, statusCache, install, broadcast) {
38
41
  broadcast({ type: 'tool_update_failed', toolId: tool.id, data: result });
39
42
  }
40
43
  } else {
44
+ toolInstallMachine.send(tool.id, { type: 'INSTALLED', version: status.installedVersion });
41
45
  log(`${tool.id} v${status.installedVersion || 'unknown'} up-to-date`);
42
46
  broadcast({ type: 'tool_status_update', toolId: tool.id, data: { installed: true, isUpToDate: true, installedVersion: status.installedVersion, status: 'installed' } });
43
47
  }
44
48
  } catch (err) {
49
+ toolInstallMachine.send(tool.id, { type: 'FAILED', error: err.message });
45
50
  log(`${tool.id} error: ${err.message}`);
46
51
  }
47
52
  }
@@ -1,6 +1,7 @@
1
1
  import { spawn } from 'child_process';
2
2
  import os from 'os';
3
3
  import { getCliVersion, getInstalledVersion, clearVersionCache, checkToolViaBunx } from './tool-version.js';
4
+ import * as toolInstallMachine from './tool-install-machine.js';
4
5
 
5
6
  const isWindows = os.platform() === 'win32';
6
7
 
@@ -86,18 +87,24 @@ async function postInstallRefresh(tool, statusCache, checkToolStatusAsync) {
86
87
  return { success: true, error: null, version: version || freshStatus.publishedVersion || 'unknown', ...freshStatus };
87
88
  }
88
89
 
89
- export function createInstaller(getTool, installLocks, statusCache, checkToolStatusAsync) {
90
+ export function createInstaller(getTool, statusCache, checkToolStatusAsync) {
90
91
  async function install(toolId, onProgress) {
91
92
  const tool = getTool(toolId);
92
93
  if (!tool) return { success: false, error: 'Tool not found' };
93
- if (installLocks.get(toolId)) return { success: false, error: 'Install in progress' };
94
- installLocks.set(toolId, true);
94
+ if (toolInstallMachine.isLocked(toolId)) return { success: false, error: 'Install in progress' };
95
+ toolInstallMachine.send(toolId, { type: 'INSTALL_START' });
95
96
  try {
96
97
  const result = await spawnForTool(tool, onProgress);
97
- if (result.success) return await postInstallRefresh(tool, statusCache, checkToolStatusAsync);
98
+ if (result.success) {
99
+ const fresh = await postInstallRefresh(tool, statusCache, checkToolStatusAsync);
100
+ toolInstallMachine.send(toolId, { type: 'INSTALL_COMPLETE', version: fresh.version });
101
+ return fresh;
102
+ }
103
+ toolInstallMachine.send(toolId, { type: 'FAILED', error: result.error });
98
104
  return result;
99
- } finally {
100
- installLocks.delete(toolId);
105
+ } catch (err) {
106
+ toolInstallMachine.send(toolId, { type: 'FAILED', error: err.message });
107
+ return { success: false, error: err.message };
101
108
  }
102
109
  }
103
110
 
@@ -106,14 +113,20 @@ export function createInstaller(getTool, installLocks, statusCache, checkToolSta
106
113
  if (!tool) return { success: false, error: 'Tool not found' };
107
114
  const current = await checkToolStatusAsync(toolId);
108
115
  if (!current?.installed) return { success: false, error: 'Tool not installed' };
109
- if (installLocks.get(toolId)) return { success: false, error: 'Install in progress' };
110
- installLocks.set(toolId, true);
116
+ if (toolInstallMachine.isLocked(toolId)) return { success: false, error: 'Install in progress' };
117
+ toolInstallMachine.send(toolId, { type: 'UPDATE_START' });
111
118
  try {
112
119
  const result = await spawnForTool(tool, onProgress);
113
- if (result.success) return await postInstallRefresh(tool, statusCache, checkToolStatusAsync);
120
+ if (result.success) {
121
+ const fresh = await postInstallRefresh(tool, statusCache, checkToolStatusAsync);
122
+ toolInstallMachine.send(toolId, { type: 'UPDATE_COMPLETE', version: fresh.version });
123
+ return fresh;
124
+ }
125
+ toolInstallMachine.send(toolId, { type: 'FAILED', error: result.error });
114
126
  return result;
115
- } finally {
116
- installLocks.delete(toolId);
127
+ } catch (err) {
128
+ toolInstallMachine.send(toolId, { type: 'FAILED', error: err.message });
129
+ return { success: false, error: err.message };
117
130
  }
118
131
  }
119
132
 
@@ -2,6 +2,7 @@ import fs from 'fs';
2
2
  import os from 'os';
3
3
  import path from 'path';
4
4
  import { execSync, spawnSync } from 'child_process';
5
+ import * as toolInstallMachine from './tool-install-machine.js';
5
6
 
6
7
  function err(code, message) { const e = new Error(message); e.code = code; throw e; }
7
8
 
@@ -141,7 +142,10 @@ export function register(router, deps) {
141
142
  router.handle('tools.list', async () => {
142
143
  try {
143
144
  const tools = await toolManager.getAllToolsAsync();
144
- return { tools: tools.map((t) => ({ id: t.id, name: t.name, pkg: t.pkg, category: t.category || 'plugin', installed: t.installed, status: t.installed ? (t.isUpToDate ? 'installed' : 'needs_update') : 'not_installed', isUpToDate: t.isUpToDate, upgradeNeeded: t.upgradeNeeded, hasUpdate: t.upgradeNeeded && t.installed, installedVersion: t.installedVersion, publishedVersion: t.publishedVersion })) };
145
+ return { tools: tools.map((t) => {
146
+ const machineState = toolInstallMachine.getState(t.id);
147
+ return { id: t.id, name: t.name, pkg: t.pkg, category: t.category || 'plugin', installed: t.installed, status: t.installed ? (t.isUpToDate ? 'installed' : 'needs_update') : 'not_installed', isUpToDate: t.isUpToDate, upgradeNeeded: t.upgradeNeeded, hasUpdate: t.upgradeNeeded && t.installed, installedVersion: t.installedVersion, publishedVersion: t.publishedVersion, machineState };
148
+ }) };
145
149
  } catch (e) { err(500, e.message); }
146
150
  });
147
151
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.727",
3
+ "version": "1.0.729",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "server.js",
package/server.js CHANGED
@@ -31,6 +31,7 @@ import { register as registerQueueHandlers } from './lib/ws-handlers-queue.js';
31
31
  import { register as registerMsgHandlers } from './lib/ws-handlers-msg.js';
32
32
  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';
33
33
  import * as execMachine from './lib/execution-machine.js';
34
+ import * as toolInstallMachine from './lib/tool-install-machine.js';
34
35
  import { installGMAgentConfigs } from './lib/gm-agent-configs.js';
35
36
  import * as toolManager from './lib/tool-manager.js';
36
37
  import { pm2Manager } from './lib/pm2-manager.js';
@@ -2099,6 +2100,22 @@ const server = http.createServer(async (req, res) => {
2099
2100
  return;
2100
2101
  }
2101
2102
 
2103
+ if (pathOnly === '/api/debug/machines' && req.method === 'GET' && process.env.DEBUG) {
2104
+ const toolSnap = {};
2105
+ for (const [id, actor] of toolInstallMachine.getMachineActors()) {
2106
+ const s = actor.getSnapshot();
2107
+ toolSnap[id] = { state: s.value, context: s.context };
2108
+ }
2109
+ const execSnap = {};
2110
+ const allExecConvIds = [...activeExecutions.keys()];
2111
+ for (const id of allExecConvIds) {
2112
+ const s = execMachine.snapshot(id);
2113
+ if (s) execSnap[id] = { state: s.value, context: { pid: s.context.pid, sessionId: s.context.sessionId, queueLen: s.context.queue?.length } };
2114
+ }
2115
+ sendJSON(req, res, 200, { toolInstall: toolSnap, execution: execSnap });
2116
+ return;
2117
+ }
2118
+
2102
2119
  if (pathOnly === '/api/tools' && req.method === 'GET') {
2103
2120
  console.log('[TOOLS-API] Handling GET /api/tools');
2104
2121
  try {
@@ -2118,12 +2135,12 @@ const server = http.createServer(async (req, res) => {
2118
2135
  upgradeNeeded: t.upgradeNeeded,
2119
2136
  hasUpdate: t.upgradeNeeded && t.installed,
2120
2137
  installedVersion: t.installedVersion,
2121
- publishedVersion: t.publishedVersion
2138
+ publishedVersion: t.publishedVersion,
2139
+ machineState: toolInstallMachine.getState(t.id),
2122
2140
  }));
2123
2141
  sendJSON(req, res, 200, { tools: result });
2124
2142
  } catch (err) {
2125
2143
  console.log('[TOOLS-API] Error getting tools, returning cached status:', err.message);
2126
- // Return synchronously cached tool status - this provides immediate response with last-known status
2127
2144
  const tools = toolManager.getAllToolsSync().map((t) => ({
2128
2145
  id: t.id,
2129
2146
  name: t.name,
@@ -2135,7 +2152,8 @@ const server = http.createServer(async (req, res) => {
2135
2152
  upgradeNeeded: t.upgradeNeeded || false,
2136
2153
  hasUpdate: (t.upgradeNeeded && t.installed) || false,
2137
2154
  installedVersion: t.installedVersion || null,
2138
- publishedVersion: t.publishedVersion || null
2155
+ publishedVersion: t.publishedVersion || null,
2156
+ machineState: toolInstallMachine.getState(t.id),
2139
2157
  }));
2140
2158
  sendJSON(req, res, 200, { tools });
2141
2159
  }
@@ -5139,8 +5157,9 @@ function onServerReady() {
5139
5157
  toolManager.autoProvision(toolBroadcaster)
5140
5158
  .catch(err => console.error('[TOOLS] Auto-provision error:', err.message))
5141
5159
  .then(() => {
5142
- // Start periodic update checker AFTER initial provisioning completes
5143
- // This runs in background and doesn't block GUI
5160
+ const acpActors = ['opencode', 'kilo', 'codex'];
5161
+ const execActorCount = execMachine.stopAll ? 0 : 0;
5162
+ console.log(`[MACHINES] tool-install: ${toolInstallMachine.getMachineActors().size} actors, acp-server: ${acpActors.length} configured`);
5144
5163
  console.log('[TOOLS] Starting periodic update checker...');
5145
5164
  toolManager.startPeriodicUpdateCheck(toolBroadcaster);
5146
5165
  });
package/static/index.html CHANGED
@@ -3314,6 +3314,10 @@
3314
3314
  <script defer src="/gm/lib/xstate.umd.min.js"></script>
3315
3315
  <script defer src="/gm/js/ws-machine.js"></script>
3316
3316
  <script defer src="/gm/js/conv-machine.js"></script>
3317
+ <script defer src="/gm/js/tool-install-machine.js"></script>
3318
+ <script defer src="/gm/js/voice-machine.js"></script>
3319
+ <script defer src="/gm/js/conv-list-machine.js"></script>
3320
+ <script defer src="/gm/js/prompt-machine.js"></script>
3317
3321
  <script defer src="/gm/lib/msgpackr.min.js"></script>
3318
3322
  <script defer src="/gm/js/websocket-manager.js"></script>
3319
3323
  <script defer src="/gm/js/ws-client.js"></script>
@@ -3323,6 +3327,7 @@
3323
3327
  <script defer src="/gm/js/state-barrier.js"></script>
3324
3328
  <script defer src="/gm/js/terminal.js"></script>
3325
3329
  <script defer src="/gm/js/script-runner.js"></script>
3330
+ <script defer src="/gm/js/tools-manager-ui.js"></script>
3326
3331
  <script defer src="/gm/js/tools-manager.js"></script>
3327
3332
  <script defer src="/gm/js/stt-handler.js"></script>
3328
3333
  <script defer src="/gm/js/voice.js"></script>