Desktop client
The Electron app that hosts the localhost gateway, the cloud relay connection, the tray UI, and the persistent stores.
The desktop client is the single Electron process that runs three logical planes: the local HTTP gateway, the cloud control connection, and the UI plane.
Three planes in one process
| Plane | Purpose |
|---|---|
| UI plane (renderer) | Onboarding overlay, Dashboard, Approvals, Activity Log, Settings. Rendered from a single preload bridge. |
| Local gateway plane (main + HTTP server) | The localhost HTTP API on 127.0.0.1:19432 with NDJSON and SSE streaming, CORS, challenge-token auth, and approval gates. |
| Cloud control plane (main + Socket.IO client) | Outbound Socket.IO connection to the relay; receives desktop.command envelopes, dispatches into the local gateway, and streams events back. |
Tray UI
The tray is the primary surface when the window is closed.
States:
| State | Meaning |
|---|---|
starting | The gateway is booting and the cloud socket is connecting. |
ready | Gateway bound, capabilities detected, cloud hello-ack received. |
degraded | Cloud socket disconnected or awaiting hello-ack. Local work still runs. |
error | Startup failed. Open the window to see the reason. |
Tray menu items:
- Open Symphony — opens the main window. Shows
Open Symphony (N pending)when approvals are pending. - Pause / Resume — toggles acceptance of cloud commands.
- Quit — standard quit.
The tray badges the macOS title with pending approval counts (capped at 99) so you notice high-risk operations even with the window hidden. Window close hides to tray; the app keeps running.
Persistent stores
The client uses electron-store under app.getPath("userData"):
| Store | Purpose |
|---|---|
desktop-settings | User-configurable settings and saved configs. |
desktop-secrets | API key encrypted via safeStorage. |
desktop-approvals | Pending approval queue. |
desktop-activity-log | 200-entry ring buffer of gateway requests (8 KiB body truncation). |
desktop-job-store | Symphony loop job records, including terminal history. |
desktop-loop-tokens | Per-loop auth tokens, encrypted. |
Plus:
~/.closedloop-ai/electron-port— plaintext active port.<userData>/gateway-identity.json— stablegatewayIdUUID.- Auto-generated
src/shared/build-info.tsstamped at prebuild.
Persistent logs and Diagnostics
Desktop writes a durable main.log through electron-log with the file transport only. The renderer console is not duplicated by electron-log; gateway-visible messages continue to flow through the allowlisted GatewayLogger transport.
Typical log locations:
| Platform | Path |
|---|---|
| macOS | ~/Library/Logs/Closedloop/main.log |
| Windows | %APPDATA%/Closedloop/logs/main.log |
| Linux | ~/.config/Closedloop/logs/main.log |
The Diagnostics tab renders the in-memory gateway log and seeds a bounded previous-session tail from main.log during boot. First-run or unreadable log tails return an empty list and do not block startup. Diagnostics supports pausing live rendering while entries continue to queue, preserves active text selection during refresh, and exposes an Open log file action for support handoff.
IPC surface for the renderer
The preload script exposes window.desktopApi with over 35 methods including:
- Settings:
getSettings,updateSettings - Runtime:
getRuntimeStatus,getActivityEvents,getLogs,clearLogs - Approvals:
getPendingApprovals,approveApproval,denyApproval,alwaysAllowApproval,removeAlwaysAllowRule - API key:
getApiKeyStatus,setApiKey,clearApiKey - Cloud:
getCloudCommandsPaused/setCloudCommandsPaused,getCloudConnectionEnabled/setCloudConnectionEnabled - Onboarding:
getOnboardingState,completeOnboarding,pickSandboxDirectory - Debug:
getDangerousAutoApprove/setDangerousAutoApprove,isDebugAuthEnabled,mintDebugToken - Updates:
checkForUpdate,applyUpdate - Diagnostics:
getLogFilePath,openLogFile - Jobs:
listRunningJobs,listCompletedJobs,getJob,getJobLogTail - Binary paths:
getBinaryPaths,patchBinaryPaths,detectCliTools - Saved configs:
saveConfig,listConfigs,deleteConfig,renameConfig,applyConfig,findMatchingConfig
Push events to the renderer:
desktop:navigate-tab— programmatic tab change.desktop:update-status— packaged updater status changes, includingavailable,downloading,downloaded,error, andnot-available.desktop:update-available— legacy compatibility event. The update banner only becomes actionable when the downloaded status reportsreadyToInstall: true.
Composition root
Everything wires together in src/main/app.ts — the DesktopApplication class boots stores, the gateway server, the cloud socket, the approval policy, the tray, and IPC. The entry point is src/main/index.ts.
Auto-update
Packaged builds use electron-updater against the ClosedLoop update service at https://api.closedloop.ai/desktop/updates, configured as a generic provider. The service authenticates to GitHub server-side and serves only Desktop updater metadata and assets, so the client never needs repository access:
autoDownload = true,autoInstallOnAppQuit = true- Initial check on boot, then every 5 minutes
update-availableanddownload-progressare logged but do not show the restart banner.- The renderer shows Update & restart only after
update-downloaded, soapplyUpdatecan callquitAndInstallimmediately. - Critical packaged updater and shutdown failures emit bounded
desktop.telemetryrecords for Datadog through the existing relay path:electron_update.initiated,electron_update.failed, anddesktop.shutdown_failed. These records include sanitized diagnostics only; raw log contents are not uploaded.
Dev builds (!app.isPackaged) compare origin/main commit hashes via git fetch and offer to pull and rebuild.
Packaging
electron-builder.yml targets macOS only — universal DMG and zip, hardened runtime, notarized, signed, and updater metadata. The manually invoked release workflow publishes versioned desktop-v<version> releases, then promotes that same build onto two separate rolling desktop-latest releases:
- One in the private build repository, carrying the updater metadata (
latest-mac.yml) and the mac zip assets that the update service reads on the client's behalf. - One in the public
closedloop-ai/closedloop-airepository, carrying a single fixed-name universal DMG — the stable, account-free URL behind the public download link.
Output goes to dist-dmg/.
Breaking change policy
Any breaking change to gateway routes, cloud relay messages, IPC, or persisted store schemas requires legacy migration logic and a tracking ticket. Stored settings include forward-compatible migrations (for example, apiOrigin → relayOrigin rename, authApiOrigin → apiOrigin promotion, "auto" tier → "high").
See the desktop gateway, cloud relay, approvals and sandbox, and telemetry pages for the details of each subsystem.