Skip to content

Latest commit

 

History

History
249 lines (203 loc) · 12.9 KB

File metadata and controls

249 lines (203 loc) · 12.9 KB

LucidAgentIDE — desktop (Electron)

A polished desktop shell around omp + the LucidAgentIDE security harness: a gated agent chat, plus live security / memory-&-context dashboards — same renderer as the browser build, with real omp acp wired in.

Run

Browser build — real chat, no Electron (screenshot-able):

bun run desktop:web        # from repo root → http://localhost:5319

The dev server drives a real omp acp session (with the gate loaded), so prompts produce genuine model replies in a plain browser — same backend the desktop app uses. Dashboards are the live read-only /api/security|memory.

The full desktop app:

cd desktop
bun install                # one-time: pulls Electron
bun run start              # bundles main/preload, launches the window

The desktop app spawns two children:

  1. bun desktop/dev.ts — serves the renderer + read-only /api dashboards.
  2. omp acp -e harness/omp/security_extension.ts — the agent loop with the security gate loaded in-process (invariant #4 holds on the GUI path too).

It reuses your existing ~/.omp credentials (OAuth or API key) — no re-login.

Architecture

One real backend serves both the browser build and the desktop app; Electron is a thin native shell on top.

desktop/
  dev.ts          Bun server: bundles renderer/app.ts, serves the read-only /api
                  dashboards AND the real chat/config backend
  acp_backend.ts  singleton omp-ACP session (chat, config, commands) — gate loaded
  acp.ts          minimal Agent Client Protocol client (JSON-RPC over stdio)
  main.ts         Electron main: bootstrap runtimes, spawn dev.ts, open the window
  preload.ts      window.lucid = native shell only (crisp zoom + window controls)
  runtime.ts      runtime resolution + first-run bootstrap (bundled bun/uv → omp + scanner)
  splash.ts       first-run setup window (shown only when bootstrap has work to do)
  updater.ts      in-app auto-update via electron-updater (GitHub Releases)
  build/          icon.svg (brand mark) + make-icons.ts + mac entitlements
  renderer/       the UI (vanilla TS, no framework) — identical in browser & Electron
    app.ts · styles.css · dom.ts · ui.ts · icons.ts · bridge.ts · format.ts

renderer/bridge.ts talks to the dev server over HTTP for everything — /api/security|memory (dashboards), /api/chat (streaming NDJSON), /api/config, /api/commands — so chat + config are real in the browser too. The only native-only bits are crisp text zoom (webFrame) and window controls, from window.lucid; in a plain browser those fall back to CSS zoom.

Features

  • Functional chat over real omp ACP (session/newsession/prompt, streaming agent_message_chunk), with the gate loaded so blocked tool calls surface as a fly-in toast.
  • Model · Mode · Thinking picker — click the titlebar model badge. The list, current values, and switching all use omp's live configOptions (session/set_config_option); no need to drop into omp.
  • Text zoom — titlebar − / 100% / +, plus Ctrl ± / Ctrl 0 (Electron webFrame.setZoomFactor; CSS zoom in the browser build). Persisted.
  • omp commands — the 39 ACP slash commands (/context, /usage, /tools, /compact, /memory, …) appear in the ⌘K palette; selecting one drops it in the composer to run.
  • Live telemetry — the status bar's context gauge + cost update from the session's usage_update stream.
  • /goal agentic loop — run an agent to a verifiable stop condition (a shell command that exits 0, or a separate checker model's judgement) with a cheaper recommended checker, durable on-disk memory + resume, and scheduled automations. Built for unattended runs: a budget kill switch halts the loop at a hard $ cap, convergence-stall + tool-failure guards stop it spinning on the same blocker, each run ends with an After-Action Report (Mermaid graphs of tool calls, LOC ±, errors, and sites visited, written to .omp/loops/*.report.md), and a cross-run ledger (.omp/loops/run-log.jsonl) tracks success rate and failure breakdown across runs. Every action is still scanned by the fail-closed gate.

AskSage gov gateway (ADR-0007)

Optional: route turns through the AskSage accredited government AI gateway (api.civ.asksage.ai) instead of (or alongside) direct providers.

  • Enable: Settings → Providers → AskSage · Gov gateway → paste your ASKSAGE_API_KEY (stored locally, git-ignored, never committed). Gov models (GPT-5.2 · AskSage Gov, Claude Opus 4 · AskSage Gov, …) then appear in the model picker automatically.
  • How: harness/omp/asksage_extension.ts registers two omp providers via pi.registerProvider (OpenAI + Anthropic routes), loaded with a second -e alongside the security gate — no fork, gate still fail-closed on every turn.
  • Monthly usage shows as a "Gov" chip in the status bar (refresh + 5-min poll).
  • Personas (composer dropdown) are scanned by the same Unicode scanner as tool calls before use; a poisoned persona is blocked, a clean one is delimited.
  • Lockdown: Settings → "AskSage-only" routes every turn through the gateway and hides direct providers.

Verified vs. to confirm on first run

  • Verified: the renderer (screenshotted: chat stream, toast, palette, config picker, zoom), the dashboards on live data, and — captured from a live omp 16.0.8 ACP turn — the exact wire format now used by main.ts/acp.ts: session/new config options, agent_message_chunk/usage_update/ available_commands_update/config_option_update, and session/set_config_option {sessionId, configId, value}.
  • Confirm in the running window: tool-call event shapes (tool_call / tool_call_update) weren't exercised (the probe turn used no tools); the gate's stderr [BLOCKED …] line is the reliable block signal regardless.

See ../DECISIONS.md ADR-0006 for why the gate stays in-process and the GUI is a front end only.

Installers (.zip / .exe) and the app icon

The app is packaged with electron-builder (config in package.jsonbuild, mac entitlements in build/entitlements.mac.plist).

Platform Targets Arch
macOS .zip app bundle arm64, x64
Windows NSIS …-Setup.exe + …-portable.exe x64
Linux .AppImage x64

Why mac ships .zip, not .dmg: electron-builder's default DMG layout mounts the image read-write and drives a Finder/AppleScript background pass, which is fragile on headless GitHub Apple-Silicon runners (hdiutil attach failed - no mountable file systems → missing background.tiff). The .zip bundle is a complete, auto-updatable distribution; users unzip and drag LucidAgentIDE.app to Applications. A signed/notarized .dmg can be reintroduced later once it can be iterated on a real Mac.

The icon

The brand icon's single source of truth is build/icon.svg — a glowing π core inside a magenta→cyan "agent orbit." bun run icons (build/make-icons.ts, run automatically by every dist:* script) rasterizes it to build/icon.png (1024², → macOS .icns) and build/icon.ico (16–256 px, → the Windows .exe, installer, and taskbar). The generated PNG/ICO/ICNS are git-ignored — only the SVG is committed.

Recommended: build via GitHub Actions (no Mac/Windows box needed)

.github/workflows/build-desktop.yml builds both installers on native runners (macOS for the .zip app bundle, Windows for the .exe) — the only way to produce a signed mac build without a Mac.

  • Manual: GitHub → Actions → "Build desktop installers" → Run workflow. Installers download as run artifacts.
  • On a release: push a tag and the installers attach to that tag's Release:
    git tag v0.1.0 && git push origin v0.1.0

Builds are unsigned — no Apple/Windows certificates required to run the workflow.

Build locally

cd desktop
bun install            # pulls electron + electron-builder
bun run dist:mac       # → release/LucidAgentIDE-mac-{arm64,x64}.zip   (on macOS)
bun run dist:win       # → release/LucidAgentIDE-<ver>-Setup.exe (+ portable.exe) (on Windows)

A mac build must be produced on macOS — electron-builder can't package/sign a mac app from Windows or Linux. Use the workflow above, or a Mac.

Building the Windows installer locally needs Developer Mode (or an elevated shell): electron-builder extracts a signing toolchain that contains symlinks, which Windows blocks for unprivileged processes (A required privilege is not held by the client). The packaged app itself (release/win-unpacked/) builds fine without it — only the final Setup.exe step needs the privilege. The CI windows-latest runner has it, so the workflow is the friction-free path.

Zero-prerequisite install (bundled runtimes + first-run setup)

The installer is designed so the user installs nothing beforehand:

  1. Bundled into the app (Resources/runtimes): the static bun and uv binaries, named <tool>-<platform>-<arch> to match runtime.ts. Fetched by bun run runtimes:mac (build/fetch-runtimes.ts) — which dist:mac runs automatically — and per-OS by the CI workflow. Supply-chain hardened: versions are pinned (bun 1.3.14 / uv 0.11.23, never latest) and every archive is SHA-256-verified against a committed hash before it's bundled — a mismatch or missing hash fails the build closed. The committed hashes were cross-checked against the vendors' own checksums (bun SHASUMS256.txt, uv <asset>.sha256). Bump a version, then REFRESH=1 bun run build/fetch-runtimes.ts to recompute the hashes (re-cross-check before committing).

  2. Bundled into Resources/repo (extraResources): the renderer + the LucidAgentIDE repo it runs (harness, tools, scanner-sidecar, desktop sources, node_modules). The main process resolves paths from there when packaged. (Verified on Windows: win-unpacked/resources/repo/ contains harness + scanner-sidecar + the in-process security gate.)

  3. Provisioned on first launch (runtime.tssplash.ts) using the bundled bun/uv, into the app's userData:

    • ompbun add -g @oh-my-pi/pi-coding-agent into a managed global dir.
    • the scanner interpreteruv venv … --python 3.12 (the sidecar has zero pip deps, so any 3.11+ interpreter works; uv downloads one if needed).

    A small setup splash appears only when there's work to do, then LUCID_OMP_BIN / SCANNER_PYTHON are passed down to the dev server and its omp + scanner children. On a machine that already has bun/omp/uv (e.g. a dev box), nothing is installed and no splash appears — resolution falls back to the user's own tools at their absolute install paths (~/.bun, ~/.local, ~/.cargo, and Homebrew /opt/homebrew/bin · /usr/local/bin), then bare PATH. The absolute paths are load-bearing: a Finder-launched app inherits a minimal PATH (/usr/bin:/bin), so a bare bun would spawn … ENOENT.

If first-run setup fails (offline, etc.), the app still launches; the fail-closed gate blocks tool calls until the scanner is available — it never treats a missing scanner as "safe" (CLAUDE.md invariant #3).

Auto-update

Packaged builds check GitHub Releases on launch and prompt to restart when a newer version is downloaded (updater.ts + the publish provider in package.json). To ship an update: bump version, then git tag vX.Y.Z && git push origin vX.Y.Z — the workflow builds installers and uploads the update feed (latest*.yml + .blockmap) to that Release.

Signing / notarization

The config builds unsigned by default; signing is opt-in via GitHub secrets and the workflow signs + notarizes automatically when they're present. Full secret list and setup: SIGNING.md.

  • Unsigned macOS: Gatekeeper — first launch via right-click → Open. (macOS auto-update needs a signed build.)
  • Unsigned Windows: SmartScreen — More info → Run anyway. Windows auto-updates fine while unsigned.

Status: the macOS .zip app bundle (arm64 + x64) builds on macOS via bun run dist:mac; the brand icon is embedded; the repo, in-process gate, and the static bun/uv runtimes are bundled. Verified self-contained: the app's bundled dev server boots under a Finder-style minimal PATH (/usr/bin:/bin) and serves /api/health — i.e. no spawn bun ENOENT. The Windows packaging is verified up to the unpacked app (win-unpacked with icon + repo); its final Setup.exe is produced by the GitHub Actions workflow on a native Windows runner (run it and grab the artifacts).