opencode/packages/desktop
github-actions[bot] 4878a0e30f
release: v2.2.5 (#60744)
Brings the versions and changelog of the published v2.2.5 release back to dev.
2026-10-30 19:18:59 -04:00
..
icons fix(desktop): ship the Icon Composer app icon on macOS (#60020) 2026-10-30 14:40:47 +10:00
resources ci: separate release builds from signing and publishing (#59046) 2026-10-26 14:21:29 -04:00
script feat(app): add worktree picker shortcuts and toggle shell mode (#60713) 2026-10-30 22:12:50 +01:00
src fix(cli): keep loopback requests off environment proxies (#60441) 2026-10-30 10:03:10 -05:00
.gitignore
AGENTS.md feat(plugin): add OpenCode Desktop GUI plugins (#55246) 2026-10-27 10:54:50 +10:00
drizzle.config.ts
electron-builder.config.test.ts fix(desktop): describe macOS folder access in the app bundle (#60503) 2026-10-30 16:28:59 +10:00
electron-builder.config.ts fix(desktop): describe macOS folder access in the app bundle (#60503) 2026-10-30 16:28:59 +10:00
electron.vite.config.test.ts ci: separate release builds from signing and publishing (#59046) 2026-10-26 14:21:29 -04:00
electron.vite.config.ts fix(desktop): prevent main bundle require collision (#60430) 2026-10-30 13:07:47 +10:00
package.json release: v2.2.5 (#60744) 2026-10-30 19:18:59 -04:00
README.md ci: separate release builds from signing and publishing (#59046) 2026-10-26 14:21:29 -04:00
sst-env.d.ts
tsconfig.json

OpenCode Desktop

The OpenCode Desktop app, built with Electron.

Development

bun install
bun dev

Build

bun run build prepares resources/ (icons, metainfo and the bundled CLI) and builds the app into out/. bun run pack then packages it, unsigned, into dist/desktop/ at the repository root: a .app tarball on macOS, the unpacked app on Windows, and AppImage/deb/rpm on Linux. The release workflow signs and notarizes afterwards.

bun run build && bun run pack

Both read the same environment as every other release package:

Variable Meaning
OPENCODE_CHANNEL dev (default), beta, or latest/prod. Picks the app identity, icons and bundled CLI.
OPENCODE_VERSION Version stamped into the app and its packages.
OPENCODE_TARGET darwin-arm64, darwin-x64, windows-arm64, windows-x64, linux-x64, linux-arm64. Defaults to the host.

The CLI is placed outside app.asar in the application's resources directory, and packaging fails if it is missing. bun run build uses packages/cli/dist when that holds the target's CLI. Otherwise dev (and local, unset or unrecognized) downloads the published dev CLI, beta downloads the beta CLI, and prod/latest fail.

bun dev is separate from packaging: it uses local renderer/server mode, the dev app identity, and the CLI source by default. bun dev --download-server <version> instead downloads that CLI version for local development.

Beta channel

Every push to the beta branch publishes OpenCode Beta (ai.opencode.desktop.beta) as version 0.0.0-beta-<run> to the beta update channel. Beta publishes only the desktop app and beta.opencode.ai; the CLI and SDK packages ship from dev. Install it from https://opencode.ai/download/beta/<platform>, for example darwin-aarch64-dmg or windows-x64-nsis. Installed builds update in place on every platform.

Each beta build compiles in its commit and the feat, fix and perf commits among its newest 200 (script/beta-notes.ts, embedded by electron.vite.config.ts; run bun script/beta-notes.ts to preview them). After an update, the beta app lists the ones after the commit of the build the user saw last in a "What's new in beta" dialog, unless release notes are turned off in Settings.

Startup benchmark

bun run bench:startup measures a packaged build from process spawn to the restored tab being ready and the renderer going idle, so dev-server and bundling costs are not part of the numbers.

OPENCODE_CHANNEL=dev bun run build && bun run pack
bun run bench:startup -- --runs 5                                  # warm service (started once, reused by every launch)
bun run bench:startup -- --runs 5 --compare dist/other/OpenCode\ Dev.exe   # A/B: alternate launches of two builds
bun run bench:startup -- --service cold                            # each launch spawns the service
bun run bench:startup -- --fresh                                   # first launch after an install (profile wiped each time)
bun run bench:startup -- --profile-main --profile-renderer --trace # CPU profiles and a Chromium startup trace
bun run bench:startup -- --seed "%APPDATA%\ai.opencode.desktop.dev"   # restore tabs and drafts from an existing profile

The app runs in an isolated home (%TEMP%\opencode-bench-startup): its own %APPDATA%, XDG directories, OpenCode database, config and service registration, with the developer's OPENCODE_* and OTEL_* environment stripped (an inherited OTLP endpoint alone adds a network round trip to every CLI exit). It never attaches to or restarts the developer's live service, and only ever kills the process tree it spawned. --service cold stops the service before each launch so the desktop has to spawn it; the isolated config directory gives that service a private port. One --warmup launch per build is discarded by default because the first launch of a new binary pays the antivirus scan. --compare alternates two builds so machine drift affects both equally; they must bundle the same CLI or the desktop restarts the service on the version mismatch.

Milestones (ms since spawn) come from the main log, the renderer's performance timeline and DOM readiness polled over CDP. Node's bootstrap timing is read from the main process after each run over --inspect (nothing attaches until the run is over): processCreated → nodeStart is Electron's native init, nodeStart → nodeBootstrapped is Node itself, and nodeBootstrapped → appStarting is Electron's JavaScript init plus our main bundle up to its first log line. Renderer idle is the start of the first 500 ms window with under 10 % main-thread task time that stays quiet for --settle-ms; rendererTaskMs is the renderer's total main-thread task time until then. Raw samples are written to dist/bench-startup.

A packaged beta or prod build registers itself as the opencode:// handler when it starts, even from the bench; the installed app takes the registration back on its next launch. Those channels run with HTTPS_PROXY pointed at a closed port (--offline forces it for dev) so the updater's first check fails fast instead of reaching GitHub.