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/update/api/beta/desktop/opencode/<asset>, for example
opencode-desktop-mac-arm64.dmg or opencode-desktop-win-x64.exe. 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
perf/startup.bench.ts times a launch with strobe, from process spawn to what a
person waits for, in pairs of launches of two builds with an interval per case. Every launch runs in a private home
(its own %APPDATA%, XDG folders, database, config and service registration, the developer's OPENCODE_*, OTEL_*
and SENTRY_* environment removed), so it never touches the developer's app, profile or live service.
| Case | Launch | Ends on |
|---|---|---|
| launch to Home | Profile kept from earlier launches; the service already runs, from the bundled CLI. | Home |
| launch to a restored tab | Profile kept; the first launch's onboarding draft is restored. | The composer |
| launch to Home, service stopped | Profile kept; the desktop spawns its service. | Home |
| first launch | No profile or GPU shader caches: right after an install, onboarding opens a draft. | The composer |
| dev start to Home | bun dev:desktop with Vite's caches and out deleted. |
Home |
# A returning user's launches of two packaged builds (pack each with OPENCODE_CHANNEL=beta or prod), or two installed
# channels.
strobe bench perf/startup.bench.ts --target desktop --base exe:<old>/OpenCode.exe --head exe:<new>/OpenCode.exe
strobe bench perf/startup.bench.ts --target desktop --base app:opencode --head app:opencode-beta
# The first launch after an install.
strobe bench perf/startup.bench.ts --target install --base app:opencode --head app:opencode-beta
# The dev loop's cold start, dev against this checkout.
strobe bench perf/startup.bench.ts --target dev --base origin/dev --head .
strobe runs on Windows, macOS and Linux. Windows measures the most; on macOS a packaged launch needs a build whose bundle
id this account has never run (packages/app/perf/README.md lists what each OS gives).
Besides launch.ready, each launch reports its milestones as launch.<name> in ms since spawn: the main process's own
marks from its log (entry, electron, window, served, bundle, logging, storage, init, layers,
visible, service.starting, service.ready; src/main/lifecycle/marks.ts), or each step of the dev command's
output. launch.painted and launch.settled are when the window's pixels first and last changed, cpu.work is the CPU
every process used until ready, and launch.node.* is Node's bootstrap timing in builds that accept --inspect. The
first launch of each build is discarded, because a new binary pays the antivirus scan. The packaged
returning-user cases keep the GPU shader caches their build compiled in earlier launches, as a person's machine has
them; first launch starts each launch without them. --why always adds a profiled
launch per build: the main process and the renderer, both from their first statement.
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.