Closedloop.ai

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

PlanePurpose
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:

StateMeaning
startingThe gateway is booting and the cloud socket is connecting.
readyGateway bound, capabilities detected, cloud hello-ack received.
degradedCloud socket disconnected or awaiting hello-ack. Local work still runs.
errorStartup failed. Open the window to see the reason.

Tray menu items:

  1. Open Symphony — opens the main window. Shows Open Symphony (N pending) when approvals are pending.
  2. Pause / Resume — toggles acceptance of cloud commands.
  3. 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"):

StorePurpose
desktop-settingsUser-configurable settings and saved configs.
desktop-secretsAPI key encrypted via safeStorage.
desktop-approvalsPending approval queue.
desktop-activity-log200-entry ring buffer of gateway requests (8 KiB body truncation).
desktop-job-storeSymphony loop job records, including terminal history.
desktop-loop-tokensPer-loop auth tokens, encrypted.

Plus:

  • ~/.closedloop-ai/electron-port — plaintext active port.
  • <userData>/gateway-identity.json — stable gatewayId UUID.
  • Auto-generated src/shared/build-info.ts stamped 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:

PlatformPath
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, including available, downloading, downloaded, error, and not-available.
  • desktop:update-available — legacy compatibility event. The update banner only becomes actionable when the downloaded status reports readyToInstall: 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-available and download-progress are logged but do not show the restart banner.
  • The renderer shows Update & restart only after update-downloaded, so applyUpdate can call quitAndInstall immediately.
  • Critical packaged updater and shutdown failures emit bounded desktop.telemetry records for Datadog through the existing relay path: electron_update.initiated, electron_update.failed, and desktop.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-ai repository, 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.

On this page