Bohdan Triapitsyn 285c3bcaae Migrate desktop shell from Tauri to Electron (#964)
* feat(electron): scaffold Electron desktop package

Main + preload + ssh manager, packaging scripts, icons, root build/lint/type-check wiring.

* feat(ui): add Electron runtime detection and desktopNative facade

isElectronShell via window.__OPENCHAMBER_ELECTRON__, isDesktopShell now covers both. desktopNative wraps window/title/theme calls so UI avoids direct Tauri imports. revealDesktopPath added.

* refactor(ui): route window/title/theme/export through desktopNative

SessionSidebar, MultiRunLauncher, useWindowTitle, ThemeSystemContext, exportSession drop direct @tauri-apps imports.

* refactor(ui): treat all desktop shells uniformly

device.ts switches Tauri-only checks to isDesktopShell. Header OpenInApp button uses actionDirectory so it falls back to the active project path.

* fix(ui): menu Copy clipboard fallback and softer sidebar tint

useMenuActions falls back to Clipboard API for the native Copy action when the page doesn't intercept. cssGenerator lowers sidebar strong/soft alpha so the tinted surface reads gentler.

* chore(electron): mirror Tauri build/type-check script shape

build script becomes no-op so root 'bun run build' skips packaging. Syntax validation (node --check) moves into type-check. electron:build root script still runs full sidecar+bundle+electron-builder.

* fix(electron): sync app identity, preload path, boot outcome, dev entry

Read version from packages/electron/package.json so 'electron ./main.mjs' dev entry reports the app version instead of Electron's. Bump electron package to 1.9.6 for workspace parity.
Resolve preload via app.getAppPath() in prod (bundle lives in dist-bundle while preload.mjs ships at app root).
Compute and inject __OPENCHAMBER_DESKTOP_BOOT_OUTCOME__ in main + preload so the loading gate dismisses (mirrors Tauri Rust injection).
Dev entry uses ./main.mjs to bypass the stale dist-bundle so source edits apply.

* refactor(open-in-app): split directory and file flows

Header button now opens the project/worktree directory only — drop activeFilePath prop and its Header prop passthrough. FilesView editor dropdown opens the active file only via new openDesktopFileInApp.

Electron main.mjs mirrors Tauri's open-chain logic: buildOpenProjectSpecs (finder/terminal direct, vscode-like via CLI -n, JetBrains via open -na --args) and buildOpenFileSpecs (finder -R reveal, terminal opens parent dir, editors via CLI or open -a). runSpecChain falls through specs until one exits 0.

* fix(files-view): keep floating toolbar mounted while its dropdowns are open

Portalled Base UI menu popups render outside floatingToolbarRef. The document mousedown listener and onMouseLeave collapsed the toolbar as soon as the popup appeared, unmounting the DropdownMenu root and swallowing clicks on its items. Track open dropdowns via onOpenChange and skip the collapse while count > 0; also ignore mousedowns that land inside a dropdown-menu-content/item.

* feat(electron): add quit confirmation with risk poller

Mirrors Tauri's macOS-only behavior: poll /api/openchamber/scheduled-tasks/status and /api/openchamber/tunnel/status every 5s. If active tunnel or running/enabled scheduled tasks are detected, Cmd+Q / dock Quit / menu Quit shows a native warning dialog listing reasons; otherwise quit proceeds silently.

performConfirmedQuit persists window state, kills sidecar, shuts down SSH, and fires a 1500ms unref'd safety timeout that calls app.exit(0) if the normal quit sequence stalls.

* feat(notifications): fix payload parsing, restore-on-click, session deep-link

Normalize input so both sidecar stdout path (flat) and UI IPC path ({ payload: {...} }) work; previous destructuring missed requireHidden (camelCase) and the payload wrapper so notifications showed with empty body.

Click handler restores the window if minimized, shows it if hidden, and focuses. When the notification payload carries sessionId, emit openchamber:open-session which the App listener routes to setCurrentSession — matches the PWA service-worker deep-link behavior. macOS notifications now also use sound 'Glass' for parity with Tauri.

* chore(electron): bump to Electron 41 + latest updater/context-menu

electron ^38.2.0 -> ^41.2.1
electron-updater ^6.6.2 -> ^6.8.3
electron-context-menu ^4.0.4 -> ^4.1.2

Dev boot verified: main process starts, preload exposes globals, API server + quit risk poller + autoUpdater all initialize without errors.

* fix: keep todo row alignment stable when expanding text

Keep checkbox and action buttons vertically centered in collapsed todo rows
Prevent first todo line from shifting when expanding to multiple lines

* fix: make commit highlights visible and input behavior reliable

Switch commit message field to native textarea for predictable auto-resize
Fix AI highlights append flow so inserted text is applied consistently
Make chat scroll-to-bottom control fully circular

* style: increase chat bubble corner radius consistency

Use larger radius for user chat message bubbles
Match chat input container radius to user message styling

* feat(electron): adopt OpenCode playbook improvements

mac: hardenedRuntime + entitlements.mac.plist + notarize + dmg.sign for Apple notarization parity.
single-instance lock + openchamber:// protocol with session/project/host routing (host switch done fully in main via activateMainWindow).
setAppUserModelId for Win toast identity; proxy-bypass-list switch; chdir(homedir) for Finder-launch cwd safety.
shell env probe (\$SHELL -il -> -l) merged into sidecar spawn; PATH deduped.
electron-log with 5MB rotation + 7-day cleanup; autoUpdater.logger wired; startup info log.
webContents zoom locked to 1 (zoom-changed + did-finish-load).
UI: openchamber:open-project -> useDirectoryStore.setDirectory.

* fix(electron): make bootOutcome mutable across re-navigation + project deep-link

host deep-link used to land on chooser because contextBridge exposed bootOutcome as read-only; initScript re-assignment became a silent no-op. drop preload's contextBridge for bootOutcome, inject it via main-world initScript, and move injection from did-finish-load to dom-ready so it lands before React mounts.

project deep-link updated currentDirectory only; activeProjectId stayed stale so the sidebar didn't highlight the new project. switch to projectsStore.setActiveProject (or addProject for new paths) which updates both.

add log.info around deep-link dispatch + host switch for diagnostics.

* fix(electron): desktop_hosts_set IPC args + persist initialHostChoiceCompleted + re-eval bootOutcome

UI calls invoke('desktop_hosts_set', { input: {...} }) but main was reading args.config — every onboarding 'i've completed installation' / host-dialog save wrote nothing, so desktopDefaultHostId stayed null and the chooser screen looped forever.

also:
- writeDesktopHostsConfig now persists desktopInitialHostChoiceCompleted so the tauri-compat flag survives writes.
- readDesktopHostsConfig returns initialHostChoiceCompleted so the UI-side config mirror is complete.
- after writing hosts, recompute state.bootOutcome + state.initScript; a subsequent window.location.reload() picks up target=local/status=ok via dom-ready injection without needing a full app restart.
- app.setName('OpenChamber') early (pre log.initialize) so electron-log logs land in ~/Library/Logs/OpenChamber/ instead of the package-derived '@openchamber/electron' path.

* chore(electron): rename appId to dev.openchamber.desktop

ai.opencode.* is the OpenCode team's reverse-DNS namespace; OpenChamber should not squat there. now that we're on Electron, drop the tauri-era inherited identifier and claim our own under openchamber.dev.

user-facing productName stays "OpenChamber". tauri identifier left as-is — legacy shell on the way out.

* feat(ci): add electron build+notarize+publish jobs to release workflow

three new jobs in release.yml, running in parallel with tauri:

- build-desktop-electron-macos: matrix(arm64, x86_64) on macos-26; installs Developer ID via keychain, runs build:sidecar + bundle:main + electron-builder --mac --arch <> --publish=never (with APPLE_ID / APPLE_APP_SPECIFIC_PASSWORD / APPLE_TEAM_ID env mapped from existing secrets). verifies hardened runtime, stapled notary ticket, required entitlements. uploads DMG/ZIP/blockmaps to the release and emits per-arch latest-mac.yml as a GH artifact.

- combine-electron-manifests: downloads latest-yml-*-apple-darwin artifacts, runs the existing finalize-latest-yml.mjs to merge per-arch files entries into a single latest-mac.yml, uploads combined yml to the release.

- finalize-release: now also waits on the two new jobs before flipping the draft release to published.

also: explicit artifactName in electron-builder config so arm64 and x64 dmg/zip never collide.

electron-updater in main.mjs (setFeedURL btriapitsyn/openchamber) fetches this latest-mac.yml on desktop_check_for_updates; downloadUpdate / quitAndInstall wire through our existing IPC handlers unchanged.

* docs: future-agent brief for tauri -> electron auto-update cutover

self-contained plan for the one-shot migration release that carries existing tauri installs into the electron shell via tauri's updater. written so a fresh agent with no branch context can execute it.

covers: the trick (repackage signed electron .app as a tauri tarball, minisign with existing TAURI_SIGNING_PRIVATE_KEY), workflow surgery on release.yml, rollback plan, validation steps against a real tauri install, and edge cases (CFBundleIdentifier change, notification perms re-prompt, deep-link re-registration).

* docs: soften framing of cutover playbook (no user-shaming)

* chore: mark electron as primary desktop shell; tune dmg installer window

AGENTS.md: explicit note that new desktop work lands in packages/electron/, packages/desktop/ (tauri) is maintenance-only until the cutover described in docs/TAURI_TO_ELECTRON_CUTOVER.md. updated runtime/entry-points/build-commands sections accordingly.

electron/package.json build.dmg: cleaner title ("OpenChamber 1.9.6" without -arch suffix), 660x400 window matching the tauri layout users are used to, icon size 128, explicit app/Applications positions.

* refactor(web): drop bun-specific runtime deps from server

- 11 test files migrated bun:test -> vitest; API (describe/it/expect) is drop-in; all 73 tests pass under vitest run.
- bun:sqlite -> better-sqlite3 in git/service.js::syncSandboxesToOpenCodeDb. api shift is db.query().get()/run() -> db.prepare().get()/run().
- add "test": "vitest run" script in packages/web.

no production code used Bun.* APIs; server is Express-on-Node already. this commit removes the remaining bun-runtime shape so the server module can be imported and booted inside an electron main process.

* feat(electron): boot web server in-process, drop sidecar subprocess

the electron main process now imports @openchamber/web/server/index.js as a workspace dependency and calls startWebUiServer({...}) directly. the returned handle exposes getPort() / stop() and the notification emitter takes an onDesktopNotification callback, so we no longer spawn a bun-compiled sidecar binary and no longer parse stdout for the one-line notify protocol.

- packages/electron/package.json: +@openchamber/web (workspace:*); extraResources drops 'sidecar'; build:sidecar script renamed to build:web-assets (kept the vite build step, dropped the bun compile step).
- packages/electron/main.mjs: remove spawn/kill-stale-sidecar/sidecar path resolver/stdout-prefix parser; rewrite spawnLocalServer to probe a free port (stored | DEFAULT_DESKTOP_PORT | OS-assigned) then import server and await startWebUiServer; killSidecar calls handle.stop({ exitProcess: false }); hoist user shell env (PATH, etc.) onto process.env once so opencode / git / rg children still inherit the expected runtime environment.
- packages/web/server/lib/notifications/emitter-runtime.js: accept an onDesktopNotification callback (late-bindable via setOnDesktopNotification). when set, notifications are dispatched through the callback instead of process.stdout; tauri path still uses stdout when no callback is bound.
- packages/web/server/index.js: main() wires options.onDesktopNotification to notificationEmitterRuntime.setOnDesktopNotification.
- release.yml + AGENTS.md updated for the new script name + runtime shape.

payoff: -300ms cold start on mac, single process in activity monitor, no stdio IPC, no bun binary in the packaged app. tauri sidecar path is untouched.

* build(electron): rebuild native deps explicitly, bump electron-builder

the previous build failed because electron-builder 24.13.3 tried to run \`bun rebuild\` on native deps (better-sqlite3, node-pty) and bun has no rebuild subcommand; it also couldn't find prebuild-install because bun hoists under node_modules/.bun/<pkg>@<ver>/ and never populates node_modules/.bin for transitive deps.

fix:
- bump electron-builder devDep to ^26, whose packageManager detection understands bun workspace layouts.
- add @electron/rebuild devDep + scripts/rebuild-native.mjs. the script rebuilds better-sqlite3 / node-pty / bun-pty against the installed electron version before electron-builder is invoked.
- set build.npmRebuild=false so electron-builder no longer attempts its own broken PM-based rebuild.
- package script: build:web-assets -> bundle:main -> rebuild:native -> electron-builder.

verified: CSC_IDENTITY_AUTO_DISCOVERY=false bun run electron:build produces signed-ad-hoc dmg/zip/blockmap/latest-mac.yml; artifacts land under packages/electron/dist as expected. cold-start from Applications should work (native bindings now match electron 41 node ABI).

* fix(electron): externalize web server + native deps from main bundle

the ESM bundle was statically inlining @openchamber/web transitively, which pulled in bun-pty/src/terminal.ts with its top-level \`import { dlopen } from "bun:ffi"\`. node's ESM loader parses every static import when the bundle loads, so the bun:ffi scheme crashed the packaged app at startup with ERR_UNSUPPORTED_ESM_URL_SCHEME — the runtime guard (if (globalThis.Bun) { await import('bun-pty') }) never got a chance to skip it.

fix: bundle-main.mjs marks @openchamber/web (+ its bun-pty / node-pty / better-sqlite3 transitives) as external. the dynamic \`await import('@openchamber/web/server/index.js')\` in main.mjs stays a runtime resolution; the conditional bun-pty import stays dynamic; native modules load from node_modules via the standard resolver.

* perf(web): classify UI-only deps as devDependencies, shrink packaged app

packages/web is a hybrid package: server code in server/, react UI source in src/, compiled UI output in dist/. the server serves dist/ as static files — it never imports react/radix/codemirror/etc. at runtime. but electron-builder, npm install, and similar tools treat everything under "dependencies" as shipping surface, so all of react + @radix-ui/* + @codemirror/* + @fontsource/* + @simplewebauthn/browser + cmdk + ghostty-web + ... were landing in app.asar even though the same code is already baked into dist/ chunks.

move ~24 UI-only packages to devDependencies. vite + its plugins still install them in dev (bun install fetches devDependencies in workspaces), so \`bun run build\` is unchanged. consumers doing \`npm install @openchamber/web\` no longer pull ~150MB of unused browser-side modules.

measured on aarch64 darwin build:
- app.asar: 281MB -> 44MB (-237MB, -84%)
- .dmg: 320MB -> 132MB (-59%)
- .zip: 305MB -> 129MB (-58%)

verified type-check, ui build, 73 vitest tests, packaged launch.

* chore(electron): center dmg installer icons, use cream brand background

dmg-builder 26 ignored our previous dmg.contents positions against its template background (they stayed at template coords, producing misalignment with the drawn arrow). switch to a solid backgroundColor (#FFFCF0, the splash light tone) so the template image is dropped entirely and our coordinates are authoritative. window tuned to 540x340, iconSize 100, iconTextSize 13.

dmgbuild treats contents coordinates as icon *centers* (not top-left), so with iconSize=100 in a 540 window, x=180 and x=360 place left and right clusters with equal 130px gaps on both sides of the window. y=140 vertically centres the icon+label pair.

* fix(electron): eliminate main-thread freezes in in-process server

Three blocking paths were running sync work on the Electron main event
loop, causing multi-second UI freezes under the new in-process server:

- package-manager.detectPackageManagerDetails fired spawnSync(pnpm/npm/
  yarn/bun bin -g) with 10s timeouts. In desktop runtime PM detection is
  pointless (app is .app bundle, updates via electron-updater) — short-
  circuit when OPENCHAMBER_RUNTIME=desktop. This was the ~5s freeze.
- buildInstalledApps iterated 22 OPEN_IN_APPS × spawnSync(mdfind, sips).
  Converted to execFile promises so child waits yield to the loop.
- orphan-project-file recovery re-scanned disk on every settings read
  (3+/s from fs/list/etc). Cache the outcome per process lifetime.

Also: resolveProjectDirectory prefers settings.lastDirectory over
activeProjectId so file-open from sidebar/chat doesn't 400 with
"Path is outside of active workspace" after the user navigates.

Plus dropdown typeahead fixes in DesktopHostSwitcher/BranchSelector:
stopPropagation on input keys so cmdk doesn't swallow typing.

* feat(electron): restore desktop LAN access for in-process server

spawnLocalServer now reads settings.desktopLanAccessEnabled and binds
on 0.0.0.0 when enabled, so phones/tablets on the same Wi-Fi can open
the app via http://<lan-ip>:<port>. Adds desktop_get_lan_address IPC
(UDP-connect route lookup with networkInterfaces fallback) for the
settings UI to show the reachable URL.

UI and settings plumbing already existed from the sidecar build; only
the Electron main-process wiring was missing.

* chore: added electron package to version bump script

* fix(electron): address PR review — harden IPC surface + polish

P1 security:
- Gate openchamber:invoke and openchamber:dialog:open by webContents
  origin. Only local (loopback / dev file://) senders can call desktop_*.
  Blocks remote hosts loaded via DesktopHostSwitcher from reading local
  files, opening apps, relaunching, etc.
- desktop_read_file now refuses paths outside $HOME / tmpdir and denies
  .ssh/.aws/.gnupg/.config/gh/credentials + .env/.pem/.key by name
  (defense-in-depth behind the origin gate).

P2:
- webPreferences.sandbox:false: add comment explaining preload needs Node
  (contextBridge+ipcRenderer) and why flipping to true would break IPC.
- desktop_set_vibrancy: comment the intentional no-op (no Electron
  equivalent for the Tauri NSVisualEffectView path), drop requiresRestart.
- desktopNative.ts: replace isTauriShell() guards with isDesktopShell()
  so the semantics match (previous check worked only because Electron
  preload exposes a __TAURI__ shim).
- AGENTS.md: correct entry description — server runs in-process, not as
  a sidecar subprocess.

* fix(electron): stop leaking desktop shell APIs to remote renderer pages

Preload was exposing __TAURI__ and __OPENCHAMBER_ELECTRON__ unconditionally,
so after DesktopHostSwitcher navigated the window to a remote OpenChamber
instance the remote UI saw isDesktopShell() === true and tried to invoke
desktop_* IPC. The main-process origin gate then threw "IPC not available
for this origin", surfacing as a user-visible error on the onboarding
screen of the remote.

Preload re-runs on cross-origin navigation; compute current origin up
front and only expose the shell globals + the openchamber:emit listener
when the document is loopback / state.localOrigin / file://. Remote
pages now look like a plain web runtime — no IPC path to reject.

* fix(electron): restore remote UI shell integration via per-command gate

Previous commit stripped __TAURI__ / __OPENCHAMBER_ELECTRON__ from remote
pages wholesale, which broke DesktopHostSwitcher for anyone switched to
a remote instance: no hosts list, "Unknown" probe status, open-in-new-
window dead. Also lost window chrome affordances that the remote UI
needs to render correctly inside the Electron shell.

Switch from an origin-level gate to a per-command allowlist:

- preload.mjs exposes __TAURI__ and __OPENCHAMBER_ELECTRON__ on every
  page (shell identity + IPC channel). __OPENCHAMBER_LOCAL_ORIGIN__ and
  __OPENCHAMBER_MACOS_MAJOR__ also go everywhere since HostSwitcher and
  window chrome depend on them and neither grants capability.
  __OPENCHAMBER_HOME__ stays local-only (leaks the OS username and is
  misleading if consumed as a workspace hint on a remote page).

- main.mjs ipcMain.handle accepts a curated COMMANDS_SAFE_FOR_REMOTE set
  (hosts_get, host_probe, new_window, new_window_at_url, set_window_*,
  is_window_fullscreen, start_window_drag, get_app_version,
  get_lan_address). Filesystem, shell.openPath, installed-apps scans,
  app relaunch, auto-update, hosts_set, dialog:open, read_file stay
  local-only — remote UI doesn't need them and can't weaponize them.

* ci(release): rebuild native modules against Electron ABI before packaging

Electron job skipped rebuild:native so bun install's Node-ABI builds of
better-sqlite3/node-pty/bun-pty shipped into the asar — packaged app
would crash on require. Local bun run package runs the step via
scripts/rebuild-native.mjs (npmRebuild is disabled in package.json);
mirror it in CI and pass ELECTRON_BUILDER_ARCH so the x64 matrix
cross-builds from the arm64 runner.

Tauri job untouched — both builds continue to produce side-by-side
release artifacts (latest.json for Tauri, latest-mac.yml for Electron)
so each shell's updater finds its own manifest.

* ci(release): split Electron arm64/x64 onto native macOS runners

Both Electron matrix entries were running on macos-26 (arm64) and
cross-building x64 from there. Works for Rust/Tauri; brittle for
native Node modules — better-sqlite3, node-pty, bun-pty (with its
rust-pty crate) each have their own cross-target quirks.

Pin arm64 → macos-14 and x64 → macos-13 so node-gyp and
@electron/rebuild build against the host arch. ELECTRON_BUILDER_ARCH
now just mirrors the runner for clarity.

* Revert "ci(release): split Electron arm64/x64 onto native macOS runners"

This reverts commit f217880e49609cf1418818af0f837b333dbb6f42.

* ci(test-build): add Electron DMG job to arm64 dispatch workflow

Parallel job to the existing Tauri DMG builder, same runner + Apple
cert path. Mirrors the release workflow steps (build:web-assets,
bundle:main, rebuild:native, electron-builder) so maintainers can
smoke-test a signed+notarized Electron DMG before merging.

* ci: use electron-builder v26 boolean arch flags

v26 dropped --arch <name> in favour of per-arch booleans (--arm64,
--x64, etc.). Test build was failing at dispatch time; release job
had the same bug latent. Switch both to the supported form.

* fix(electron): route external links to the system browser

<a href> clicks and window.open calls with non-local URLs were loading
inside the Electron BrowserWindow (or spawning a second Electron window
as a makeshift browser). Add an origin-aware navigation guard to each
window: loopback / state.localOrigin / configured desktop hosts keep
their existing in-window behaviour (HostSwitcher, in-window probes);
everything else hands off to shell.openExternal so http/https links
open in the user's default browser.
2026-04-20 15:41:15 +03:00
2025-12-07 19:32:53 +02:00
2025-12-07 19:32:53 +02:00
2026-04-17 19:06:27 +03:00
2025-12-07 19:32:53 +02:00
2025-12-07 19:32:53 +02:00
2025-12-07 19:32:53 +02:00
2025-12-07 19:32:53 +02:00
2025-12-07 19:32:53 +02:00
2025-12-07 19:32:53 +02:00
2025-12-07 19:32:53 +02:00

OpenChamber

GitHub stars GitHub release Created with OpenCode Discord Support the project

OpenCode, everywhere. Desktop. Browser. Phone.

A rich interface for OpenCode. Review diffs, manage agents, run dev servers, and keep the big picture while your AI codes.

OpenChamber Chat

More screenshots

Tool Output Settings Web Version Diff View VS Code Extension

PWA Chat PWA Diff

Why use OpenChamber?

  • Cross-device continuity: Start in TUI, continue on tablet/phone, return to terminal - same session
  • Remote access: Use OpenCode from anywhere via browser
  • Familiarity: A visual alternative for developers who prefer GUI workflows

Features

Core (all app versions)

  • Branchable chat timeline with /undo, /redo, and one-click forks from earlier turns
  • Smart tool UIs for diffs, file operations, permissions, and long-running task progress
  • Voice mode with speech input and read-aloud responses for hands-free workflows
  • Multi-agent runs from one prompt with isolated worktrees for safe side-by-side comparisons
  • Git workflows in-app: identities, commits, PR creation, checks, and merge actions
  • GitHub-native workflows: start sessions from issues and pull requests with context already attached
  • Plan/Build mode with a dedicated plan view for drafting and iterating implementation steps
  • Inline comment drafts on diffs, files, and plans that can be sent back to the agent
  • Context visibility tools (token/cost breakdowns, raw message inspection, and activity summaries)
  • Integrated terminal with per-directory sessions and stable performance on heavy output
  • Built-in skills catalog and local skill management for reusable automation workflows

Web / PWA

  • Provider-aware tunnel access model with Cloudflare quick, managed-remote, and managed-local modes
  • One-scan onboarding with tunnel QR + password URL helpers
  • Mobile-first experience: optimized chat controls, keyboard-safe layouts, and attachment-friendly UI
  • Background notifications plus reliable cross-tab session activity tracking
  • Built-in self-update + restart flow that keeps your server settings intact

Desktop (macOS)

  • Native macOS menu integration with polished app actions and deep-link handling
  • Multi-window support for parallel project/session workflows
  • "Open In" shortcuts for Finder, Terminal, and your preferred editor
  • Fast switching between local and remote instances
  • Workspace-first startup flow with directory picker and steadier window restore behavior

VS Code Extension

  • Editor-native workflow: open files directly from tool output and keep sessions beside your code
  • Agent Manager for parallel multi-model runs from a single prompt
  • Right-click actions to add context, explain selections, and improve code in-place
  • In-extension settings, responsive layout, and theme mapping that matches your editor
  • Hardened runtime lifecycle and health checks for faster startup and fewer stuck reconnect states

Custom Themes

  • Use it from anywhere - Cloudflare tunnel with QR code onboarding. Scan, connect, code from your couch.
  • Branchable chat timeline - Undo, redo, fork from any turn. Explore different approaches without losing your place.
  • GitHub-native workflows - Start sessions from issues and PRs with context already attached. Review checks, merge - all in-app.
  • Project Actions - Run dev servers, configure SSH port forwarding, open remote URLs locally. Your project commands, one click away.
  • Connect to remote machines - Desktop app connects to remote OpenChamber instances over SSH, with dedicated lifecycle and UX flows.

Quick Start

Prerequisite: OpenCode CLI installed.

Desktop (macOS)

Download from Releases.

VS Code

Install from Marketplace or search "OpenChamber" in Extensions.

CLI (Web + PWA)

requires Node.js 20+

curl -fsSL https://raw.githubusercontent.com/btriapitsyn/openchamber/main/scripts/install.sh | bash
openchamber --ui-password be-creative-here
Advanced CLI options
openchamber --port 8080              # Custom port
openchamber --ui-password secret     # Password-protect UI
openchamber tunnel help              # Tunnel lifecycle commands
openchamber tunnel providers         # Show provider capabilities
openchamber tunnel profile add --provider cloudflare --mode managed-remote --name prod-main --hostname app.example.com --token <token>
openchamber tunnel start --profile prod-main
openchamber tunnel start --provider cloudflare --mode quick --qr
openchamber tunnel start --provider cloudflare --mode managed-local --config ~/.cloudflared/config.yml
openchamber tunnel status --all      # Show tunnel state across instances
openchamber tunnel stop --port 3000  # Stop tunnel only (server stays running)
openchamber logs                     # Follow latest instance logs
OPENCODE_PORT=4096 OPENCODE_SKIP_START=true openchamber                    # Connect to external OpenCode server
OPENCODE_HOST=https://myhost:4096 OPENCODE_SKIP_START=true openchamber  # Connect via custom host/HTTPS
openchamber stop                     # Stop server
openchamber update                   # Update to latest

Connect to an existing OpenCode server:

OPENCODE_PORT=4096 OPENCODE_SKIP_START=true openchamber
OPENCODE_HOST=https://myhost:4096 OPENCODE_SKIP_START=true openchamber

Bind managed OpenCode server to all interfaces (use only on trusted networks):

OPENCHAMBER_OPENCODE_HOSTNAME=0.0.0.0 openchamber --port 3000
systemd service (VPN / LAN access)

Run OpenChamber and OpenCode as separate persistent services — useful when you want to access your dev machine over a VPN (e.g. Tailscale) or LAN without a Cloudflare tunnel.

How it works:

  • OpenCode runs as its own service, binding only to localhost.
  • OpenChamber connects to it via OPENCODE_HOST and --host 0.0.0.0 makes it reachable on your VPN IP.
  • --foreground keeps the CLI process alive so systemd can track and restart it.

~/.config/systemd/user/opencode.service

[Unit]
Description=OpenCode Server

[Service]
Type=simple
ExecStart=opencode serve --port 4095
Environment="PATH=/home/linuxbrew/.linuxbrew/bin:/home/linuxbrew/.linuxbrew/sbin:/home/YOU/.local/bin:/home/YOU/.npm-global/bin:/usr/local/bin:/usr/bin:/bin"
Environment=SSH_AUTH_SOCK=%t/ssh-agent.socket
Restart=on-failure
RestartSec=5

[Install]
WantedBy=default.target

Why set PATH and SSH_AUTH_SOCK? systemd user services start with a minimal environment — no shell profile is sourced. Without an explicit PATH, OpenCode won't find tools installed via Homebrew, npm, or ~/.local/bin. Without SSH_AUTH_SOCK, git operations over SSH (push, pull, clone) will fail because the agent socket isn't inherited. Adjust the PATH to match your own tool installation paths. %t expands to $XDG_RUNTIME_DIR (e.g. /run/user/1000), where most SSH agents write their socket.

~/.config/systemd/user/openchamber.service

[Unit]
Description=OpenChamber Web Server
After=opencode.service

[Service]
Type=simple
ExecStart=openchamber serve --port 3000 --host 0.0.0.0 --ui-password your-password --foreground
Environment="OPENCODE_HOST=http://localhost:4095"
Environment="OPENCODE_SKIP_START=true"
Restart=on-failure
RestartSec=5

[Install]
WantedBy=default.target
systemctl --user daemon-reload
systemctl --user enable --now opencode openchamber

OpenChamber will be reachable at http://<your-vpn-hostname>:3000 from any device on your VPN.

Note: --host 0.0.0.0 is required to listen on all interfaces. The default bind address is 127.0.0.1 (localhost only). Use --host <ip> or OPENCHAMBER_HOST=<ip> to bind to a specific interface instead.

Docker
docker compose up -d

Available at http://localhost:3000.

UI Password:

environment:
  UI_PASSWORD: your_secure_password

Cloudflare Tunnel (optional):

environment:
  OPENCHAMBER_TUNNEL_MODE: quick # quick | managed-remote | managed-local
  OPENCHAMBER_TUNNEL_PROVIDER: cloudflare

For managed-remote mode, provide:

environment:
  OPENCHAMBER_TUNNEL_MODE: managed-remote
  OPENCHAMBER_TUNNEL_HOSTNAME: app.example.com
  OPENCHAMBER_TUNNEL_TOKEN: <token>

For managed-local mode, optionally provide:

environment:
  OPENCHAMBER_TUNNEL_MODE: managed-local
  OPENCHAMBER_TUNNEL_CONFIG: /home/openchamber/.cloudflared/config.yml

Managed-local path note: OPENCHAMBER_TUNNEL_CONFIG must point to a path inside the container user home (/home/openchamber/...). If your Cloudflare config references a credentials JSON file, that file path must also be accessible inside the container (mount with volumes).

Reverse proxy notes

  • For a complete reverse proxy setup guide, see docs/REVERSE_PROXY.md.
  • Website docs source lives at packages/docs/content/docs/reverse-proxy.mdx.

Tunnel behavior notes

  • OpenChamber supports one active tunnel per running instance (port).
  • Starting a tunnel with a different mode/provider on the same instance replaces the current tunnel.
  • Replacing or stopping a tunnel revokes existing connect links and invalidates remote tunnel sessions for that instance.
  • Connect links are one-time tokens; generating a new link revokes the previous unused link.

Data Directory Permission Note: The data/ directory is mounted into the container for persistent storage (config, sessions, SSH keys, workspaces). Before running, ensure the directory exists and has proper permissions:

mkdir -p data/openchamber data/opencode/share data/opencode/config data/ssh
chown -R 1000:1000 data/

SSH/Git: If git push/pull fails, run ssh -T git@github.com in terminal.

Features

Chat & Interaction
  • Branchable chat timeline with /undo, /redo, and one-click forks from any turn
  • Multi-agent runs from one prompt with isolated worktrees for safe side-by-side comparisons
  • Voice mode with speech input and read-aloud responses for hands-free workflows
  • Plan/Build mode with a dedicated plan view for drafting and iterating steps
  • Inline comment drafts on diffs, files, and plans - send feedback back to the agent
  • Shell mode via leading ! with inline output
  • Share messages as images
  • Mermaid diagrams render inline with copy/download actions
  • Smart tool UIs for diffs, file operations, permissions, and task progress
Git & GitHub
  • Full Git sidebar with staging, commits, push/pull, branch management, and rebase/merge flows
  • PR creation with AI-generated descriptions, status checks, and merge actions
  • Start sessions from GitHub issues and pull requests with context baked in
  • Multi-remote push and fork-aware PR creation
  • Worktree integration: isolated sessions per branch, merge back with conflict handling
  • Git identities, gitmoji support, and multi-account GitHub auth
Files, Diff & Terminal
  • Workspace file browser with inline editing, syntax highlighting, and markdown preview
  • Beautiful diff viewer with stacked/inline modes, lazy loading for large changesets
  • Integrated terminal with per-directory sessions, tabbed interface, and stable heavy-output performance
  • Clickable file paths in messages - jump to exact line locations
  • File-type icons across all views for faster visual scanning
Web / PWA
  • Cloudflare tunnel with quick, managed-remote, and managed-local modes, secure one-time connect links, and QR onboarding
  • Mobile-first: optimized chat controls, keyboard-safe layouts, drag-to-reorder projects
  • Background notifications and cross-tab session tracking
  • Self-update + restart flow that keeps your server settings intact
  • Installable as PWA with project-aware naming
Desktop (macOS)
  • Connect to remote OpenChamber instances over SSH with dedicated lifecycle flows
  • Project Actions: run dev servers, SSH port forwarding, open remote URLs locally
  • Multi-window support for parallel project workflows
  • "Open In" shortcuts for Finder, Terminal, and your preferred editor
  • Fast switching between local and remote instances
  • Native macOS menu, deep-link handling, and polished startup
VS Code Extension
  • Editor-native: open files from tool output, keep sessions beside your code
  • Agent Manager for parallel multi-model runs from a single prompt
  • Right-click actions: add context, explain selections, improve code in-place
  • Session editor panel, responsive layout, and theme mapping to your editor
  • Edit-style tool results open directly in focused diff views
Customization
  • 18+ built-in themes with light/dark variants
  • Custom themes via JSON files in ~/.config/openchamber/themes/ - hot reload, no restart
  • Configurable keyboard shortcuts for chat, panels, and services
  • Font size, spacing, corner radius, and layout controls
  • Customizable project icons with upload and automatic favicon discovery
  • Skills catalog and local skill management for reusable automation

Read the Guide: Custom Themes

Context & Productivity
  • Token usage, cost breakdowns, and raw message inspection panel
  • Usage quota tracking across multiple providers with pace/prediction indicators
  • Favorite model cycling via keyboard shortcuts
  • Session folders and subfolders with drag-to-reorder
  • Persistent project notes and todos per project
  • Draft persistence per session with expanded focus mode for longer prompts

Roadmap

Active development. Here's what's being worked on or planned:

  • Windows and Linux desktop apps
  • Mobile app with remote instance and laptop connectivity
  • More built-in tunneling options
  • Kanban board for multi-agent management - keeping the human in the loop and in control
  • Custom OpenCode plugins/tools built-in catalog
  • Linear integration
  • Built-in browser for running dev apps with agent integration

Acknowledgments

Independent project, not affiliated with the OpenCode team.

Special thanks to:

  • OpenCode - For the excellent API and extensible architecture.
  • Flexoki - Beautiful color scheme by Steph Ango.
  • Pierre - Fast, beautiful diff viewer with syntax highlighting.
  • Tauri - Desktop application framework.
  • Ghostty-web - Great implementation of a Ghostty web renderer.
  • David Hill - Who inspired me to release this without overthinking.
  • My wife, who - with zero AI background - sat down with the app for the first time and built the firework celebration that plays on every successful push.
  • Every contributor who shaped this project with their PRs, ideas, and attention to detail.

Contributing

See CONTRIBUTING.md for development setup and guidelines.

Docs source lives in packages/docs.

License

MIT

S
Description
No description provided
Readme MIT
82 MiB
Languages
TypeScript 74.8%
JavaScript 19.8%
MDX 4.7%
CSS 0.4%
HTML 0.1%
Other 0.1%