Skip to content

Latest commit

 

History

History
382 lines (294 loc) · 34.1 KB

File metadata and controls

382 lines (294 loc) · 34.1 KB

AgentGUI

Multi-agent GUI client for AI coding agents (Claude Code, Gemini CLI, OpenCode, Goose, etc.) with real-time streaming, WebSocket sync, and SQLite persistence.

Running

npm install
npm run dev        # node server.js --watch

Server starts on http://localhost:3000, redirects to /gm/.

Architecture

server.js              Bootstrap: imports, constants, wiring of all factories; delegates to lib/ modules for HTTP handler, WS setup, routes, startup
database.js            SQLite connection, schema DDL, migrations (queries extracted to lib/db-queries.js)
acp-queries.js         ACP query helpers (UUID, timestamp, JSON utilities)
bin/gmgui.cjs          CLI entry point (npx agentgui / bun x agentgui)

lib/claude-runner.js   Agent framework - AgentRunner/AgentRegistry classes, direct protocol execution
lib/acp-runner.js      ACP JSON-RPC session lifecycle (init, session/new, prompt, drain)
lib/acp-protocol.js    ACP session/update message normalization (shared by all ACP agents)
lib/acp-sdk-manager.js ACP tool lifecycle - on-demand start opencode/kilo/codex, health checks, idle timeout
lib/acp-server-machine.js  XState v5 machine per ACP tool: stopped/starting/running/crashed/restarting states
lib/agent-discovery.js Agent binary detection (findCommand), ACP server query, discoverAgents, CLI wrapper logic
lib/agent-descriptors.js  Data-driven ACP agent descriptor builder
lib/checkpoint-manager.js  Session recovery - load checkpoints, inject into resume flow, idempotency
lib/codec.js           msgpack encode/decode (pack/unpack wrappers)
lib/db-queries.js      All 88 query functions (createQueries factory, extracted from database.js)
lib/execution-machine.js   XState v5 machine per conversation: idle/streaming/draining/rate_limited states
lib/gm-agent-configs.js    GM agent configuration and spawning
lib/jsonl-parser.js    JSONL event parsing, session tracking, streaming state (extracted from jsonl-watcher.js)
lib/jsonl-watcher.js   Watches ~/.claude/projects for JSONL file changes, delegates parsing to jsonl-parser.js
lib/oauth-common.js    Shared OAuth helpers (buildBaseUrl, isRemoteRequest, encodeOAuthState, result/relay pages)
lib/oauth-gemini.js    Gemini OAuth flow (credential discovery, token exchange, callback handling)
lib/oauth-codex.js     Codex CLI OAuth flow (PKCE S256, token exchange, callback handling)
lib/plugin-loader.js   Plugin discovery and loading (EventEmitter-based)
lib/pm2-manager.js     PM2 process management wrapper
lib/speech.js          Speech-to-text and text-to-speech via @huggingface/transformers
lib/speech-manager.js  TTS orchestration (eager TTS, voice cache, model download, broadcastModelProgress)
lib/tool-install-machine.js  XState v5 machine per tool: unchecked/checking/idle/installing/installed/updating/needs_update/failed states
lib/tool-manager.js    Tool facade - re-exports from tool-version-check, tool-version-fetch, tool-spawner, tool-provisioner
lib/tool-version-check.js  Sync/local version detection: BIN_MAP, FRAMEWORK_PATHS, checkCliInstalled, getCliVersion, checkToolInstalled, compareVersions, getInstalledVersion
lib/tool-version-fetch.js  Async/network version functions: getPublishedVersion, fetchPublishedVersion, clearVersionCache, checkToolViaBunx
lib/tool-spawner.js    npm/bun install/update spawn with timeout and heartbeat
lib/tool-provisioner.js  Auto-provisioning and periodic update checking
lib/routes-speech.js   Speech/TTS HTTP route handlers (stt, tts, voices, speech-status)
lib/routes-oauth.js    OAuth HTTP route handlers (gemini-oauth/*, codex-oauth/*)
lib/routes-tools.js    Tool management HTTP route handlers (list, install, update, history, refresh)
lib/routes-util.js     Utility HTTP route handlers (clone, folders, git, home, version, import)
lib/routes-agents.js   Agent list/search/auth-status/descriptor/models HTTP route handlers
lib/routes-conversations.js  Conversation CRUD HTTP route handlers (list, create, get, update, delete, archive, restore)
lib/routes-messages.js  Message/stream/queue HTTP route handlers (GET+POST messages, stream, queue CRUD)
lib/routes-sessions.js  Session/chunk/full/execution HTTP route handlers (session get, chunks, full load, execution events)
lib/routes-runs.js     Runs HTTP route handlers (POST /api/runs, runs search, run by id, wait, cancel, thread run cancel/wait)
lib/routes-scripts.js  Scripts/cancel/resume/inject HTTP route handlers (conversation scripts, run-script, stop-script, cancel, resume, inject)
lib/routes-agent-actions.js  Agent auth and update HTTP route handlers (POST /api/agents/:id/auth, POST /api/agents/:id/update)
lib/routes-auth-config.js  Auth config HTTP route handlers (GET /api/auth/configs, POST /api/auth/save-config)
lib/routes-upload.js   Express sub-app: POST /api/upload/:conversationId (Busboy file upload) + GET /files/:conversationId fsbrowse router; createExpressApp(deps) factory
lib/http-utils.js       HTTP utility functions (parseBody, acceptsEncoding, compressAndSend, sendJSON)
lib/http-handler.js    Main HTTP request handler factory: createHttpHandler(deps) => async (req,res); rate limiting, auth, CORS, route dispatch, static file serving
lib/provider-config.js  Provider config helpers (buildSystemPrompt, maskKey, getProviderConfigs, saveProviderConfig, PROVIDER_CONFIGS)
lib/server-utils.js     Server utility functions (logError, errLogPath, makeCleanupExecution, makeGetModelsForAgent)
lib/routes-registry.js  Route + WS handler registration: createRegistry(wsRouter, deps) => _routes; wires all HTTP route _match objects and all WS handler registrations
lib/ws-setup.js        WebSocket server setup: createWsSetup(server, deps) => { wss, hotReloadClients }; connection auth, client tracking, watch file reload, heartbeat
lib/ws-legacy-handlers.js  Legacy WS message handler: subscribe/unsubscribe/terminal PTY/pm2 commands; called from ws-setup.js onLegacy
lib/server-startup.js  Server startup: createOnServerReady(deps) => { onServerReady, getJsonlWatcher }; tools, ACP, speech, PM2 monitoring init
lib/server-startup2.js  Startup helpers: createAutoImport (hasIndexFilesChanged + performAutoImport), createDbRecovery (orphaned session cleanup), createPluginLoader (plugin extension loading)
lib/routes-debug.js    Debug/backup/restore/ws-stats HTTP route handlers
lib/routes-threads.js  Thread CRUD HTTP route handlers (ACP v0.2.3 thread API)
lib/ws-protocol.js     WebSocket RPC router (WsRouter class)
lib/ws-optimizer.js    Per-client priority queue for WS event batching
lib/ws-handlers-conv.js  Conversation CRUD, chunks, cancel, steer, inject RPC handlers
lib/ws-handlers-msg.js   Message send/stream/list RPC handlers + execution start/enqueue
lib/ws-handlers-queue.js Queue list/delete/update RPC handlers
lib/ws-handlers-session.js  Session/agent RPC handlers
lib/ws-handlers-run.js  Thread/run RPC handlers
lib/ws-handlers-util.js  Utility RPC handlers (speech, auth, git, tools, voice)
lib/ws-handlers-oauth.js  Gemini + Codex OAuth WS RPC handlers
lib/ws-handlers-scripts.js  npm script run/stop WS RPC handlers
lib/plugins/           Server plugins (acp, agents, auth, database, files, git, speech, stream, tools, websocket, workflow)

static/index.html      Main HTML shell
static/app.js          UI IIFEs (sidebar search, error boundary, import, archived view, presets)
static/js/app-shortcuts.js  Keyboard shortcuts overlay
static/theme.js        Theme switching
static/css/main.css              All application styles (extracted from index.html)
static/css/tools-popup.css       Tool popup styles
static/js/client.js    AgentGUIClient class (constructor + _dbg + init); instantiation at bottom
static/js/client-ws.js          WebSocket listeners, _convIsStreaming, _setConvStreaming, setupRendererListeners, restoreStateFromUrl, isValidId (prototype extension)
static/js/client-url.js         URL/scroll helpers: updateUrlForConversation, saveScrollPosition, restoreScrollPosition, setupScrollTracking (prototype extension)
static/js/client-ui.js          setupUI (modified, calls _setupUIButtonEvents/_setupUIWindowEvents) + setupChatMicButton (prototype extension)
static/js/client-ui-controls.js _setupUIButtonEvents + _setupUIWindowEvents extracted helpers (prototype extension)
static/js/client-ws-msg.js      connectWebSocket, handleWebSocketMessage, queueEvent (prototype extension)
static/js/client-streaming.js   handleStreamingStart (prototype extension)
static/js/client-streaming2.js  handleStreamingResumed, handleStreamingProgress, _handleStreamingProgressInner (prototype extension)
static/js/client-streaming3.js  renderBlockContent, scrollToBottom, _showNewContentPill, _removeNewContentPill, handleStreamingError (prototype extension)
static/js/client-streaming4.js  handleStreamingComplete, _promptPushIfWeOwnRemote, handleConversationCreated, handleMessageCreated, queue handlers (prototype extension)
static/js/client-events.js      fetchAndRenderQueue, handleRateLimitHit/Clear, handleAllConversationsDeleted, isHtmlContent, sanitizeHtml, parseMarkdownCodeBlocks (prototype extension)
static/js/client-render.js      renderCodeBlock, renderMessageContent (prototype extension)
static/js/client-exec.js        startExecution, optimistic message helpers, _subscribeToConversationUpdates, _flushBgCache (prototype extension)
static/js/client-helpers.js     _recoverMissedChunks, cache/placeholder/height/countdown/debug helpers, showLoadingSpinner/hideLoadingSpinner (prototype extension)
static/js/client-ui2.js         _showWelcomeScreen, _showSkeletonLoading, streamToConversation, _hydrateSessionBlocks (prototype extension)
static/js/client-conv.js        _getLazyObserver, _renderConversationContent, renderChunk, _renderChunkInner, loadAgents, loadSubAgentsForCli (prototype extension)
static/js/client-agents.js      checkSpeechStatus, loadModelsForAgent, _populateModelSelector, lock/unlockAgentAndModel, applyAgentAndModelSelection, loadConversations, updateConnectionStatus (prototype extension)
static/js/client-status.js      _updateConnectionIndicator, _handleModelDownloadProgress, _handleTTSSetupProgress, _toggleConnectionTooltip, updateMetrics, controls, toggleTheme, createNewConversation (prototype extension)
static/js/client-cache.js       cacheCurrentConversation, invalidateCache, loadConversationMessages (prototype extension)
static/js/client-load.js        _makeLoadRequest, _verifyRequestId, _completeLoadRequest, _loadConvRender (prototype extension)
static/js/client-scroll.js      syncPromptState, updateBusyPromptArea, removeScrollUpDetection, setupScrollUpDetection (prototype extension)
static/js/client-utils.js       renderMessagesFragment, renderMessages, escapeHtml, showError, on, emit, agent/model getters, draft/prompt helpers, destroy (prototype extension)
static/js/conversations.js       Conversation management (class definition)
static/js/conv-list-renderer.js  Conversation list render, CRUD, WS listener (prototype extension)
static/js/conv-sidebar-actions.js  Sidebar delegated listeners, folder browser (prototype extension)
static/js/conv-sidebar-clone.js  Delete-all, clone UI, DOM-ready bootstrap (prototype extension)
static/js/streaming-renderer.js  Renders agent streaming events as HTML
static/js/event-processor.js     Processes incoming events
static/js/event-filter-config.js Filters events by type
static/js/websocket-manager.js   WebSocket send/subscribe/disconnect methods (prototype extension)
static/js/ws-core.js             WebSocketManager class + connect/reconnect/heartbeat core
static/js/ws-latency.js          WebSocket latency tracking, ping/pong, quality tiers (prototype extension)
static/js/ws-client.js           WsClient RPC wrapper over WebSocketManager
static/js/ui-components.js       UI component helpers (modal, tabs, alert, spinner, progress, collapsible)
static/js/ui-components-rendering.js  Input/select/button/badge factory helpers (static extension)
static/js/syntax-highlighter.js  Code syntax highlighting (class definition)
static/js/syntax-highlighter-render.js  Token-to-HTML render logic (prototype extension)
static/js/voice.js               Voice input/output
static/js/stt-handler.js         Speech-to-text recording and upload
static/js/features.js            View toggle, drag-drop upload, model progress indicator
static/js/tools-manager.js       Tool install/update UI orchestrator
static/js/tools-manager-ui.js    Tool card rendering + voice selector helpers
static/js/agent-auth.js          Agent authentication UI (dropdown, auth-status, provider keys)
static/js/agent-auth-oauth.js    OAuth modal functions (triggerAuth, onWsMessage, paste fallback)
static/js/dialogs.js             Modal dialog system (class definition)
static/js/dialogs-types.js       Dialog type-specific render helpers (prototype extension)
static/js/image-loader.js        Lazy image loading orchestration for agent file read events
static/js/image-loader-element.js  ImageLoader DOM element rendering (prototype extension)
static/js/pm2-monitor.js         PM2 process monitor UI
static/js/script-runner.js       npm script runner UI
static/js/state-barrier.js       Atomic state machine for conversation management
static/js/terminal.js            xterm.js terminal integration
static/js/ws-machine.js          XState v5 WS connection machine: disconnected/connecting/connected/reconnecting
static/js/conv-machine.js        XState v5 per-conversation UI machine: idle/streaming/queued
static/js/tool-install-machine.js  XState v5 per-tool UI install machine: idle/installing/installed/updating/needs_update/failed
static/js/voice-machine.js       XState v5 voice/TTS machine: idle/queued/speaking/disabled (circuit-breaker)
static/js/conv-list-machine.js   XState v5 conversation list machine: unloaded/loading/loaded/error
static/js/prompt-machine.js      XState v5 prompt area machine: ready/loading/streaming/queued/disabled
static/lib/xstate.umd.min.js     XState v5 browser bundle (UMD, served locally from node_modules)
static/lib/msgpackr.min.js       msgpack browser bundle
static/lib/webjsx.js             WebJSX library
static/vendor/                   Third-party assets (highlight.js, Prism, RippleUI, xterm.js)

XState State Machines

XState v5 machines own their domains exclusively. No ad-hoc Maps/Sets parallel to machines.

Server (lib/): execution-machine (per conversation: idle/streaming/draining/rate_limited), acp-server-machine (per tool: stopped/starting/running/crashed/restarting), tool-install-machine (per tool: unchecked→checking→idle/installed/needs_update/installing/updating/failed). API: send(id, event), isLocked(), snapshots at GET /api/debug/machines when DEBUG=1.

Client (static/js/, UMD): ws-machine (disconnected/connecting/connected/reconnecting), conv-machine (per conv: idle/streaming/queued), tool-install-machine (per tool), voice-machine (single: idle/queued/speaking/disabled circuit-breaker), conv-list-machine (single: unloaded/loading/loaded/error), prompt-machine (single: ready/loading/streaming/queued/disabled). Load order: xstate.umd.min.js → ws-machine → conv-machine → tool-install-machine → voice-machine → conv-list-machine → prompt-machine. Exposed at window.__* globals for debug.

Key Details

  • Express is used only for file upload (/api/upload/:conversationId) and fsbrowse file browser (/files/:conversationId). All other routes use raw http.createServer with manual routing.
  • Agent discovery scans PATH for known CLI binaries (claude, opencode, gemini, goose, etc.) at startup.
  • Database lives at ~/.gmgui/data.db. Tables: conversations, messages, events, sessions, stream chunks.
  • WebSocket endpoint is at BASE_URL + /sync. Supports subscribe/unsubscribe by sessionId or conversationId, and ping.
  • All WS RPC uses msgpack binary encoding (lib/codec.js). Wire format: { r, m, p } request, { r, d } reply, { type, seq } broadcast push.
  • 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.
  • Static assets use Cache-Control: public, no-cache + ETag. Browser always revalidates (sends If-None-Match), server returns 304 if unchanged. Compressed once on first request, served from RAM (_assetCache Map keyed by etag).
  • Deployment: runs behind Traefik/Caddy which handles TLS and can support WebTransport/QUIC.

Environment Variables

  • PORT - Server port (default: 3000)
  • BASE_URL - URL prefix (default: /gm)
  • STARTUP_CWD - Working directory passed to agents
  • HOT_RELOAD - Set to "false" to disable watch mode
  • CODEX_HOME - Override Codex CLI home directory (default: ~/.codex)
  • RATE_LIMIT_MAX - Max HTTP requests per IP per minute (default: 300)
  • PASSWORD - Basic auth password for all HTTP routes (optional)
  • AGENTGUI_BASE_URL - Override base URL for OAuth callbacks (e.g., https://myserver.com)

ACP Tool Lifecycle

On startup, agentgui auto-launches bundled ACP tools (opencode, kilo) as HTTP servers:

  • OpenCode: port 18100 (opencode acp --port 18100)
  • Kilo: port 18101 (kilo acp --port 18101)

Managed by lib/acp-sdk-manager.js. Features: crash restart with exponential backoff (max 10 in 5min), health checks every 30s via GET /provider, clean shutdown on SIGTERM. The acpPort field on discovered agents is set automatically once healthy. Models are queried from the running ACP HTTP servers via their /provider endpoint.

REST API

All routes prefixed with BASE_URL (default /gm). Key endpoints:

Conversations: GET /api/conversations, POST /api/conversations, GET/POST/DELETE /api/conversations/:id, POST /api/conversations/:id/archive, POST /api/conversations/:id/restore, GET /api/conversations/:id/messages, POST /api/conversations/:id/messages, POST /api/conversations/:id/stream, GET /api/conversations/:id/full, GET /api/conversations/:id/chunks, GET /api/conversations/:id/sessions/latest

Sessions: GET /api/sessions/:id, GET /api/sessions/:id/chunks, GET /api/sessions/:id/execution

Agents & ACP: GET /api/agents, GET /api/acp/status, GET /api/health

Speech: POST /api/stt, POST /api/tts, GET /api/speech-status

Tools: GET /api/tools, GET/POST /api/tools/:id/install, POST /api/tools/:id/update, GET /api/tools/:id/history, POST /api/tools/update, POST /api/tools/refresh-all

OAuth: POST /api/codex-oauth/start, GET /api/codex-oauth/status, POST /api/codex-oauth/relay, POST /api/codex-oauth/complete, GET /codex-oauth2callback

Utility: POST /api/folders, GET /api/home

Tool Update System

Tool updates are managed through a complete pipeline:

Update Flow:

  1. Frontend (static/js/tools-manager.js) initiates POST to /api/tools/{id}/update
  2. Server (server.js lines 1904-1961 for individual, 1973-2003 for batch) spawns bun x process
  3. Tool manager (lib/tool-manager.js lines 400-432) executes bun x <package> and detects new version
  4. Version is saved to database: queries.updateToolStatus(toolId, { version, status: 'installed' })
  5. WebSocket broadcasts tool_update_complete with version and status data
  6. Frontend machine transitions to installed/failed via WS event, UI re-renders from machine state

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.

Version Detection Sources (lib/tool-manager.js):

  • Claude Code: ~/.claude/plugins/{pluginId}/plugin.json
  • OpenCode: ~/.config/opencode/agents/{pluginId}/plugin.json
  • Gemini CLI: ~/.gemini/extensions/{pluginId}/plugin.json
  • Kilo: ~/.config/kilo/agents/{pluginId}/plugin.json

Database Schema (database.js):

  • Table: tool_installations (toolId, version, status, installed_at, error_message)
  • Table: tool_install_history (action, status, error_message for audit trail)

Tool Detection System

TOOLS array in lib/tool-manager.js: cli (via which + --version) or plugin (via plugin.json). Current: claude, opencode, gemini, kilo, codex, agent-browser (uses -V, not --version), + plugin tools (gm-cc, gm-oc, gm-gc, gm-kilo, gm-codex).

BIN_MAP: Single constant in lib/tool-version-check.js shared by detect + version functions; new CLI tools must be added.

FRAMEWORK_PATHS: Data table (pluginDir/versionFile/parseVersion/optional markerFile). New framework = one table entry.

Provisioning: autoProvision() at startup (~10s), startPeriodicUpdateCheck() every 6h. Both broadcast tool status via WS.

Tool Installation and Update UI Flow

When user clicks Install/Update button on a tool:

  1. Frontend (static/js/tools-manager.js): Sends INSTALL/UPDATE event to toolInstallMachineAPI, sends POST. Machine guards duplicate requests via isLocked().
  2. Backend (server.js): tool-install-machine.js sends INSTALL_START/UPDATE_START, runs async, sends INSTALL_COMPLETE/UPDATE_COMPLETE/FAILED. Broadcasts WS events.
  3. Frontend WebSocket Handler: Sends COMPLETE/FAILED/PROGRESS to machine. UI renders from machine state only.

WebSocket Protocol

Endpoint: BASE_URL + /sync. Msgpack binary. Wire: RPC request {r, m, p}, reply {r, d} or {r, e}, broadcast {type, seq, ...} batched by WSOptimizer. Per-client priority queue: high-priority (streaming_start, message_created, streaming_complete) flush immediately; normal/low batch by latency tier. Rate limit: 100 msg/sec (re-queued if overflow).

Legacy messages (onLegacy): subscribe/unsubscribe/ping/latency_report/terminal_/pm2_/set_voice/get_subscriptions

RPC methods (86 total by category): agent (auth/authstat/desc/get/ls/models/search/subagents/update), auth (configs/save), codex (start/status/relay/complete), conv (ls/new/get/upd/del/cancel/chunks/full/steer/inject/search/prune/scripts/run-script), gemini (start/status/relay/complete), git (check/push), msg (send/stream/get/ls), q (ls/upd/del), run (new/stream/get/wait/cancel/search/resume), sess (get/latest/chunks/exec), speech (download/status), thread (new/get/upd/del/search/copy/history/run.stream/run.cancel/run.steer), tools (list), util (home/folders/clone/voices/voice.cache/voice.generate/ws.stats/discover.claude/import.claude)

Steering

Steering stops the running agent (SIGKILL) and immediately resumes with the new message:

  1. conv.steer RPC (ws-handlers-conv.js) — kills active process, marks session interrupted, creates new user message, calls startExecution() to resume
  2. Frontend inject button (#injectBtn) — when streaming: reads message input, fires conv.steer, clears input
  3. conv.claudeSessionId on the conversation row ensures the resumed execution picks up --resume <sessionId> automatically

Execution State Management

Three parallel state stores (must stay in sync):

  1. In-memory maps: activeExecutions, messageQueues
  2. Database: conversations.isStreaming, sessions.status
  3. WebSocket clients: streamingConversations Set on each client

cleanupExecution(conversationId) — atomic cleanup function in server.js. Always use this, never inline-delete from maps. Clears activeExecutions, sets DB isStreaming=0.

Queue drain: If processMessageWithStreaming throws, catch block calls cleanupExecution and retries drain after 100ms. Queue never deadlocks.

Message Flow

User send → check if streaming → (streaming: queue server-side, skip optimistic; else: show optimistic message) → RPC msg.stream → backend checks activeExecutions.has(convId) → (yes: queue, broadcast queue_status; no: execute, return session) → broadcast message_created (non-queued only). Queue renders as yellow blocks. On complete, remove .event-streaming-* DOM blocks.

Conversations Sidebar

ConversationManager in static/js/conversations.js:

  • Polls /api/conversations every 30s
  • 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
  • On empty result with existing cache: keeps existing (server error assumption)
  • render() uses DOM reconciliation by data-conv-id — reuses existing nodes, removes orphans
  • showEmpty() and showLoading() both clear listEl.innerHTML — only called when appropriate
  • conversation_deleted WS event handled in setupWebSocketListenerdeleteConversation() filters array
  • confirmDelete() calls deleteConversation() directly AND server broadcasts conversation_deleted — double-call is safe (filter is idempotent)

Base64 Image Rendering in File Read Events

When an agent reads an image file, the event type may not be 'file_read'. Three content structures exist:

Structure A (nested): event.content.source.type === 'base64', data at event.content.source.data Structure B (flat): event.content.type === 'base64', data at event.content.data Structure C (raw string): event.content is a base64 string detected by magic-byte prefix

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.

MIME type priority: event.media_type → magic-byte detection (PNG/JPEG/WebP/GIF) → application/octet-stream.

Voice Model Download

Models (~470MB: Whisper Base ~280MB + TTS ~190MB) downloaded at startup from GitHub LFS or HuggingFace (fallback). UI: voice tab hidden until ready; progress indicator in header; model_download_progress WS broadcast. Cache: ~/.gmgui/models/.

Performance & Observability

Asset serving: gzip only (no brotli), pre-compressed once, cached in _assetCache (etag-keyed). HTML cached, invalidated on hot-reload. /api/conversations: single DISTINCT query (not N+1). Chunks: getConversationChunksSince() pushes filter to DB. Client init: loadAgents/loadConversations/checkSpeechStatus parallel. WS: perMessageDeflate: false (msgpack + zlib blocked event loop).

Debug API (DEBUG=1): /api/debug/machines snapshots, /api/debug/state inspection, /api/debug/ws-stats latency. Browser: window.__debug.getSyncState() exposes all XState machines.

PKCE S256 flow vs auth.openai.com. POST /api/codex-oauth/start → authUrl. User authenticates → redirect to /codex-oauth2callback (local: intercepts localhost:1455/auth/callback; remote: relay page POSTs to /api/codex-oauth/relay). Tokens saved to $CODEX_HOME/auth.json. WS handlers: codex.start/status/relay/complete.

ACP SDK Integration

  • @agentclientprotocol/sdk (^0.4.1) added to dependencies
  • Full integration (replacing custom WS protocol) is optional/incremental — current WS already gives logical multiplexing via concurrent async handlers

Theme-Aware Rendering

CSS custom properties for code/thinking blocks live in static/css/main.css:

  • --color-bg-code, --color-code-text, --color-code-border — light values in :root, dark overrides in html.dark
  • --color-thinking-bg — light value in :root (#f5f3ff), dark override in html.dark (.block-thinking) set to #1e1a2e

static/js/streaming-renderer.js uses var(--color-bg-code) etc. in inline styles — no hardcoded #1e293b/#e2e8f0/#d1d5db hex values. Remaining hardcoded hex in the renderer are intentional semantic colors (blue links, amber warnings, red errors, purple thinking accents) and must not be replaced with CSS vars.

parseAndRenderMarkdown() in streaming-renderer.js handles: ##/### headers, -/* ul lists, 1. ol lists, > blockquotes, --- hr, inline bold/italic/code/links via _mdInline(). Thinking block content is rendered through this function.

README.md Documentation

GitHub Badges and Metrics:

The README.md uses shields.io badges with a consistent pattern:

  • Header badges (lines 7-9): Star count, last commit, latest release — each links to corresponding GitHub page
  • GitHub Stats table (lines 54-62): Detailed metrics (stars, forks, watchers, issues, activity) — each badge links to its resource page
  • All badges use: style=flat-square, color=blue, dynamic data (no hardcoded values)

Debug API section (lines 174-192):

  • Documents DEBUG=1 environment variable for state inspection
  • Lists /api/debug/* endpoints: machines, state, ws-stats
  • Lists browser console window.__debug properties with purpose
  • Links to CLAUDE.md for complete architecture documentation

Approach validated: Header badges are compact (visual prominence); stats table is detailed (discoverability). Non-redundant, no duplicate metrics, complementary visibility.

For future observability improvements: Use shields.io with the established pattern (flat-square, color=blue, dynamic endpoints). Link badges to corresponding GitHub resource pages. Document in README alongside the badge.

Known Gotchas

  • agent-browser --version prints help, not version. Use -V flag.
  • all_conversations_deleted must be in BROADCAST_TYPES set in server.js or it won't fan-out to all clients.
  • streaming_start and message_created are high-priority in WSOptimizer — they flush immediately, not batched.
  • Sidebar animation: transition: none !important in index.html CSS — sidebar snaps instantly on toggle by design.
  • Claude Code runs with plugins enabled--dangerously-skip-permissions was removed to allow gm plugin enforcement.
  • 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.
  • 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.
  • Terminal output is base64-encoded (encoding: 'base64' field on message). Client decodes with decodeURIComponent(escape(atob(data))) pattern for multibyte safety.
  • HTML cache (_htmlCache) is only populated when client accepts gzip. In watch mode it's never cached (always fresh).
  • app.js and app-shortcuts.js script loading: Both are <script defer> tags loaded AFTER agent-auth.js in index.html. They depend on window.wsClient, window.conversationManager, and window._escHtml being initialized first. Defer order is guaranteed by source order — adding new defer scripts that depend on these modules requires careful ordering.
  • window.__debug registry: Structured sub-keys via live getters — machines (conv/toolInstall/voice/convList/prompt/recording/terminal/ws), ws (state/latency/url), auth, perf, config, renderer, conv. Legacy methods preserved: getState(), getSyncState(), getMessageState(). Uninitialized machines return 'uninitialized' rather than crashing. All XState v5 machines have no parallel ad-hoc state.
  • isJsonlBacked flag: Only claude-code (protocol: direct) writes JSONL files. All other agents use stream-event-handler.js for broadcasting. isJsonlBacked = resolvedAgentId === 'claude-code' — guards in stream-event-handler.js prevent double-broadcast for claude-code, and gates ACP streaming for non-JSONL agents.
  • toolIds in server-startup.js must match TOOLS in tool-manager.js: initializeToolInstallations runs for each toolId, creating the tool_installations row. tool_install_history has a FK to tool_installations(tool_id). Any tool omitted from toolIds will cause a FOREIGN KEY constraint failure when the periodic update checker writes history for it.
  • JsonlWatcher._read(fp) override: Captures this._currentFp before calling super._read(fp), making the file path available to _line() callbacks for project-directory decoding in _conv(). JSONL project dirs are encoded (e.g., -config-workspace-agentgui) — decoded via '/' + dirName.slice(1).replace(/-/g, '/').
  • createHttpHandler uses getWss: () => wss (lazy getter): Passing wss directly would crash with TDZ since wss is declared after createHttpHandler is called. The function form defers access until request time when wss is initialized.
  • _promptPushIfWeOwnRemote fires after every streaming_complete: client-streaming4.js calls git.check on the server after each agent turn. If ownsRemote && (hasChanges || hasUnpushed), it auto-sends "Push the changes to the remote repository." to the current conversation. ownsRemote is true for non-github remotes, and for github.com remotes only when GITHUB_USER env var is set and appears in the URL. Without GITHUB_USER, github.com remotes return ownsRemote=false (safe default).

Critical Knowledge for Future Sessions

Codebase Insight Stale: .codeinsight snapshot is outdated (v1.0.811 claimed server.js=3407L, db-queries.js=1412L, but actual as of 2026-04-17: server.js=201L, db-queries.js=94L). Heavy refactoring already done. Before acting on insight "issues" (SQL injection claims, hardcoded secrets, large files), always verify with grep/read against current state. Most reported issues are false positives or stale.

Test Harness Pattern: Use better-sqlite3 in-memory Database, call initSchema()migrateConversationColumns()migrateACPSchema() in order (conversations table needs agentType column from second migration). createQueries signature: (db, prep, generateId) where prep=(sql)=>db.prepare(sql). Silence console during schema init to keep test output clean. Node ships with better-sqlite3 available (optional dep resolved).

CI Auto-Rewrites History: Every push to main triggers Auto-Declaudeify workflow that filters Claude coauthor commits and force-pushes filtered history. After a push, git fetch origin is needed — local SHA drifts from origin SHA.

Debug Endpoints Scattered: Routes at /api/debug, /api/debug/machines, /api/debug/state, /api/ws-stats, /api/debug/ws-stats (alias). routes-debug.js wires all five + backup/restore. DEBUG API in browser: window.__debug.getSyncState().

Plugin Files NOT Orphans: lib/plugins/* are dynamically loaded via lib/plugin-loader.js (import with file:// URL + cache-busting ?v= timestamp). Only truly dead plugin was lib/plugins/git-plugin.js. Static scan showing orphans is misleading.