agentgui 1.0.675 → 1.0.677

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
@@ -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 (line 1986 in server.js). This ensures database persistence across page reloads.
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` lines 26-87):
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` lines 168-199):
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
- The system auto-detects installed AI coding tools via `bun x` package resolution:
121
- - **OpenCode**: `opencode-ai` package (id: gm-oc)
122
- - **Gemini CLI**: `@google/gemini-cli` package (id: gm-gc)
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
- Tool configuration in `lib/tool-manager.js` TOOLS array includes id, name, pkg, and pluginId. Each tool has a different plugin folder name than its npm package name:
127
- - Claude Code: pkg='@anthropic-ai/claude-code', pluginId='gm' (stored at ~/.claude/plugins/gm/)
128
- - Gemini CLI: pkg='@google/gemini-cli', pluginId='gm' (stored at ~/.gemini/extensions/gm/)
129
- - Kilo: pkg='@kilocode/cli', pluginId='@kilocode/cli' (stored at ~/.config/kilo/agents/@kilocode/cli/)
130
- - OpenCode: pkg='opencode-ai', pluginId='opencode-ai' (stored at ~/.config/opencode/agents/opencode-ai/)
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
- Detection happens by spawning `bun x <package> --version` to check if tools are installed. Version detection uses pluginId to find the correct plugin.json file. Response from `/api/tools` includes: id, name, pkg, installed, status (one of: installed|needs_update|not_installed), isUpToDate, upgradeNeeded, hasUpdate. Frontend displays tools in UI and updates based on installation status.
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
- - Immediately updates tool status to 'installing'/'updating' and re-renders UI
140
- - Sends POST request to `/api/tools/{id}/install` or `/api/tools/{id}/update`
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
- ## Voice Model Download
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
- ### Testing
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
- ## Base64 Image Rendering in File Read Events
223
-
224
- ### Problem: Images Displaying as Raw Text
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
- **Structure B** (flat inside `content`):
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
- **Structure C** (content is raw base64 string, no wrapping object):
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
- **Path 2 Generic fallback** (`renderGeneric`):
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
- ### MIME Type Resolution in renderFileRead
200
+ ## Execution State Management
282
201
 
283
- Priority order inside `renderFileRead`:
284
- 1. `event.media_type` field (explicit)
285
- 2. Detected from magic bytes via `detectBase64Image()` → maps `jpeg` → `image/jpeg`, others → `image/<type>`
286
- 3. Falls back to `application/octet-stream` (shows broken image)
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
- Always include `media_type` on the event when possible. If absent, magic-byte detection covers PNG/JPEG/WebP/GIF automatically.
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
- ### Debugging Checklist When Images Show as Text
209
+ **Queue drain:** If `processMessageWithStreaming` throws, catch block calls `cleanupExecution` and retries drain after 100ms. Queue never deadlocks.
291
210
 
292
- 1. `console.log(event)` the raw event object arriving at the renderer — verify `content` structure.
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
- ### Why Two Attempts Failed Before the Fix
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
- - Attempt 1: Modified only `renderFileRead` but the event had an unrecognised type so the switch never reached `renderFileRead`.
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
- ## Tool Update Testing & Diagnostics
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
- A comprehensive diagnostic page is available at `http://localhost:3000/gm/tool-update-test.html` (`static/tool-update-test.html`) with 7 interactive test sections:
237
+ ## Base64 Image Rendering in File Read Events
310
238
 
311
- 1. **API Connection Test** - Verifies server HTTP connectivity
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
- ### Batch Update Fix (Critical)
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
- **Issue:** When updating all tools via `/api/tools/update` endpoint, tool versions were not persisted to the database because the `version` parameter was missing from the `updateToolStatus` call.
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
- **Location:** `server.js` line 1986 in the batch update handler (`/api/tools/update`)
247
+ MIME type priority: `event.media_type` magic-byte detection (PNG/JPEG/WebP/GIF) → `application/octet-stream`.
324
248
 
