agentgui 1.0.728 → 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 +19 -8
- package/lib/tool-install-machine.js +157 -0
- package/lib/tool-manager.js +1 -2
- package/lib/tool-provisioner.js +6 -1
- package/lib/tool-spawner.js +24 -11
- package/lib/ws-handlers-util.js +5 -1
- package/package.json +1 -1
- package/server.js +24 -5
- package/static/index.html +5 -0
- package/static/js/client.js +5 -13
- package/static/js/conv-list-machine.js +137 -0
- package/static/js/conversations.js +39 -74
- package/static/js/prompt-machine.js +108 -0
- package/static/js/tool-install-machine.js +155 -0
- package/static/js/tools-manager-ui.js +119 -0
- package/static/js/tools-manager.js +164 -435
- package/static/js/voice-machine.js +145 -0
- package/static/js/voice.js +132 -119
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
|
|
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
|
|
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
|
-
**
|
|
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
|
|
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`):
|
|
194
|
-
2. **Backend** (`server.js`):
|
|
195
|
-
3. **Frontend WebSocket Handler**:
|
|
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
|
+
}
|
package/lib/tool-manager.js
CHANGED
|
@@ -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,
|
|
53
|
+
const { install, update } = createInstaller(getTool, statusCache, checkToolStatusAsync);
|
|
55
54
|
export { install, update };
|
|
56
55
|
|
|
57
56
|
export function getAllTools() {
|
package/lib/tool-provisioner.js
CHANGED
|
@@ -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
|
}
|
package/lib/tool-spawner.js
CHANGED
|
@@ -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,
|
|
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 (
|
|
94
|
-
|
|
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)
|
|
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
|
-
}
|
|
100
|
-
|
|
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 (
|
|
110
|
-
|
|
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)
|
|
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
|
-
}
|
|
116
|
-
|
|
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
|
|
package/lib/ws-handlers-util.js
CHANGED
|
@@ -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) =>
|
|
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
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
|
-
|
|
5143
|
-
|
|
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>
|
package/static/js/client.js
CHANGED
|
@@ -75,16 +75,6 @@ class AgentGUIClient {
|
|
|
75
75
|
this._loadInProgress = {}; // { [conversationId]: { requestId, abortController, timestamp, prevConversationId } }
|
|
76
76
|
this._currentRequestId = 0; // Auto-incrementing request counter
|
|
77
77
|
|
|
78
|
-
// Prompt area state machine: READY | LOADING | STREAMING | QUEUED | DISABLED
|
|
79
|
-
// Controls atomic transitions to prevent inconsistent UI states
|
|
80
|
-
this._promptState = 'READY'; // Initial state
|
|
81
|
-
this._promptStateTransitions = {
|
|
82
|
-
'READY': ['LOADING', 'STREAMING', 'DISABLED'],
|
|
83
|
-
'LOADING': ['READY', 'STREAMING', 'DISABLED'],
|
|
84
|
-
'STREAMING': ['QUEUED', 'READY'],
|
|
85
|
-
'QUEUED': ['STREAMING', 'READY'],
|
|
86
|
-
'DISABLED': ['READY']
|
|
87
|
-
};
|
|
88
78
|
|
|
89
79
|
this._scrollTarget = 0;
|
|
90
80
|
this._scrollAnimating = false;
|
|
@@ -799,6 +789,7 @@ class AgentGUIClient {
|
|
|
799
789
|
|
|
800
790
|
async handleStreamingStart(data) {
|
|
801
791
|
console.log('Streaming started:', data);
|
|
792
|
+
if (window.promptMachineAPI) window.promptMachineAPI.send({ type: 'STREAMING', conversationId: data.conversationId });
|
|
802
793
|
this._clearThinkingCountdown();
|
|
803
794
|
if (this._lastSendTime > 0) {
|
|
804
795
|
const actual = Date.now() - this._lastSendTime;
|
|
@@ -1066,6 +1057,7 @@ class AgentGUIClient {
|
|
|
1066
1057
|
|
|
1067
1058
|
handleStreamingError(data) {
|
|
1068
1059
|
console.error('Streaming error:', data);
|
|
1060
|
+
if (window.promptMachineAPI) window.promptMachineAPI.send({ type: 'READY' });
|
|
1069
1061
|
this._clearThinkingCountdown();
|
|
1070
1062
|
|
|
1071
1063
|
// Hide stop and inject buttons on error
|
|
@@ -1143,6 +1135,7 @@ class AgentGUIClient {
|
|
|
1143
1135
|
|
|
1144
1136
|
handleStreamingComplete(data) {
|
|
1145
1137
|
console.log('Streaming completed:', data);
|
|
1138
|
+
if (window.promptMachineAPI) window.promptMachineAPI.send({ type: 'READY' });
|
|
1146
1139
|
this._clearThinkingCountdown();
|
|
1147
1140
|
|
|
1148
1141
|
const conversationId = data.conversationId || this.state.currentSession?.conversationId;
|
|
@@ -2532,15 +2525,14 @@ class AgentGUIClient {
|
|
|
2532
2525
|
*/
|
|
2533
2526
|
disableControls() {
|
|
2534
2527
|
if (this.ui.sendButton) this.ui.sendButton.disabled = true;
|
|
2528
|
+
if (window.promptMachineAPI) window.promptMachineAPI.send({ type: 'DISABLED' });
|
|
2535
2529
|
}
|
|
2536
2530
|
|
|
2537
|
-
/**
|
|
2538
|
-
* Enable UI controls after execution completes or fails
|
|
2539
|
-
*/
|
|
2540
2531
|
enableControls() {
|
|
2541
2532
|
if (this.ui.sendButton) {
|
|
2542
2533
|
this.ui.sendButton.disabled = !this.wsManager?.isConnected;
|
|
2543
2534
|
}
|
|
2535
|
+
if (window.promptMachineAPI) window.promptMachineAPI.send({ type: 'READY' });
|
|
2544
2536
|
this.updateBusyPromptArea(this.state.currentConversation?.id);
|
|
2545
2537
|
}
|
|
2546
2538
|
|