agentgui 1.0.675 → 1.0.676
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 +119 -218
- package/database.js +55 -10
- package/docs/index.html +1 -1
- package/lib/claude-runner.js +5 -4
- package/lib/codec.js +2 -51
- package/lib/tool-manager.js +5 -3
- package/lib/ws-handlers-conv.js +22 -38
- package/lib/ws-optimizer.js +11 -10
- package/package.json +2 -3
- package/scripts/patch-fsbrowse.js +25 -16
- package/server.js +109 -60
- package/static/index.html +36 -1
- package/static/js/client.js +58 -25
- package/static/js/conversations.js +26 -2
- package/static/js/terminal.js +2 -2
- package/static/js/websocket-manager.js +5 -22
- package/static/theme.js +6 -0
- package/test-state-management.mjs +269 -0
- package/test-thread-steering.mjs +100 -0
package/CLAUDE.md
CHANGED
|
@@ -18,6 +18,13 @@ server.js HTTP server + WebSocket + all API routes (raw http.create
|
|
|
18
18
|
database.js SQLite setup (WAL mode), schema, query functions
|
|
19
19
|
lib/claude-runner.js Agent framework - spawns CLI processes, parses stream-json output
|
|
20
20
|
lib/acp-manager.js ACP tool lifecycle - auto-starts opencode/kilo HTTP servers, restart on crash
|
|
21
|
+
lib/ws-protocol.js WebSocket RPC router (WsRouter class)
|
|
22
|
+
lib/ws-optimizer.js Per-client priority queue for WS event batching
|
|
23
|
+
lib/ws-handlers-conv.js Conversation/message/queue RPC handlers (~70 methods total)
|
|
24
|
+
lib/ws-handlers-session.js Session/agent RPC handlers
|
|
25
|
+
lib/ws-handlers-run.js Thread/run RPC handlers
|
|
26
|
+
lib/ws-handlers-util.js Utility RPC handlers (speech, auth, git, tools)
|
|
27
|
+
lib/tool-manager.js Tool detection, installation, version checking
|
|
21
28
|
lib/speech.js Speech-to-text and text-to-speech via @huggingface/transformers
|
|
22
29
|
bin/gmgui.cjs CLI entry point (npx agentgui / bun x agentgui)
|
|
23
30
|
static/index.html Main HTML shell
|
|
@@ -33,6 +40,7 @@ static/js/ui-components.js UI component helpers
|
|
|
33
40
|
static/js/syntax-highlighter.js Code syntax highlighting
|
|
34
41
|
static/js/voice.js Voice input/output
|
|
35
42
|
static/js/features.js View toggle, drag-drop upload, model progress indicator
|
|
43
|
+
static/js/tools-manager.js Tool install/update UI
|
|
36
44
|
static/templates/ 31 HTML template fragments for event rendering
|
|
37
45
|
```
|
|
38
46
|
|
|
@@ -42,6 +50,10 @@ static/templates/ 31 HTML template fragments for event rendering
|
|
|
42
50
|
- Agent discovery scans PATH for known CLI binaries (claude, opencode, gemini, goose, etc.) at startup.
|
|
43
51
|
- Database lives at `~/.gmgui/data.db`. Tables: conversations, messages, events, sessions, stream chunks.
|
|
44
52
|
- WebSocket endpoint is at `BASE_URL + /sync`. Supports subscribe/unsubscribe by sessionId or conversationId, and ping.
|
|
53
|
+
- All WS RPC uses msgpack binary encoding (lib/codec.js). Wire format: `{ r, m, p }` request, `{ r, d }` reply, `{ type, seq }` broadcast push.
|
|
54
|
+
- `perMessageDeflate` is disabled on the WS server — msgpack binary doesn't compress well and brotli/gzip was blocking the event loop. HTTP-layer gzip handles static assets.
|
|
55
|
+
- Static assets use `Cache-Control: max-age=31536000, immutable` + ETag. Compressed once on first request, served from RAM (`_assetCache` Map keyed by etag).
|
|
56
|
+
- Deployment: runs behind Traefik/Caddy which handles TLS and can support WebTransport/QUIC.
|
|
45
57
|
|
|
46
58
|
## Environment Variables
|
|
47
59
|
|
|
@@ -103,286 +115,175 @@ Tool updates are managed through a complete pipeline:
|
|
|
103
115
|
5. WebSocket broadcasts `tool_update_complete` with version and status data
|
|
104
116
|
6. Frontend updates UI and removes tool from `operationInProgress` set
|
|
105
117
|
|
|
106
|
-
**Critical Detail:** When updating tools in batch (`/api/tools/update`), the version parameter MUST be included in the database update call
|
|
118
|
+
**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.
|
|
107
119
|
|
|
108
|
-
**Version Detection Sources** (`lib/tool-manager.js`
|
|
120
|
+
**Version Detection Sources** (`lib/tool-manager.js`):
|
|
109
121
|
- Claude Code: `~/.claude/plugins/{pluginId}/plugin.json`
|
|
110
122
|
- OpenCode: `~/.config/opencode/agents/{pluginId}/plugin.json`
|
|
111
123
|
- Gemini CLI: `~/.gemini/extensions/{pluginId}/plugin.json`
|
|
112
124
|
- Kilo: `~/.config/kilo/agents/{pluginId}/plugin.json`
|
|
113
125
|
|
|
114
|
-
**Database Schema** (`database.js`
|
|
126
|
+
**Database Schema** (`database.js`):
|
|
115
127
|
- Table: `tool_installations` (toolId, version, status, installed_at, error_message)
|
|
116
128
|
- Table: `tool_install_history` (action, status, error_message for audit trail)
|
|
117
129
|
|
|
118
130
|
## Tool Detection System
|
|
119
131
|
|
|
120
|
-
|
|
121
|
-
-
|
|
122
|
-
-
|
|
123
|
-
- **Kilo**: `@kilocode/cli` package (id: gm-kilo)
|
|
124
|
-
- **Claude Code**: `@anthropic-ai/claude-code` package (id: gm-cc)
|
|
132
|
+
TOOLS array in `lib/tool-manager.js` — two categories:
|
|
133
|
+
- **`cli`**: `{ id, name, pkg, category: 'cli' }` — detected via `which <bin>` + `<bin> --version`
|
|
134
|
+
- **`plugin`**: `{ id, name, pkg, installPkg, pluginId, category: 'plugin', frameWork }` — detected via plugin.json files
|
|
125
135
|
|
|
126
|
-
|
|
127
|
-
-
|
|
128
|
-
-
|
|
129
|
-
-
|
|
130
|
-
-
|
|
136
|
+
Current tools:
|
|
137
|
+
- `cli-claude`: bin=`claude`, pkg=`@anthropic-ai/claude-code`
|
|
138
|
+
- `cli-opencode`: bin=`opencode`, pkg=`opencode-ai`
|
|
139
|
+
- `cli-gemini`: bin=`gemini`, pkg=`@google/gemini-cli`
|
|
140
|
+
- `cli-kilo`: bin=`kilo`, pkg=`@kilocode/cli`
|
|
141
|
+
- `cli-codex`: bin=`codex`, pkg=`@openai/codex`
|
|
142
|
+
- `cli-agent-browser`: bin=`agent-browser`, pkg=`agent-browser` — uses `-V` flag (not `--version`) for version detection
|
|
143
|
+
- `gm-cc`, `gm-oc`, `gm-gc`, `gm-kilo`, `gm-codex`: plugin tools
|
|
131
144
|
|
|
132
|
-
|
|
145
|
+
**binMap gotcha:** `checkCliInstalled()` and `getCliVersion()` both have a `binMap` object. Any new CLI tool must be added to BOTH. `agent-browser` uses `-V` (not `--version`) — a `versionFlag` override handles this.
|
|
146
|
+
|
|
147
|
+
**Background provisioning:** `autoProvision()` runs at startup, checks/installs missing tools (~10s). `startPeriodicUpdateCheck()` runs every 6 hours in background to check for updates. Both broadcast tool status via WebSocket so UI stays in sync.
|
|
133
148
|
|
|
134
149
|
### Tool Installation and Update UI Flow
|
|
135
150
|
|
|
136
151
|
When user clicks Install/Update button on a tool:
|
|
137
152
|
|
|
138
|
-
1. **Frontend** (`static/js/tools-manager.js`):
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
- Adds toolId to `operationInProgress` to prevent duplicate requests
|
|
142
|
-
- Button becomes disabled showing progress indicator while install runs
|
|
143
|
-
|
|
144
|
-
2. **Backend** (`server.js` lines 1819-1851):
|
|
145
|
-
- Receives POST request, updates database status to 'installing'/'updating'
|
|
146
|
-
- Sends immediate response `{ success: true }`
|
|
147
|
-
- Asynchronously calls `toolManager.install/update()` in background
|
|
148
|
-
- Upon completion, broadcasts WebSocket event `tool_install_complete` or `tool_install_failed`
|
|
149
|
-
|
|
150
|
-
3. **Frontend WebSocket Handler** (`static/js/tools-manager.js` lines 138-151):
|
|
151
|
-
- Listens for `tool_install_complete` or `tool_install_failed` events
|
|
152
|
-
- Updates tool status and re-renders final state
|
|
153
|
-
- Removes toolId from `operationInProgress`, enabling button again
|
|
154
|
-
|
|
155
|
-
The UI shows progress in three phases: immediate "Installing" status, progress bar animation during install, and final "Installed"/"Failed" status when complete.
|
|
153
|
+
1. **Frontend** (`static/js/tools-manager.js`): Immediately updates status to 'installing'/'updating', sends POST, adds toolId to `operationInProgress` to prevent duplicates
|
|
154
|
+
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
|
|
155
|
+
3. **Frontend WebSocket Handler**: Listens for completion events, updates UI, removes from `operationInProgress`
|
|
156
156
|
|
|
157
157
|
## WebSocket Protocol
|
|
158
158
|
|
|
159
159
|
Endpoint: `BASE_URL + /sync`
|
|
160
160
|
|
|
161
|
+
**Wire format (msgpack binary):**
|
|
162
|
+
- Client RPC request: `{ r: requestId, m: method, p: params }`
|
|
163
|
+
- Server RPC reply: `{ r: requestId, d: data }` or `{ r: requestId, e: { c: code, m: message } }`
|
|
164
|
+
- Server push/broadcast: `{ type, seq, ...data }` or array of these when batched
|
|
165
|
+
|
|
166
|
+
**Legacy control messages** (bypass RPC router, handled in `onLegacy`): `subscribe`, `unsubscribe`, `ping`, `latency_report`, `terminal_*`, `pm2_*`, `set_voice`, `get_subscriptions`
|
|
167
|
+
|
|
161
168
|
Client sends:
|
|
162
169
|
- `{ type: "subscribe", sessionId }` or `{ type: "subscribe", conversationId }`
|
|
163
170
|
- `{ type: "unsubscribe", sessionId }`
|
|
164
171
|
- `{ type: "ping" }`
|
|
165
172
|
|
|
166
173
|
Server broadcasts:
|
|
167
|
-
- `streaming_start` - Agent execution started
|
|
168
|
-
- `streaming_progress` - New event/chunk from agent
|
|
169
|
-
- `streaming_complete` - Execution finished
|
|
170
|
-
- `streaming_error` - Execution failed
|
|
174
|
+
- `streaming_start` - Agent execution started (high priority, flushes immediately)
|
|
175
|
+
- `streaming_progress` - New event/chunk from agent (normal priority, batched)
|
|
176
|
+
- `streaming_complete` - Execution finished (high priority)
|
|
177
|
+
- `streaming_error` - Execution failed (high priority)
|
|
178
|
+
- `message_created` - New message (high priority, flushes immediately)
|
|
171
179
|
- `conversation_created`, `conversation_updated`, `conversation_deleted`
|
|
180
|
+
- `all_conversations_deleted` - Must be in BROADCAST_TYPES set
|
|
172
181
|
- `model_download_progress` - Voice model download progress
|
|
173
182
|
- `voice_list` - Available TTS voices
|
|
174
183
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
Speech models (~470MB total) are downloaded automatically on server startup. No credentials required.
|
|
178
|
-
|
|
179
|
-
### Download Sources (fallback chain)
|
|
180
|
-
1. **GitHub LFS** (primary): `https://github.com/AnEntrypoint/models` - LFS-tracked ONNX files via `media.githubusercontent.com`, small files via `raw.githubusercontent.com`
|
|
181
|
-
2. **HuggingFace** (fallback): `onnx-community/whisper-base` for STT, `AnEntrypoint/sttttsmodels` for TTS
|
|
182
|
-
|
|
183
|
-
### Models
|
|
184
|
-
- **Whisper Base** (~280MB): encoder + decoder ONNX models, tokenizer, config files
|
|
185
|
-
- **TTS Models** (~190MB): mimi encoder/decoder, flow_lm, text_conditioner, tokenizer
|
|
186
|
-
|
|
187
|
-
### UI Behavior
|
|
188
|
-
- Voice tab is hidden until models are ready
|
|
189
|
-
- A circular progress indicator appears in the header during download
|
|
190
|
-
- Once models are downloaded, the Voice tab becomes visible
|
|
191
|
-
- Model status is broadcast via WebSocket `model_download_progress` events
|
|
192
|
-
|
|
193
|
-
### Cache Location
|
|
194
|
-
Models are stored at `~/.gmgui/models/` (whisper in `onnx-community/whisper-base/`, TTS in `tts/`).
|
|
195
|
-
|
|
196
|
-
## Tool Update Process Fix
|
|
197
|
-
|
|
198
|
-
### Issue
|
|
199
|
-
Tool update/install operations would complete successfully but the version display in the UI would not update to reflect the new version.
|
|
200
|
-
|
|
201
|
-
### Root Cause
|
|
202
|
-
The WebSocket broadcast event for tool update/install completion was missing the `version` field. The server was sending only the `freshStatus` object (which contains `installedVersion`), but not including the extracted `version` field from the tool-manager result.
|
|
203
|
-
|
|
204
|
-
Frontend expected: `data.data.version`
|
|
205
|
-
Backend was sending: only `data.data.installedVersion`
|
|
206
|
-
|
|
207
|
-
### Solution
|
|
208
|
-
Updated WebSocket broadcasts in `server.js`:
|
|
209
|
-
- Line 1883: Install endpoint now includes `version` in broadcast data
|
|
210
|
-
- Line 1942: Update endpoint now includes `version` in broadcast data
|
|
211
|
-
- Line 1987: Legacy install endpoint now saves `version` to database
|
|
212
|
-
|
|
213
|
-
The broadcasts now include both the immediately-detected `version` field and the comprehensive `freshStatus` object, ensuring the frontend has complete information to update the UI correctly.
|
|
184
|
+
**WSOptimizer** (`lib/ws-optimizer.js`): Per-client priority queue. High-priority events flush immediately; normal/low batch by latency tier (16ms excellent → 200ms bad). Rate limit: 100 msg/sec — overflow is re-queued (not dropped). No `lastKey` deduplication (was removed — caused valid event drops).
|
|
214
185
|
|
|
215
|
-
|
|
216
|
-
After update/install completes:
|
|
217
|
-
1. WebSocket event `tool_update_complete` or `tool_install_complete` is broadcast
|
|
218
|
-
2. Frontend receives complete data with `version`, `installedVersion`, `isUpToDate`, etc.
|
|
219
|
-
3. UI version display updates to show new version
|
|
220
|
-
4. Status reverts to "Installed" or "Up-to-date" accordingly
|
|
186
|
+
## Steering
|
|
221
187
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
When an agent reads an image file, the streaming event may not have `type='file_read'`. It can arrive with any type (or fall through to the default case in the renderer switch). Without the `renderGeneric` fallback, the image data displays as raw base64 text instead of an `<img>` element.
|
|
227
|
-
|
|
228
|
-
### Event Structure for Image File Reads
|
|
229
|
-
|
|
230
|
-
Two nested structures are used by different agent versions. Both must be handled:
|
|
231
|
-
|
|
232
|
-
**Structure A** (nested under `source`):
|
|
233
|
-
```json
|
|
234
|
-
{
|
|
235
|
-
"type": "<anything>",
|
|
236
|
-
"path": "/path/to/image.png",
|
|
237
|
-
"content": {
|
|
238
|
-
"source": {
|
|
239
|
-
"type": "base64",
|
|
240
|
-
"data": "<base64-string>"
|
|
241
|
-
},
|
|
242
|
-
"media_type": "image/png"
|
|
243
|
-
}
|
|
244
|
-
}
|
|
188
|
+
Steering sends a follow-up prompt to a running agent via stdin JSON-RPC:
|
|
189
|
+
```js
|
|
190
|
+
// conv.steer handler sends to proc.stdin:
|
|
191
|
+
{ jsonrpc: '2.0', id: Date.now(), method: 'session/prompt', params: { sessionId, prompt: [{ type: 'text', text }] } }
|
|
245
192
|
```
|
|
246
193
|
|
|
247
|
-
**
|
|
248
|
-
```json
|
|
249
|
-
{
|
|
250
|
-
"type": "<anything>",
|
|
251
|
-
"path": "/path/to/image.png",
|
|
252
|
-
"content": {
|
|
253
|
-
"type": "base64",
|
|
254
|
-
"data": "<base64-string>"
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
```
|
|
194
|
+
**Process lookup:** `entry.proc` (set by `onProcess` callback on `activeExecutions` entry) OR `activeProcessesByConvId.get(id)`. Check both — race condition between `activeExecutions` being set and `onProcess` firing.
|
|
258
195
|
|
|
259
|
-
**
|
|
260
|
-
```json
|
|
261
|
-
{
|
|
262
|
-
"type": "<anything>",
|
|
263
|
-
"path": "/path/to/image.png",
|
|
264
|
-
"content": "iVBORw0KGgo..."
|
|
265
|
-
}
|
|
266
|
-
```
|
|
267
|
-
Structure C is detected by `detectBase64Image()` which checks magic-byte prefixes (PNG: `iVBORw0KGgo`, JPEG: `/9j/4AAQ`, WebP: `UklGRi`, GIF: `R0lGODlh`).
|
|
268
|
-
|
|
269
|
-
### Two Rendering Paths in streaming-renderer.js
|
|
270
|
-
|
|
271
|
-
**Path 1 – Direct dispatch** (`renderEvent` switch statement):
|
|
272
|
-
- `case 'file_read'` routes directly to `renderFileRead(event)`.
|
|
273
|
-
- Handles all three content structures above.
|
|
196
|
+
**Claude Code stdin:** `supportsStdin: true`, `closeStdin: false` in `lib/claude-runner.js`. Stdin must stay open for steering to work.
|
|
274
197
|
|
|
275
|
-
**
|
|
276
|
-
- Called for any event type not matched by the switch (the `default` case).
|
|
277
|
-
- First thing it does: check for `event.content?.source?.type === 'base64'` OR `event.content?.type === 'base64'` AND `event.path` present.
|
|
278
|
-
- If true, delegates to `renderFileRead(event)` so the image renders correctly.
|
|
279
|
-
- Without this fallback, any file-read event that arrives with an unrecognised type displays as raw key-value text.
|
|
198
|
+
**Process lifetime:** After execution ends, process stays alive 30s (steeringTimeout) for follow-up steers. `conv.steer` resets timeout to another 30s on each steer.
|
|
280
199
|
|
|
281
|
-
|
|
200
|
+
## Execution State Management
|
|
282
201
|
|
|
283
|
-
|
|
284
|
-
1. `
|
|
285
|
-
2.
|
|
286
|
-
3.
|
|
202
|
+
Three parallel state stores (must stay in sync):
|
|
203
|
+
1. **In-memory maps:** `activeExecutions`, `activeProcessesByConvId`, `messageQueues`, `steeringTimeouts`
|
|
204
|
+
2. **Database:** `conversations.isStreaming`, `sessions.status`
|
|
205
|
+
3. **WebSocket clients:** `streamingConversations` Set on each client
|
|
287
206
|
|
|
288
|
-
|
|
207
|
+
**`cleanupExecution(conversationId)`** — atomic cleanup function in server.js. Always use this, never inline-delete from maps. Clears all maps, kills process, cancels timeout, sets DB isStreaming=0.
|
|
289
208
|
|
|
290
|
-
|
|
209
|
+
**Queue drain:** If `processMessageWithStreaming` throws, catch block calls `cleanupExecution` and retries drain after 100ms. Queue never deadlocks.
|
|
291
210
|
|
|
292
|
-
|
|
293
|
-
2. Check `event.type` — if it is not `'file_read'`, the switch default fires `renderGeneric`.
|
|
294
|
-
3. Confirm `renderGeneric` has the base64 fallback guard at the top (search for `content?.source?.type === 'base64'`).
|
|
295
|
-
4. Confirm `renderFileRead` handles both `content.source.data` and `content.data` paths (both exist in the code).
|
|
296
|
-
5. Verify `event.path` is set — the fallback in `renderGeneric` requires `event.path` to delegate correctly.
|
|
297
|
-
6. If `media_type` is missing and content is not PNG/JPEG/WebP/GIF, add it to the event or extend `detectBase64Image` signatures.
|
|
211
|
+
## Message Flow
|
|
298
212
|
|
|
299
|
-
|
|
213
|
+
1. User sends → `startExecution()` checks `streamingConversations.has(convId)`
|
|
214
|
+
2. If NOT streaming: show optimistic "User" message in UI
|
|
215
|
+
3. If streaming: skip optimistic (will queue server-side)
|
|
216
|
+
4. Send via RPC `msg.stream` → backend creates message + broadcasts `message_created`
|
|
217
|
+
5. Backend checks `activeExecutions.has(convId)`:
|
|
218
|
+
- YES: queues, returns `{ queued: true }`, broadcasts `queue_status`
|
|
219
|
+
- NO: executes, returns `{ session }`
|
|
220
|
+
6. Queue items render as yellow control blocks in `queue-indicator` div
|
|
221
|
+
7. `message_created` only broadcast for non-queued messages (ws-handlers-conv.js)
|
|
222
|
+
8. When queued message executes: becomes regular user message, queue-indicator updates
|
|
300
223
|
|
|
301
|
-
|
|
302
|
-
- Attempt 2: Added fallback in `renderGeneric` but checked only `event.content?.source?.type === 'base64'` — missed Structure B where data sits directly on `event.content` (no `source` nesting).
|
|
303
|
-
- Fix: `renderGeneric` now checks both structures before falling through to generic key-value rendering.
|
|
224
|
+
**Streaming session blocks:** `handleStreamingComplete()` removes `.event-streaming-start` and `.event-streaming-complete` DOM blocks to prevent accumulation in long conversations.
|
|
304
225
|
|
|
305
|
-
|
|
226
|
+
## Conversations Sidebar
|
|
306
227
|
|
|
307
|
-
|
|
228
|
+
`ConversationManager` in `static/js/conversations.js`:
|
|
229
|
+
- Polls `/api/conversations` every 30s
|
|
230
|
+
- On poll: if result is non-empty but smaller than cached list, **merges** (keeps cached items not in poll) rather than replacing — prevents transient server responses from dropping conversations
|
|
231
|
+
- On empty result with existing cache: keeps existing (server error assumption)
|
|
232
|
+
- `render()` uses DOM reconciliation by `data-conv-id` — reuses existing nodes, removes orphans
|
|
233
|
+
- `showEmpty()` and `showLoading()` both clear `listEl.innerHTML` — only called when appropriate
|
|
234
|
+
- `conversation_deleted` WS event handled in `setupWebSocketListener` — `deleteConversation()` filters array
|
|
235
|
+
- `confirmDelete()` calls `deleteConversation()` directly AND server broadcasts `conversation_deleted` — double-call is safe (filter is idempotent)
|
|
308
236
|
|
|
309
|
-
|
|
237
|
+
## Base64 Image Rendering in File Read Events
|
|
310
238
|
|
|
311
|
-
|
|
312
|
-
2. **Get Tools Status** - Lists all tools with their current status, versions, and update availability
|
|
313
|
-
3. **WebSocket Connection Test** - Tests real-time event streaming (ping/pong)
|
|
314
|
-
4. **Single Tool Update Test** - Triggers update for a specific tool and monitors completion
|
|
315
|
-
5. **Event Stream Monitoring** - Watches all WebSocket events in real-time
|
|
316
|
-
6. **Database Status** - Checks database accessibility and tool persistence
|
|
317
|
-
7. **System Info** - Displays environment and configuration details
|
|
239
|
+
When an agent reads an image file, the event type may not be `'file_read'`. Three content structures exist:
|
|
318
240
|
|
|
319
|
-
|
|
241
|
+
**Structure A** (nested): `event.content.source.type === 'base64'`, data at `event.content.source.data`
|
|
242
|
+
**Structure B** (flat): `event.content.type === 'base64'`, data at `event.content.data`
|
|
243
|
+
**Structure C** (raw string): `event.content` is a base64 string detected by magic-byte prefix
|
|
320
244
|
|
|
321
|
-
|
|
245
|
+
`renderGeneric` checks for A and B first; if found with `event.path` present, delegates to `renderFileRead`. Without this fallback, non-`file_read` typed image events display as raw text.
|
|
322
246
|
|
|
323
|
-
|
|
247
|
+
MIME type priority: `event.media_type` → magic-byte detection (PNG/JPEG/WebP/GIF) → `application/octet-stream`.
|
|
324
248
|
|
|
325
|
-
|
|
326
|
-
```javascript
|
|
327
|
-
// BEFORE (missing version):
|
|
328
|
-
queries.updateToolStatus(toolId, { status: 'installed', installed_at: Date.now() });
|
|
249
|
+
## Voice Model Download
|
|
329
250
|
|
|
330
|
-
|
|
331
|
-
const version = result.version || null;
|
|
332
|
-
queries.updateToolStatus(toolId, { status: 'installed', version, installed_at: Date.now() });
|
|
333
|
-
```
|
|
251
|
+
Speech models (~470MB total) are downloaded automatically on server startup. No credentials required.
|
|
334
252
|
|
|
335
|
-
|
|
253
|
+
### Download Sources (fallback chain)
|
|
254
|
+
1. **GitHub LFS** (primary): `https://github.com/AnEntrypoint/models`
|
|
255
|
+
2. **HuggingFace** (fallback): `onnx-community/whisper-base` for STT, `AnEntrypoint/sttttsmodels` for TTS
|
|
336
256
|
|
|
337
|
-
###
|
|
257
|
+
### Models
|
|
258
|
+
- **Whisper Base** (~280MB): encoder + decoder ONNX models, tokenizer, config files
|
|
259
|
+
- **TTS Models** (~190MB): mimi encoder/decoder, flow_lm, text_conditioner, tokenizer
|
|
338
260
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
4. Monitor WebSocket events - you should see `tool_update_progress` and `tool_update_complete`
|
|
344
|
-
5. Click "Check Status" to verify version was saved to database
|
|
345
|
-
6. Reload the page - versions should persist
|
|
261
|
+
### UI Behavior
|
|
262
|
+
- Voice tab hidden until models ready; circular progress indicator in header during download
|
|
263
|
+
- Model status broadcast via WebSocket `model_download_progress` events
|
|
264
|
+
- Cache location: `~/.gmgui/models/`
|
|
346
265
|
|
|
347
|
-
|
|
348
|
-
- Individual tool update: version saved ✓
|
|
349
|
-
- Batch tool update: version saved for all tools ✓
|
|
350
|
-
- Database persists across page reload ✓
|
|
351
|
-
- Frontend shows "Up-to-date" or "Update available" ✓
|
|
352
|
-
- Tool install history records the action ✓
|
|
266
|
+
## Performance Notes
|
|
353
267
|
|
|
354
|
-
|
|
268
|
+
- **Static asset serving:** gzip-only (no brotli — too slow for payloads this size). Pre-compressed once on first request, cached in `_assetCache` Map (etag → `{ raw, gz }`). HTML cached as `_htmlCache` after first request, invalidated on hot-reload.
|
|
269
|
+
- **`/api/conversations` N+1 fix:** Uses `getActiveSessionConversationIds()` (single `DISTINCT` query) instead of per-conversation `getSessionsByStatus()` calls.
|
|
270
|
+
- **`conv.chunks` since-filter:** Pushed to DB via `getConversationChunksSince(convId, since)` — no JS array filter on full chunk set.
|
|
271
|
+
- **Client init:** `loadAgents()`, `loadConversations()`, `checkSpeechStatus()` run in parallel via `Promise.all()`.
|
|
272
|
+
- **`perMessageDeflate: false`** on WebSocket server — msgpack binary doesn't compress well, and zlib was blocking the event loop on every streaming_progress send.
|
|
355
273
|
|
|
356
274
|
## ACP SDK Integration
|
|
357
275
|
|
|
358
|
-
|
|
359
|
-
-
|
|
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
|
|
276
|
+
- **@agentclientprotocol/sdk** (`^0.4.1`) added to dependencies
|
|
277
|
+
- Full integration (replacing custom WS protocol) is optional/incremental — current WS already gives logical multiplexing via concurrent async handlers
|
|
380
278
|
|
|
381
|
-
|
|
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
|
|
279
|
+
## Known Gotchas
|
|
387
280
|
|
|
388
|
-
|
|
281
|
+
- **`agent-browser --version`** prints help, not version. Use `-V` flag.
|
|
282
|
+
- **`all_conversations_deleted`** must be in `BROADCAST_TYPES` set in server.js or it won't fan-out to all clients.
|
|
283
|
+
- **`streaming_start` and `message_created`** are high-priority in WSOptimizer — they flush immediately, not batched.
|
|
284
|
+
- **Sidebar animation:** `transition: none !important` in index.html CSS — sidebar snaps instantly on toggle by design.
|
|
285
|
+
- **gm plugin requires no `--dangerously-skip-permissions`** flag in claude-runner.js. That flag disables all plugins.
|
|
286
|
+
- **Tool status race on startup:** `autoProvision()` broadcasts `tool_status_update` for already-installed tools so the UI shows correct state before the first manual fetch.
|
|
287
|
+
- **Thinking blocks** are transient (not in DB), rendered only via `handleStreamingProgress()` in client.js. The `renderEvent` switch case for `thinking_block` is disabled to prevent double-render.
|
|
288
|
+
- **Terminal output** is base64-encoded (`encoding: 'base64'` field on message). Client decodes with `decodeURIComponent(escape(atob(data)))` pattern for multibyte safety.
|
|
289
|
+
- **HTML cache** (`_htmlCache`) is only populated when client accepts gzip. In watch mode it's never cached (always fresh).
|
package/database.js
CHANGED
|
@@ -479,6 +479,10 @@ try {
|
|
|
479
479
|
db.exec('ALTER TABLE sessions ADD COLUMN interrupt TEXT');
|
|
480
480
|
console.log('[Migration] Added interrupt column to sessions');
|
|
481
481
|
}
|
|
482
|
+
if (!sessColsACP.includes('claudeSessionId')) {
|
|
483
|
+
db.exec('ALTER TABLE sessions ADD COLUMN claudeSessionId TEXT');
|
|
484
|
+
console.log('[Migration] Added claudeSessionId column to sessions');
|
|
485
|
+
}
|
|
482
486
|
|
|
483
487
|
// Create ACP tables
|
|
484
488
|
db.exec(`
|
|
@@ -679,9 +683,13 @@ export const queries = {
|
|
|
679
683
|
};
|
|
680
684
|
},
|
|
681
685
|
|
|
682
|
-
setClaudeSessionId(conversationId, claudeSessionId) {
|
|
686
|
+
setClaudeSessionId(conversationId, claudeSessionId, sessionId = null) {
|
|
683
687
|
const stmt = prep('UPDATE conversations SET claudeSessionId = ?, updated_at = ? WHERE id = ?');
|
|
684
688
|
stmt.run(claudeSessionId, Date.now(), conversationId);
|
|
689
|
+
// Also track on the current AgentGUI session so we can clean up all sessions on delete
|
|
690
|
+
if (sessionId) {
|
|
691
|
+
prep('UPDATE sessions SET claudeSessionId = ? WHERE id = ?').run(claudeSessionId, sessionId);
|
|
692
|
+
}
|
|
685
693
|
},
|
|
686
694
|
|
|
687
695
|
getClaudeSessionId(conversationId) {
|
|
@@ -930,6 +938,13 @@ export const queries = {
|
|
|
930
938
|
return stmt.all();
|
|
931
939
|
},
|
|
932
940
|
|
|
941
|
+
getActiveSessionConversationIds() {
|
|
942
|
+
const stmt = prep(
|
|
943
|
+
"SELECT DISTINCT conversationId FROM sessions WHERE status IN ('active', 'pending')"
|
|
944
|
+
);
|
|
945
|
+
return stmt.all().map(r => r.conversationId);
|
|
946
|
+
},
|
|
947
|
+
|
|
933
948
|
getSessionsByConversation(conversationId, limit = 10, offset = 0) {
|
|
934
949
|
const stmt = prep(
|
|
935
950
|
'SELECT * FROM sessions WHERE conversationId = ? ORDER BY started_at DESC LIMIT ? OFFSET ?'
|
|
@@ -1023,9 +1038,14 @@ export const queries = {
|
|
|
1023
1038
|
const conv = this.getConversation(id);
|
|
1024
1039
|
if (!conv) return false;
|
|
1025
1040
|
|
|
1026
|
-
// Delete
|
|
1027
|
-
|
|
1028
|
-
|
|
1041
|
+
// Delete all Claude Code session files for this conversation (all executions)
|
|
1042
|
+
const sessionClaudeIds = prep('SELECT DISTINCT claudeSessionId FROM sessions WHERE conversationId = ? AND claudeSessionId IS NOT NULL').all(id).map(r => r.claudeSessionId);
|
|
1043
|
+
// Also include the current claudeSessionId on the conversation record
|
|
1044
|
+
if (conv.claudeSessionId && !sessionClaudeIds.includes(conv.claudeSessionId)) {
|
|
1045
|
+
sessionClaudeIds.push(conv.claudeSessionId);
|
|
1046
|
+
}
|
|
1047
|
+
for (const csid of sessionClaudeIds) {
|
|
1048
|
+
this.deleteClaudeSessionFile(csid);
|
|
1029
1049
|
}
|
|
1030
1050
|
|
|
1031
1051
|
const deleteStmt = db.transaction(() => {
|
|
@@ -1086,6 +1106,17 @@ export const queries = {
|
|
|
1086
1106
|
}
|
|
1087
1107
|
}
|
|
1088
1108
|
|
|
1109
|
+
// Also delete the session subdirectory (contains subagents/, tool-results/)
|
|
1110
|
+
const sessionDir = path.join(projectPath, sessionId);
|
|
1111
|
+
if (fs.existsSync(sessionDir)) {
|
|
1112
|
+
try {
|
|
1113
|
+
fs.rmSync(sessionDir, { recursive: true, force: true });
|
|
1114
|
+
console.log(`[deleteClaudeSessionFile] Deleted Claude session dir: ${sessionDir}`);
|
|
1115
|
+
} catch (dirErr) {
|
|
1116
|
+
console.error(`[deleteClaudeSessionFile] Failed to delete session dir ${sessionDir}:`, dirErr.message);
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1089
1120
|
return true;
|
|
1090
1121
|
}
|
|
1091
1122
|
}
|
|
@@ -1099,12 +1130,14 @@ export const queries = {
|
|
|
1099
1130
|
|
|
1100
1131
|
deleteAllConversations() {
|
|
1101
1132
|
try {
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1133
|
+
// Delete all Claude session files tracked per-session and per-conversation
|
|
1134
|
+
const allClaudeSessionIds = prep('SELECT DISTINCT claudeSessionId FROM sessions WHERE claudeSessionId IS NOT NULL').all().map(r => r.claudeSessionId);
|
|
1135
|
+
const convClaudeIds = prep('SELECT DISTINCT claudeSessionId FROM conversations WHERE claudeSessionId IS NOT NULL').all().map(r => r.claudeSessionId);
|
|
1136
|
+
for (const csid of convClaudeIds) {
|
|
1137
|
+
if (!allClaudeSessionIds.includes(csid)) allClaudeSessionIds.push(csid);
|
|
1138
|
+
}
|
|
1139
|
+
for (const csid of allClaudeSessionIds) {
|
|
1140
|
+
this.deleteClaudeSessionFile(csid);
|
|
1108
1141
|
}
|
|
1109
1142
|
|
|
1110
1143
|
const deleteAllStmt = db.transaction(() => {
|
|
@@ -1477,6 +1510,18 @@ export const queries = {
|
|
|
1477
1510
|
});
|
|
1478
1511
|
},
|
|
1479
1512
|
|
|
1513
|
+
getConversationChunksSince(conversationId, since) {
|
|
1514
|
+
const stmt = prep(
|
|
1515
|
+
`SELECT id, sessionId, conversationId, sequence, type, data, created_at
|
|
1516
|
+
FROM chunks WHERE conversationId = ? AND created_at > ? ORDER BY created_at ASC`
|
|
1517
|
+
);
|
|
1518
|
+
const rows = stmt.all(conversationId, since);
|
|
1519
|
+
return rows.map(row => {
|
|
1520
|
+
try { return { ...row, data: typeof row.data === 'string' ? JSON.parse(row.data) : row.data }; }
|
|
1521
|
+
catch (e) { return row; }
|
|
1522
|
+
});
|
|
1523
|
+
},
|
|
1524
|
+
|
|
1480
1525
|
getRecentConversationChunks(conversationId, limit = 500) {
|
|
1481
1526
|
const stmt = prep(
|
|
1482
1527
|
`SELECT id, sessionId, conversationId, sequence, type, data, created_at
|
package/docs/index.html
CHANGED
|
@@ -246,7 +246,7 @@ $ npm run dev</pre>
|
|
|
246
246
|
<a href="https://github.com/AnEntrypoint/agentgui/issues" class="text-content2 hover:text-content1 transition-colors">Issues</a>
|
|
247
247
|
<a href="https://github.com/AnEntrypoint/agentgui#readme" class="text-content2 hover:text-content1 transition-colors">Documentation</a>
|
|
248
248
|
</div>
|
|
249
|
-
<p class="text-content3 text-sm">MIT License © 2025 AnEntrypoint · Made with ❤️ by
|
|
249
|
+
<p class="text-content3 text-sm">MIT License © 2025 AnEntrypoint · Made with ❤️ by AnEntrypoint</p>
|
|
250
250
|
</div>
|
|
251
251
|
</footer>
|
|
252
252
|
|
package/lib/claude-runner.js
CHANGED
|
@@ -10,8 +10,9 @@ function getSpawnOptions(cwd, additionalOptions = {}) {
|
|
|
10
10
|
if (!options.env) {
|
|
11
11
|
options.env = { ...process.env };
|
|
12
12
|
}
|
|
13
|
-
//
|
|
14
|
-
//
|
|
13
|
+
// Remove CLAUDECODE so claude doesn't refuse to run inside another Claude Code session.
|
|
14
|
+
// The gm plugin still works — claude sets CLAUDECODE itself once it starts.
|
|
15
|
+
delete options.env.CLAUDECODE;
|
|
15
16
|
return options;
|
|
16
17
|
}
|
|
17
18
|
|
|
@@ -621,8 +622,8 @@ registry.register({
|
|
|
621
622
|
name: 'Claude Code',
|
|
622
623
|
command: 'claude',
|
|
623
624
|
protocol: 'direct',
|
|
624
|
-
supportsStdin:
|
|
625
|
-
closeStdin:
|
|
625
|
+
supportsStdin: true, // keep stdin open for JSON-RPC steering commands
|
|
626
|
+
closeStdin: false, // must stay open for steering to work
|
|
626
627
|
useJsonRpcStdin: false,
|
|
627
628
|
supportedFeatures: ['streaming', 'resume', 'system-prompt', 'permissions-skip', 'steering'],
|
|
628
629
|
spawnEnv: { MAX_THINKING_TOKENS: '0', AGENTGUI_SUBPROCESS: '1' },
|
package/lib/codec.js
CHANGED
|
@@ -1,53 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Binary codec for WebSocket messages.
|
|
3
|
-
* Wraps msgpackr for framing + GPT tokenizer BPE compression for large text fields.
|
|
4
|
-
*
|
|
5
|
-
* Wire format: msgpackr-packed object where string fields > THRESHOLD chars
|
|
6
|
-
* are replaced with { __tok: true, d: Uint32Array } — tokenized and decompressed
|
|
7
|
-
* transparently on both sides.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
1
|
import { pack, unpack } from 'msgpackr';
|
|
11
|
-
import { encode as tokEncode, decode as tokDecode } from 'gpt-tokenizer';
|
|
12
|
-
|
|
13
|
-
const THRESHOLD = 200; // bytes before compression is worthwhile
|
|
14
|
-
const COMPRESSIBLE = new Set(['content', 'text', 'output', 'response', 'prompt', 'input', 'data']);
|
|
15
|
-
|
|
16
|
-
function compressText(str) {
|
|
17
|
-
return { __tok: true, d: tokEncode(str) };
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function decompressText(val) {
|
|
21
|
-
return tokDecode(val.d);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function encodeObj(obj) {
|
|
25
|
-
if (!obj || typeof obj !== 'object') return obj;
|
|
26
|
-
if (Array.isArray(obj)) return obj.map(encodeObj);
|
|
27
|
-
const out = {};
|
|
28
|
-
for (const k of Object.keys(obj)) {
|
|
29
|
-
const v = obj[k];
|
|
30
|
-
if (COMPRESSIBLE.has(k) && typeof v === 'string' && v.length > THRESHOLD) {
|
|
31
|
-
out[k] = compressText(v);
|
|
32
|
-
} else if (v && typeof v === 'object' && !ArrayBuffer.isView(v)) {
|
|
33
|
-
out[k] = encodeObj(v);
|
|
34
|
-
} else {
|
|
35
|
-
out[k] = v;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
return out;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function decodeObj(obj) {
|
|
42
|
-
if (!obj || typeof obj !== 'object') return obj;
|
|
43
|
-
if (Array.isArray(obj)) return obj.map(decodeObj);
|
|
44
|
-
if (obj.__tok && obj.d) return decompressText(obj);
|
|
45
|
-
const out = {};
|
|
46
|
-
for (const k of Object.keys(obj)) {
|
|
47
|
-
out[k] = decodeObj(obj[k]);
|
|
48
|
-
}
|
|
49
|
-
return out;
|
|
50
|
-
}
|
|
51
2
|
|
|
52
|
-
export function encode(obj) { return pack(
|
|
53
|
-
export function decode(buf) { return
|
|
3
|
+
export function encode(obj) { return pack(obj); }
|
|
4
|
+
export function decode(buf) { return unpack(buf instanceof Uint8Array ? buf : new Uint8Array(buf)); }
|