325
- **Fix Applied:**
326
- ```javascript
327
- // BEFORE (missing version):
328
- queries.updateToolStatus(toolId, { status: 'installed', installed_at: Date.now() });
249
+ ## Voice Model Download
329
250
 
330
- // AFTER (version preserved):
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
- **Impact:** Ensures tool versions are correctly saved after batch updates, enabling the UI to display accurate version information and update status across page reloads.
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
- ### Testing Tool Updates
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
- **Manual Steps:**
340
- 1. Open `http://localhost:3000/gm/tool-update-test.html`
341
- 2. Click "Get Tools List" and note current versions
342
- 3. Click "Start Update" for a tool (e.g., gm-cc)
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
- **Expected Outcomes:**
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
- ### Current Status
359
- - **@agentclientprotocol/sdk** (`^0.4.1`) has been added to dependencies
360
- - The SDK is positioned as the main protocol for client-server and server-ACP tools communication
361
-
362
- ### Clear All Conversations Fix
363
-
364
- **Issue:** After clicking "Clear All Conversations", the conversation threads would reappear in the sidebar.
365
-
366
- **Root Cause:** The `all_conversations_deleted` broadcast event was being sent by the server (in `lib/ws-handlers-conv.js`), but:
367
- 1. The event type was not in the `BROADCAST_TYPES` set in `server.js`, so it wasn't being broadcast to all clients
368
- 2. The conversation manager (`static/js/conversations.js`) had no handler for this event type
369
- 3. Client cleanup in `handleAllConversationsDeleted` was incomplete
370
-
371
- **Solution Applied:**
372
- 1. Added `'all_conversations_deleted'` to `BROADCAST_TYPES` set (server.js:4147)
373
- 2. Added event handler in conversation manager to clear all local state (conversations.js:573-577)
374
- 3. Enhanced client cleanup to clear all caches and state before reloading (client.js:1321-1330)
375
-
376
- **Files Modified:**
377
- - `server.js`: Added `all_conversations_deleted` to BROADCAST_TYPES
378
- - `static/js/conversations.js`: Added handler for all_conversations_deleted event
379
- - `static/js/client.js`: Enhanced handleAllConversationsDeleted with complete state cleanup
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
- ### Next Steps for Full ACP SDK Integration
382
- The ACP SDK dependency has been added. Full integration would involve:
383
- 1. Replacing custom WebSocket protocol with ACP SDK's RPC/messaging layer
384
- 2. Updating `lib/acp-manager.js` to use ACP SDK for ACP tool communication
385
- 3. Migrating `lib/ws-protocol.js` handlers to use ACP SDK message types
386
- 4. Updating client-side WebSocket handlers to work with ACP SDK events
279
+ ## Known Gotchas
387
280
 
388
- This refactoring is optional and can be done incrementally as needed.
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 associated Claude Code session file if it exists
1027
- if (conv.claudeSessionId) {
1028
- this.deleteClaudeSessionFile(conv.claudeSessionId);
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
- const conversations = prep('SELECT id, claudeSessionId FROM conversations').all();
1103
-
1104
- for (const conv of conversations) {
1105
- if (conv.claudeSessionId) {
1106
- this.deleteClaudeSessionFile(conv.claudeSessionId);
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 the AgentGUI team</p>
249
+ <p class="text-content3 text-sm">MIT License © 2025 AnEntrypoint · Made with ❤️ by AnEntrypoint</p>
250
250
  </div>
251
251
  </footer>
252
252
 
@@ -10,8 +10,9 @@ function getSpawnOptions(cwd, additionalOptions = {}) {
10
10
  if (!options.env) {
11
11
  options.env = { ...process.env };
12
12
  }
13
- // Keep CLAUDECODE for plugin support (gm plugin requires this)
14
- // Don't delete it as it prevents plugins from loading
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: false, // prompt passed as positional arg, not stdin
625
- closeStdin: true, // close stdin so claude doesn't wait for input in --print mode
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(encodeObj(obj)); }
53
- export function decode(buf) { return decodeObj(unpack(buf instanceof Uint8Array ? buf : new Uint8Array(buf))); }
3
+ export function encode(obj) { return pack(obj); }
4
+ export function decode(buf) { return unpack(buf instanceof Uint8Array ? buf : new Uint8Array(buf)); }