diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1ead53370b..2068338f0c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,16 @@ file is the human-readable summary of what changed and why.
### Desktop / Web UI
+- **The desktop app now identifies itself as CoDev to your operating system** —
+ its application id changed from `ai.opencode.desktop` to `ai.codev.desktop`,
+ its deep links from `opencode://` to `codev://`, and its Linux packages from
+ `opencode` to `codev`. **This is a clean break with no migration**: the app
+ stores everything under a directory named after its id, so settings, session
+ history, the server list, sandboxes and the downloaded sandbox image do not
+ carry over — the old directory is left untouched under `ai.opencode.desktop*`
+ in your application data folder and can be deleted by hand. Any existing
+ `opencode://` link stops working, and an installed build will not update
+ across the rename: install the new one and remove the old.
- **A sandbox's project is named after its folder** — inside a sandbox your
folder is mounted at a fixed path, so every sandbox showed the same
meaningless **workspace** label in the sidebar, tabs and command palette.
diff --git a/DIVERGENCES.md b/DIVERGENCES.md
index d56599d1f1..f86ad8f40e 100644
--- a/DIVERGENCES.md
+++ b/DIVERGENCES.md
@@ -182,16 +182,18 @@ from the squash message first, or use a plain merge.
| `packages/app/src/desktop-menu.ts`, `packages/app/src/components/windows-app-menu.tsx` | App-menu label "OpenCode" → "CoDev Code"; "OpenCode Documentation" item → "CoDev Code Documentation" (href `opencode.ai/docs` stays — real URL); trigger `aria-label` "OpenCode menu" → "CoDev Code menu" (×2). Plus a **"Check for Updates..." item added to the Help menu**, it and a preceding separator both `platforms: ["windows"]`; `desktop-menu.ts` gains an exported `desktopMenuItemDisabled(item, {updaterStatus, commandDisabled})` — sibling to the existing `desktopMenuVisible` — which `WindowsAppMenu` now calls to honour `enabled: "updater"`. | Upstream only ever exposes the update check from the macOS app menu (`platforms: ["macos"]`), and `createMenu` early-returns on non-darwin while `WindowsAppMenu` filters on `"windows"` — so on Windows and Linux there was no menu path to it at all, leaving Settings → General as the only manual entry point. `WindowsAppMenu` renders for both Windows and Linux (`titlebar.tsx` gates on `windows() \|\| linux()`) but filters with the `"windows"` key, so `platforms: ["windows"]` reaches both without widening `DesktopMenuPlatform`. The separator carries the same restriction or macOS's Help menu ends on a stray divider. `enabled: "updater"` was silently ignored in this component — only `command` entries could disable — so a dev build would have shown an item that errors on click; the updater's own `disabled` status is the renderer-side equivalent of the main process's `UPDATER_ENABLED`. | Keep our labels, the Help entry, and `entryDisabled`; take upstream's surrounding menu changes. Rebrand any new "OpenCode" label/aria-label. If upstream adds its own non-macOS update entry, drop ours for theirs. Covered by `packages/app/src/desktop-menu.test.ts` ("offers an update check on every platform exactly once"), which will fail on a duplicate rather than let both ship, plus the `item disabled state` block over `desktopMenuItemDisabled`. Keep the disabled logic in `desktop-menu.ts` rather than inlining it back into the component — it is unreachable from a test there, since the app package has no component-render harness. |
| Hardcoded product-string rebrand in TUI + web app: `packages/tui/src/app.tsx`, `packages/tui/src/routes/session/permission.tsx`, `packages/tui/src/attention.ts` (+ `packages/opencode/test/cli/cmd/tui/attention.test.ts`), `packages/app/src/wsl/settings-model.ts` (+ `.test.ts`), `packages/ui/src/components/favicon.tsx` | User-visible "OpenCode" strings outside the i18n dicts → fork brand: update-complete toast "Successfully updated to OpenCode v…" → "CoDev Code"; permission dialog "…until OpenCode is restarted" → "CoDev Code" (the sibling patterns line was already rebranded); OS-notification fallback title `opencode` → `codev` and built-in sound-pack display name "OpenCode Default" → "CoDev Code Default" (pack id `opencode.default` stays — internal identifier; the three default-title expectations in `packages/opencode/test/cli/cmd/tui/attention.test.ts` follow, while its explicit-title pass-through fixture stays `opencode`); WSL settings actions "Install/Update OpenCode" → "…CoDev Code" (+ test expectations/titles; fixture paths `/usr/local/bin/opencode` stay); PWA `apple-mobile-web-app-title` "OpenCode" → "CoDev Code" (matches the rebranded `site.webmanifest`, favicon-assets row) | These render in surfaces the fork ships (TUI + embedded web UI); the i18n-dict strings remain a known gap (UI branding bullet below). | Keep our strings; take upstream's surrounding changes. Rebrand any new hardcoded user-visible "OpenCode" string in these surfaces; leave identifiers, URLs, and fixture paths. |
| **Universal macOS builds**: `packages/desktop/electron.vite.config.ts`, `packages/desktop/electron-builder.config.ts`, `packages/desktop/package.json` | Three changes that together make `electron-builder --mac --universal` (and `--x64`) produce a working app. (1) The `codev:node-pty-narrower` plugin and its `nodePtyPkg` constant are gone; the main build externalises `"@lydell/node-pty"` itself. (2) `@lydell/node-pty` moves from `devDependencies` to `dependencies`, so the wrapper is actually packed. (3) `mac.x64ArchFiles` is set to `**/app.asar.unpacked/node_modules/**`. | (1) Upstream rewrote the import to `@lydell/node-pty-${process.platform}-${process.arch}` — the **build host's** arch, resolved once, while `electron-vite build` runs before `electron-builder` and emits one bundle for whatever is packaged after it. On Apple Silicon that baked `darwin-arm64` in, so an `--x64` or `--universal` build shipped an Electron shell that starts and then cannot load its terminal backend. The wrapper's own entry does the same platform/arch lookup at run time, so externalising it picks the right binary on the machine that runs the app. (2) The wrapper was a devDependency and therefore never shipped: the old narrowing worked only because it pointed at the *platform* packages, which are `optionalDependencies`. Verified against a packaged build — `asar list` showed `@lydell/node-pty-darwin-*` present and `@lydell/node-pty/index.js` absent — so `require("@lydell/node-pty")` could not resolve. The bundled server (`packages/opencode/dist/node/node.js`) already required the bare wrapper, so this was a live bug in packaged builds before the vite change, not one it introduced. (3) All six platform packages ship in both the x64 and arm64 halves, so eight Mach-O files (node-pty's `pty.node` and its extension-less `spawn-helper`, `@parcel/watcher`, `msgpackr-extract`) are byte-identical across them; `@electron/universal` reads that as a mistake and refuses to merge. They are prebuilt binaries chosen at run time, not per-arch build output. | Keep all three. On conflict take upstream's files and re-apply. Do not reintroduce host-arch narrowing to silence a bundler warning, and do not move `@lydell/node-pty` back to `devDependencies` — the two are load-bearing together. Verify a packaged build rather than trusting the build log: `grep -o "@lydell/node-pty[a-z0-9-]*" out/main/index.js` must print the bare wrapper with no platform suffix, `asar list` must contain `/node_modules/@lydell/node-pty/index.js`, and the app must load its `pty.node` from inside the bundle when run from **outside** this repo — inside it, Node's module walk-up finds the repo's `node_modules` and masks a missing wrapper. |
-| Desktop app text + identifier rebrand: `packages/desktop/**` (README, `package.json`, `src/renderer/index.html`, `src/renderer/i18n/{en,es}.ts`, `src/main/{index,windows,server,logging}.ts`, `src/main/wsl/{sidecar,startup,servers,servers.test}.ts`, `src/main/install-state.test.ts`, `scripts/copy-metainfo.ts`, `electron-builder.config.ts`) plus the WSL/deep-link surfaces it shares with `packages/app` (`src/wsl/{types,settings-model,settings-model.test,settings,dialog-add-server}.ts(x)`, `src/i18n/{en,es}.ts`, `src/pages/layout/{deep-links,helpers.test}.ts`, `src/app.tsx`, `src/index.ts`) and `packages/desktop/src/{preload,renderer}` | Every user-visible "OpenCode" the desktop app renders → "CoDev Code": crash dialogs, window `
`, updater/CLI-installed i18n (en+es, with the CLI-command hint `'opencode'`→`'codev'`), the unpackaged app-name fallback `"OpenCode Dev"`, the AppStream metainfo (product name, description, `developer` → `CoDev` with the upstream `id="ly.anoma"` dropped, the three URLs repointed to the fork repo, upstream's lander screenshot removed), the OS deep-link handler display name, README, and `package.json` (`homepage` → the fork repo, `author` → `CoDev `; both were briefly deleted outright, which broke `deb`/`rpm` packaging — electron-builder derives the Debian `Maintainer` field from `author.email` and refuses to build fpm targets without `homepage`, so the two fields have to carry fork values rather than be absent. `AppImage`/`nsis`/`dmg` do not use fpm and never noticed. Values match the AppStream metainfo `scripts/copy-metainfo.ts` already emits: `CoDev` and ``). Codebase identifiers renamed `Opencode`→`Codev`/`opencode`→`codev` (`WslOpencodeCheck`, `resolveWslOpencode`, `installWslOpencode`, `expectOpencodeVersion`, `releaseOpencodeResolve`, `checkOpencode`/`refreshOpencodeCheck*`/`setOpencodeCheck`, `opencodeChecks`, `wslOpencodeAction`, …) together with the internal wire values whose producer and consumer both ship here (`virtual:opencode-server`, the `opencode:{node-pty,virtual-server,copy-server}` vite plugin names, the `wsl-servers-install-opencode` IPC channel, the `install-opencode` job kind, the `__OPENCODE__` deep-link bridge global, the `opencode:deep-link` renderer event, the `opencode-onboarding-` temp dir, the `wsl.onboarding.installOpencode*` i18n keys), and the server-log collector path `/opencode/log` → `/codev/log` (a real bug fix — the fork CLI writes to `/codev/log`, `packages/core/src/global.ts` `app = "codev"`, so debug-log export was silently collecting nothing). Two stale product-named locals renamed to what they hold (`opencode`→`binary` in wsl/sidecar.ts, `opencode`→`checked` in the servers test). | The fork ships the desktop app, so every surface it shows and every identifier a maintainer reads should say codev. | Keep our strings + identifiers; take upstream's surrounding changes. PRESERVED deliberately, do not rename: `@opencode-ai/*` package ids, the `ai.opencode.desktop*` appIds and `opencode://` scheme + `schemes:["opencode"]` (identity plumbing), the `packageName:"opencode"` Linux package names and `opencode-desktop.desktop` legacy launcher, the persisted on-disk store names (`opencode.settings`, `opencode.{draft,workspace,global,window,updater}.*`, the `opencode` data dir in install-state), the basic-auth username `"opencode"` (must match the server default in `packages/server/src/auth.ts`), and the `../opencode/dist` build paths. |
+| Desktop app text + identifier rebrand: `packages/desktop/**` (README, `package.json`, `src/renderer/index.html`, `src/renderer/i18n/{en,es}.ts`, `src/main/{index,windows,server,logging}.ts`, `src/main/wsl/{sidecar,startup,servers,servers.test}.ts`, `src/main/install-state.test.ts`, `scripts/copy-metainfo.ts`, `electron-builder.config.ts`) plus the WSL/deep-link surfaces it shares with `packages/app` (`src/wsl/{types,settings-model,settings-model.test,settings,dialog-add-server}.ts(x)`, `src/i18n/{en,es}.ts`, `src/pages/layout/{deep-links,helpers.test}.ts`, `src/app.tsx`, `src/index.ts`) and `packages/desktop/src/{preload,renderer}` | Every user-visible "OpenCode" the desktop app renders → "CoDev Code": crash dialogs, window ``, updater/CLI-installed i18n (en+es, with the CLI-command hint `'opencode'`→`'codev'`), the unpackaged app-name fallback `"OpenCode Dev"`, the AppStream metainfo (product name, description, `developer` → `CoDev` with the upstream `id="ly.anoma"` dropped, the three URLs repointed to the fork repo, upstream's lander screenshot removed), the OS deep-link handler display name, README, and `package.json` (`homepage` → the fork repo, `author` → `CoDev `; both were briefly deleted outright, which broke `deb`/`rpm` packaging — electron-builder derives the Debian `Maintainer` field from `author.email` and refuses to build fpm targets without `homepage`, so the two fields have to carry fork values rather than be absent. `AppImage`/`nsis`/`dmg` do not use fpm and never noticed. Values match the AppStream metainfo `scripts/copy-metainfo.ts` already emits: `CoDev` and ``). Codebase identifiers renamed `Opencode`→`Codev`/`opencode`→`codev` (`WslOpencodeCheck`, `resolveWslOpencode`, `installWslOpencode`, `expectOpencodeVersion`, `releaseOpencodeResolve`, `checkOpencode`/`refreshOpencodeCheck*`/`setOpencodeCheck`, `opencodeChecks`, `wslOpencodeAction`, …) together with the internal wire values whose producer and consumer both ship here (`virtual:opencode-server`, the `opencode:{node-pty,virtual-server,copy-server}` vite plugin names, the `wsl-servers-install-opencode` IPC channel, the `install-opencode` job kind, the `__OPENCODE__` deep-link bridge global, the `opencode:deep-link` renderer event, the `opencode-onboarding-` temp dir, the `wsl.onboarding.installOpencode*` i18n keys), and the server-log collector path `/opencode/log` → `/codev/log` (a real bug fix — the fork CLI writes to `/codev/log`, `packages/core/src/global.ts` `app = "codev"`, so debug-log export was silently collecting nothing). Two stale product-named locals renamed to what they hold (`opencode`→`binary` in wsl/sidecar.ts, `opencode`→`checked` in the servers test). | The fork ships the desktop app, so every surface it shows and every identifier a maintainer reads should say codev. | Keep our strings + identifiers; take upstream's surrounding changes. PRESERVED deliberately, do not rename: `@opencode-ai/*` package ids, the persisted on-disk store names (`opencode.settings`, `opencode.{draft,workspace,global,window,updater}.*`, the `opencode` data dir in install-state), the basic-auth username `"opencode"` (must match the server default in `packages/server/src/auth.ts`), and the `../opencode/dist` build paths. |
| `packages/app/src/i18n/{en,es}.ts`, `packages/app/src/pages/layout/deep-links.ts` | App-self-reference "OpenCode" → "CoDev Code" across the shared web UI's i18n dicts (28 strings per locale): settings row descriptions, the update toasts, the WSL onboarding step/status strings, the `dialog.server.description`, `error.chain.mcpFailed`, `sidebar.gettingStarted.line1`, the provider-connect "use {{provider}} models in OpenCode" lines, and `app.name.desktop`. The Zen free-models dialog title (`dialog.model.unpaid.freeModels.title`, "Free models provided by OpenCode") was **neutralized** to "Free models included" / "Modelos gratuitos incluidos" rather than rebranded or kept — it read oddly branded either way. i18n **keys** are untouched (they carry `Opencode`/lowercase `opencode`, which never match the exact-case `OpenCode` in values). The deep-link bridge window type `OpenCodeWindow` → `CodevWindow` (its `__CODEV__` global was already renamed in the desktop row). | These render in the desktop app (and the web console), which the fork ships; closes the "in-app i18n strings" gap for this package. | Keep our strings; take upstream's surrounding changes. PRESERVED deliberately: the real external-service names **"OpenCode Zen"** / **"OpenCode Go"** (in the `opencodeZen.*` provider-connect keys and the "OpenCode Go" upsell), plus every `opencode.ai/*` URL — the fork routes to those actual services, so the display text must match what users land on. The built-in **"OpenCode" theme** name (`packages/ui/src/theme/context.tsx`, the shiki theme id, `desktop-theme.schema.json`) is also kept — it names a specific palette, and the fork ships its own `codev` theme alongside it. |
| Browser-opened docs-site links: `packages/tui/src/app.tsx`, `packages/app/src/desktop-menu.ts`, `packages/app/src/components/{settings-general,settings-v2/general,dialog-custom-provider}.tsx`, `packages/app/src/pages/{home,layout,error}.tsx`, `packages/app/src/i18n/{en,es}.ts`, `packages/opencode/src/cli/cmd/{providers,github.handler}.ts` | Links the app opens in the user's browser repointed from `opencode.ai` to the fork's hosted docs. Each one derives its base from `DOCS_URL` (`packages/codev-brand/src/index.ts`, base64-encoded) rather than hardcoding the host: the site is locale-routed and the Code docs live under `/code`, and no URL takes a trailing slash (the site 308-redirects `…/themes/` to `…/themes`). Sites: TUI "Open docs" command and app Documentation menu item → docs root; settings theme "Learn more" (v1 + v2) → `…/code/themes`; custom-provider dialog link → `…/code/providers#custom-provider`; CLI Cloudflare-gateway hint → `…/code/providers#cloudflare-ai-gateway`; GitHub-agent hint → `…/code/github`; home/layout sidebar Help buttons `opencode.ai/desktop-feedback` → docs root. The error page's report button `opencode.ai/desktop-feedback` → `github.com/quickbeard/codev-code/issues` instead (it's a report action, not a docs lookup), icon `discord` → `github`, and the en/es `error.page.report.*` strings → "…CoDev Code team" / "on GitHub". Help submenu (rendered by the shipped web UI's Windows app menu too): "Support Forum" (upstream Discord invite) deleted — the fork has no forum; "Share Feedback"/"Report a Bug" → `github.com/quickbeard/codev-code/issues/new?template=feature-request.yml`/`bug-report.yml` (the fork's template names are hyphenated, unlike upstream's underscored ones). | Fork users should land on the fork's docs and report issues to the fork, not upstream's site/Discord. NOT repointed: Zen/Go product URLs (`opencode.ai/zen`, `/go`, `/auth`), `$schema`/theme-schema ids, API/share/changelog endpoints, MCP OAuth `client_uri`, prompt-file mentions. Also NOT on this base: the ripgrep mirror (`/codev/docs/code/downloads/…`, see its own row) — it is a `public/` static asset, not locale-routed content, and 404s under `/en/docs/code`. | Keep our URLs, icon, and strings; take upstream's surrounding changes. Repoint any new browser-opened `opencode.ai` docs/help link to the fork docs base; leave the NOT-repointed list. |
| `packages/desktop/src/main/index.ts`, `packages/desktop/src/main/windows.ts` | APP_NAMES + window title → "CoDev Code" (Dev/Beta variants too) | Desktop app display name. | Keep our names. |
-| `packages/desktop/electron-builder.config.ts` | `productName` ×3 → "CoDev Code…"; artifactName → `codev-code-desktop-…`; protocol display name. Auto-update `publish` repointed off GitHub entirely: upstream's beta `anomalyco/opencode-beta` and prod `anomalyco/opencode` collapse to one shared `generic` config (`const publish`) pointing at the fork's MinIO bucket — `url` from `process.env.CODEV_UPDATE_URL` falling back to a base64 `UPDATE_URL` const (same `atob` convention as `packages/codev-brand/src/index.ts`), plus two fields that are load-bearing rather than cosmetic: `channel: "latest"` and `useMultipleRangeRequest: false`. Also a second `extraResources` entry shipping `../opencode/dist/node` → `server-bundle` (filter `node.js` + `*.wasm`, dropping the sourcemap, which outweighs the bundle) so the `@codev/sandbox` providers have a real on-disk server directory to mount into guests; `src/main/sandbox/bundle.ts` (fork-only) resolves it under `process.resourcesPath` by that name. Also the sandbox vz helper: `mac.extraResources` ships `packages/codev-sandbox/native/vzhelper/dist/codev-vmm` (built universal by `scripts/prebuild.ts`) as `Resources/codev-vmm`, and an `afterSign` hook (`signVmmHelper`) re-signs it with `vzhelper.entitlements` (`com.apple.security.virtualization` — an entitlement the Electron app itself must not carry) under the same identity that signed the app, hardened runtime on, timestamp only for non-ad-hoc, before electron-builder's built-in notarization runs. | Installer/product branding, and a packaged build must not offer upstream OpenCode releases as CoDev updates. GitHub could never have worked here: `quickbeard/codev-code` is private and `GitHubProvider.getLatestTagName` requests `/releases/latest` unauthenticated, and the repo has never carried a desktop asset. A bucket also splits nothing — one channel, one prefix. **`channel: "latest"` is required, not a default restated**: `detectUpdateChannel` is on by default, so with it absent `getResolvedPublishConfig` takes the channel from `AppInfo.channel`, which is the version's first semver prerelease component — `8` for `1.18.4-8` — and `getUpdateInfoFileName` then writes `8.yml`/`8-mac.yml`/`8-linux.yml` while the app requests `latest.yml` (`autoUpdater.channel` in `src/main/updater.ts`), i.e. a permanent `ERR_UPDATER_CHANNEL_FILE_NOT_FOUND`. **`useMultipleRangeRequest: false` is required for MinIO**, which answers a multipart range request with the whole object; electron-updater hardcodes the same `false` for its own `s3` provider citing minio/minio#5285 but defaults it to `true` for any `generic` URL that is not `s3.amazonaws.com`. Beta keeps a `publish` block despite the fork shipping no beta installers because `UPDATER_ENABLED` covers that channel and a build with no publish config gets no `app-update.yml` at all, so every check would fail with ENOENT. `${channel}` is deliberately NOT used as a URL macro — `macroExpander` expands it to that same `AppInfo.channel` (`8`), not `CODEV_CHANNEL`. appId (`ai.opencode.desktop`) and the `opencode://` deep-link scheme stay upstream — identity plumbing wired into main/index.ts and app deep-links; do not rename casually. `rpm.packageName` (`opencode`/`opencode-beta`/`opencode-dev`) also stays — it names the installed Linux package, not a publish target. | Keep our names and the whole `publish` const; take upstream's other config. Never drop `channel` or `useMultipleRangeRequest` to "match upstream" — both are covered by `electron-builder.config.test.ts`. If upstream reworks its publish targets, re-point ours rather than merging theirs. Note `generic` has no publisher (`createPublisher` returns null), so installers reach the bucket by hand; build with `--publish never`. Keep the `server-bundle` extraResource and the `codev-vmm` extraResource + `afterSign` hook (all covered by `electron-builder.config.test.ts`) — renaming either strands packaged sandbox providers, and dropping `afterSign` ships a helper without the virtualization entitlement, which fails at VM creation (or fails notarization if left ad-hoc signed). |
+| `packages/desktop/electron-builder.config.ts` | `productName` ×3 → "CoDev Code…"; artifactName → `codev-code-desktop-…`; protocol display name. Auto-update `publish` repointed off GitHub entirely: upstream's beta `anomalyco/opencode-beta` and prod `anomalyco/opencode` collapse to one shared `generic` config (`const publish`) pointing at the fork's MinIO bucket — `url` from `process.env.CODEV_UPDATE_URL` falling back to a base64 `UPDATE_URL` const (same `atob` convention as `packages/codev-brand/src/index.ts`), plus two fields that are load-bearing rather than cosmetic: `channel: "latest"` and `useMultipleRangeRequest: false`. Also a second `extraResources` entry shipping `../opencode/dist/node` → `server-bundle` (filter `node.js` + `*.wasm`, dropping the sourcemap, which outweighs the bundle) so the `@codev/sandbox` providers have a real on-disk server directory to mount into guests; `src/main/sandbox/bundle.ts` (fork-only) resolves it under `process.resourcesPath` by that name. Also the sandbox vz helper: `mac.extraResources` ships `packages/codev-sandbox/native/vzhelper/dist/codev-vmm` (built universal by `scripts/prebuild.ts`) as `Resources/codev-vmm`, and an `afterSign` hook (`signVmmHelper`) re-signs it with `vzhelper.entitlements` (`com.apple.security.virtualization` — an entitlement the Electron app itself must not carry) under the same identity that signed the app, hardened runtime on, timestamp only for non-ad-hoc, before electron-builder's built-in notarization runs. | Installer/product branding, and a packaged build must not offer upstream OpenCode releases as CoDev updates. GitHub could never have worked here: `quickbeard/codev-code` is private and `GitHubProvider.getLatestTagName` requests `/releases/latest` unauthenticated, and the repo has never carried a desktop asset. A bucket also splits nothing — one channel, one prefix. **`channel: "latest"` is required, not a default restated**: `detectUpdateChannel` is on by default, so with it absent `getResolvedPublishConfig` takes the channel from `AppInfo.channel`, which is the version's first semver prerelease component — `8` for `1.18.4-8` — and `getUpdateInfoFileName` then writes `8.yml`/`8-mac.yml`/`8-linux.yml` while the app requests `latest.yml` (`autoUpdater.channel` in `src/main/updater.ts`), i.e. a permanent `ERR_UPDATER_CHANNEL_FILE_NOT_FOUND`. **`useMultipleRangeRequest: false` is required for MinIO**, which answers a multipart range request with the whole object; electron-updater hardcodes the same `false` for its own `s3` provider citing minio/minio#5285 but defaults it to `true` for any `generic` URL that is not `s3.amazonaws.com`. Beta keeps a `publish` block despite the fork shipping no beta installers because `UPDATER_ENABLED` covers that channel and a build with no publish config gets no `app-update.yml` at all, so every check would fail with ENOENT. `${channel}` is deliberately NOT used as a URL macro — `macroExpander` expands it to that same `AppInfo.channel` (`8`), not `CODEV_CHANNEL`. appId is `ai.codev.desktop{,.beta,.dev}` and `rpm.packageName` is `codev`/`codev-beta`/`codev-dev` — see the desktop identity row for the full rename and what it costs. | Keep our names and the whole `publish` const; take upstream's other config. Never drop `channel` or `useMultipleRangeRequest` to "match upstream" — both are covered by `electron-builder.config.test.ts`. If upstream reworks its publish targets, re-point ours rather than merging theirs. Note `generic` has no publisher (`createPublisher` returns null), so installers reach the bucket by hand; build with `--publish never`. Keep the `server-bundle` extraResource and the `codev-vmm` extraResource + `afterSign` hook (all covered by `electron-builder.config.test.ts`) — renaming either strands packaged sandbox providers, and dropping `afterSign` ships a helper without the virtualization entitlement, which fails at VM creation (or fails notarization if left ad-hoc signed). |
| `packages/desktop/src/main/updater.ts` | `autoUpdater.allowDowngrade` `true` → `false`. | Upstream's `true` is safe against immutable GitHub release tags. The fork's feed is a mutable bucket path, where re-uploading or failing to replace a stale `latest.yml` would actively downgrade every client on the next 10-minute poll. | Keep `false`; take upstream's other updater settings. `channel = "latest"` and `allowPrerelease = false` stay as upstream has them — the first must match `publish.channel` in `electron-builder.config.ts`, the second is inert for the `generic` provider (it only steers GitHub tag resolution). |
| Dual-path WSL install: `packages/desktop/src/main/wsl/{runtime,servers}.ts` | `runtime.ts`: three internals gain `export` (`runPowerShell`, `runInteractiveCommand`, `resolveSystem32Command`) — no behavior change. `servers.ts`: the `install-wsl` job calls `installWslRuntimeDualPath` and the `install-distro` job calls `installWslDistroDualPath` (both from the fork-only `install-bundle.ts`, which holds all new logic) instead of `installWslRuntimeElevated` / `installWslDistro` directly; both return the same `WslCommandResult` shape, so the surrounding job/error handling is untouched. | The customer fleet sits behind whitelist-only proxies that cannot reach Microsoft's servers. The dual path downloads the pinned bundle (`wsl.zip`: runtime MSI + Ubuntu-24.04 image) from the codev storage host, enables VirtualMachinePlatform via offline dism + installs the MSI behind one UAC prompt, and falls back to upstream's `wsl --install` / `--web-download` paths only when the bundle cannot be *obtained* — a completed-but-failed elevated install is returned as-is, never retried behind a second UAC prompt. | Keep the two call swaps and the three `export` keywords; take upstream's surrounding changes. If upstream reworks `installWslRuntimeElevated` / `installWslDistro`, keep them exported from `runtime.ts` — `install-bundle.ts` calls them as the fallback path. |
| Desktop sandbox wiring: `packages/desktop/src/main/{index,store-keys}.ts`, `packages/desktop/package.json` | Hooks the fork-only `src/main/sandbox/{bundle,controller,ipc}.ts` modules into startup: `index.ts` constructs the sandbox controller beside the WSL servers controller, adds `sandboxes.stopAll()` to `stopSidecars`, registers the `sandbox-*` IPC channels next to the WSL ones, and calls `sandboxes.initialize()` (all platforms) once the local server is ready; `store-keys.ts` gains `SANDBOXES_KEY = "sandboxes"`; `package.json` gains the `@codev/sandbox` and `@codev/gateway` workspace devDependencies (bundled into the main process by electron-vite, so not runtime dependencies; gateway supplies the sandbox egress broker's credentials and config fragment). | The desktop app is where sandboxes are created and run; `createSandboxController` generalizes the proven WSL-servers controller pattern to the per-platform `@codev/sandbox` providers (macOS vz / Windows wsl / Linux bwrap). | Keep the wiring lines and take upstream's surrounding changes; they are additive and order-insensitive apart from `initialize()` needing the local server ready first. Do not rename the `sandbox-*` channels or the `sandboxes` store key — preload/renderer and persisted sandbox configs depend on them. |
| Desktop sandbox renderer slice: `packages/app/package.json`, `packages/app/src/{index.ts,app.tsx}`, `packages/app/src/context/{platform,server}.tsx`, `packages/app/src/components/{settings-v2/servers.tsx,dialog-select-server.tsx}`, `packages/app/src/wsl/settings.tsx`, `packages/app/src/i18n/{en,es}.ts`, `packages/desktop/src/preload/{index,types}.ts`, `packages/desktop/src/renderer/index.tsx` | Surfaces the fork-only sandbox modules (`packages/app/src/sandbox/*`, `packages/desktop/src/renderer/sandbox/*`) in the UI: app `package.json` exports `./sandbox/types` (mirror of `./wsl/types`); `index.ts` re-exports `useSandboxes` + the sandbox types; `app.tsx` mounts `SandboxesProvider` inside `WslServersProvider`; `platform.tsx` gains optional `sandboxes?: SandboxesPlatform`; `server.tsx` adds the third `ServerConnection.Sidecar` variant `{ variant: "sandbox"; name; projectDir? }` with key `sandbox:` (`projectDir` is the host folder mounted into the guest — the guest only ever sees its own mount point, so this is the only way the app can name a sandbox's project after the folder it works on); `settings-v2/servers.tsx` filters sandbox rows out of the generic list and renders `SandboxServerSettings` beside the WSL section; `dialog-select-server.tsx` `handleRemove` routes `sandbox:` keys to `platform.sandboxes.remove` (like `wsl:` keys) and moves the `tabs.removeServer(key)` + `server.remove(key)` pair *above* both awaited platform removals; `wsl/settings.tsx` `AddServerMenu` shows the menu when either wslServers or sandboxes exist and gains an "Add sandbox" item; i18n gains the `sandbox.*` key family (including the optional per-sandbox CPU/memory fields, macOS-only since only the vz backend applies them, and `sandbox.add.projectDirHint`, the standing explanation above the folder field); preload exposes `api.sandboxes` over the `sandbox-*` channels and `renderer/index.tsx` wires the platform api, loading gate, ready-connection list, and the `startupServer` fallback (fork-only wrapper that validates `sandbox:` default keys the way `availableStartupServer` validates `wsl:` ones). | Ready sandboxes must appear as first-class servers and be manageable from settings; the slice mirrors the WSL vertical exactly so upstream conflicts resolve the same way in both. Removing a server destroys a disk and takes seconds, and the connection leaves the list the moment it finishes, so the tab cleanup has to happen before the await rather than after it. | Keep every listed edit; take upstream's surrounding changes. If upstream adds a `Sidecar` variant or reworks `AddServerMenu`, re-apply the sandbox variant/menu item on top. The `sandbox:` key format is persisted as the default-server key and as `config.id` in the main-process store — never change one side alone. Keep the tab cleanup ahead of the platform removals in `handleRemove`; if upstream reorders it back, re-apply. |
+| Desktop identity: `packages/desktop/src/main/{index,store}.ts`, `packages/desktop/electron-builder.config{,.test}.ts`, `packages/desktop/scripts/copy-metainfo.ts`, `packages/desktop/resources/linux/opencode-desktop.desktop` (deleted), `packages/app/src/pages/layout/{deep-links.ts,helpers.test.ts}`, `packages/codev-brand/test/brand.test.ts` | appId `ai.opencode.desktop{,.beta,.dev}` → `ai.codev.desktop{,.beta,.dev}` (both copies: `APP_IDS` in main and in the builder config, plus the unpackaged fallback); deep-link scheme `opencode://` → `codev://` in all three places that must agree — `protocols.schemes`, `app.setAsDefaultProtocolClient`, and the renderer's `parseUrl` — plus the `argv.startsWith` filter the second-instance handler uses; `rpm.packageName` `opencode`/`opencode-beta`/`opencode-dev` → `codev`/`codev-beta`/`codev-dev`; the hidden `opencode-desktop.desktop` legacy launcher and its `deb`/`rpm` fpm wiring deleted. `src/main/migrate.ts` keeps the old ids — they name the *Tauri* app's directories, not ours. | The appId is the whole desktop identity: `app.setPath("userData", join(appData, appId))`, so it also names the data directory, and it drives the macOS bundle id, the Windows AppUserModelId and NSIS keys, the Linux `.desktop` filename, `executableName` and `StartupWMClass`, and the AppStream metainfo id. Leaving it as opencode meant the fork's app identified itself as upstream to the OS. Renamed as a clean cut with no data migration and no back-compat shim, on the explicit basis that there is no installed base: existing profiles under `appData/ai.opencode.desktop*` are simply orphaned (settings, session history, sandboxes and the downloaded guest image), and a packaged update across the rename would install beside the old app rather than replace it. The legacy launcher went with it — it existed to rescue GNOME/KDE pins from a superseded packaging id, and its `Exec=/opt/OpenCode/...` no longer matched any install layout. | Keep our ids and scheme; take upstream's other config. Never rename the `TAURI_APP_IDS` in `migrate.ts` to match — a blind find-and-replace there points the Tauri migration at a directory that never existed. All three deep-link sites have to move together or links open nothing; `electron-builder.config.test.ts` covers the ids, the package names and the scheme per channel. |
+
| `packages/app/src/context/global.tsx` | Two lines: imports `seedSandboxProjects` from the fork-only `packages/app/src/sandbox/projects.ts` and calls it with `server.list` + `ensureServerCtx`, beside the memo that already creates a ctx per connection. All the logic lives in the fork-only module. | A sandbox exposes exactly one directory (`/workspace` on vz/bwrap, the wslpath translation on WSL) and the guest registers it as its own project, but the app's per-server project list starts empty and nothing ever fills it. With no project, every "New session" entry point — `home-model.openNewSession`, the titlebar's `openNewTab` fallback chain — finds nothing for that server and falls back to another one, so the sandbox looks selected while the session runs on the local server. The seed reads `sync.data.path.directory`, which every server ctx already fetches, so it stays backend-agnostic. The same module also names that project after the sandbox's host folder (via `client.project.update`, the call behind rename-project), because the guest sees only its mount point and every sandbox would otherwise read "workspace" in the sidebar, tabs and palette. | Keep both lines; take upstream's other changes to the file. The call must stay inside the Global context init (it needs `ensureServerCtx`) — `SandboxesProvider` is mounted above `GlobalProvider` in `app.tsx`, so it cannot live there. |
| `packages/app/src/pages/layout/project-avatar-state.ts` | `unread` gains a `connection()` guard before `notification.ensureServerState(server())`, matching the guard `hasPermissions`/`hasQuestions`/`loading` already apply to `sync()`. | `ensureServerState` throws on a key absent from the connection list, and the throw propagates out of a render-time memo, killing the whole renderer. Sidecar connections are listed only while healthy — `readySandboxConnections`/`readyWslConnections` publish a sandbox or WSL server only in the `ready` state — so any removal, restart, or crash of one drops its key while its session tabs are still mounted. Reproduced by removing a sandbox that had an open session tab: `Error: Notification server not found: sandbox:`. | Keep the guard; take upstream's other changes to the hook. This is an upstream bug (WSL servers hit the identical path), so if upstream fixes it their way, drop ours in favour of theirs. |
@@ -259,8 +261,9 @@ from the squash message first, or use a plain merge.
footnote strings and `` all say "OpenCode"; the file asks to be kept in sync with
`packages/ui/src/components/logo.tsx`, which has since moved to the codev wordmark, so
rebranding it means the wordmark *and* the strings together or the page reads inconsistently),
- and the `ai.opencode.desktop` appId / `opencode://` scheme (identity plumbing wired into
- deep-link handling). Desktop app icons are no longer a gap — see the icons row in the table.
+ Desktop app icons are no longer a gap — see the icons row in the table. The
+ `ai.opencode.desktop` appId and `opencode://` scheme are no longer a gap either: both were
+ renamed to `ai.codev.desktop` / `codev://` (see the desktop identity row in the table).
- **URLs**: links the app opens in the browser point at the fork — docs at
the base in `DOCS_URL` (`packages/codev-brand/src/index.ts`, base64-encoded —
the site is locale-routed and the Code docs live under `/code`; no trailing
diff --git a/packages/app/src/components/prompt-input/build-request-parts.test.ts b/packages/app/src/components/prompt-input/build-request-parts.test.ts
index ab84cb6eae..5581d0a23d 100644
--- a/packages/app/src/components/prompt-input/build-request-parts.test.ts
+++ b/packages/app/src/components/prompt-input/build-request-parts.test.ts
@@ -84,7 +84,7 @@ describe("buildRequestParts", () => {
type: "image",
id: "img_external",
filename: "opencode.global.dat",
- sourcePath: "C:\\Users\\Luke\\AppData\\Roaming\\ai.opencode.desktop.beta\\opencode.global.dat",
+ sourcePath: "C:\\Users\\Luke\\AppData\\Roaming\\ai.codev.desktop.beta\\opencode.global.dat",
mime: "text/plain",
dataUrl: "data:text/plain;base64,AAA",
},
@@ -96,7 +96,7 @@ describe("buildRequestParts", () => {
})
expect(result.requestParts.find((part) => part.type === "file")?.filename).toBe(
- "C:\\Users\\Luke\\AppData\\Roaming\\ai.opencode.desktop.beta\\opencode.global.dat",
+ "C:\\Users\\Luke\\AppData\\Roaming\\ai.codev.desktop.beta\\opencode.global.dat",
)
})
diff --git a/packages/app/src/pages/layout/deep-links.ts b/packages/app/src/pages/layout/deep-links.ts
index 9ae80595e4..e45137b458 100644
--- a/packages/app/src/pages/layout/deep-links.ts
+++ b/packages/app/src/pages/layout/deep-links.ts
@@ -1,7 +1,7 @@
export const deepLinkEvent = "codev:deep-link"
const parseUrl = (input: string) => {
- if (!input.startsWith("opencode://")) return
+ if (!input.startsWith("codev://")) return
if (typeof URL.canParse === "function" && !URL.canParse(input)) return
try {
return new URL(input)
diff --git a/packages/app/src/pages/layout/helpers.test.ts b/packages/app/src/pages/layout/helpers.test.ts
index e442496721..8607cdf218 100644
--- a/packages/app/src/pages/layout/helpers.test.ts
+++ b/packages/app/src/pages/layout/helpers.test.ts
@@ -38,24 +38,24 @@ const session = (input: Partial & Pick) =>
describe("layout deep links", () => {
test("parses open-project deep links", () => {
- expect(parseDeepLink("opencode://open-project?directory=/tmp/demo")).toBe("/tmp/demo")
+ expect(parseDeepLink("codev://open-project?directory=/tmp/demo")).toBe("/tmp/demo")
})
test("ignores non-project deep links", () => {
- expect(parseDeepLink("opencode://other?directory=/tmp/demo")).toBeUndefined()
+ expect(parseDeepLink("codev://other?directory=/tmp/demo")).toBeUndefined()
expect(parseDeepLink("https://example.com")).toBeUndefined()
})
test("ignores malformed deep links safely", () => {
- expect(() => parseDeepLink("opencode://open-project/%E0%A4%A%")).not.toThrow()
- expect(parseDeepLink("opencode://open-project/%E0%A4%A%")).toBeUndefined()
+ expect(() => parseDeepLink("codev://open-project/%E0%A4%A%")).not.toThrow()
+ expect(parseDeepLink("codev://open-project/%E0%A4%A%")).toBeUndefined()
})
test("parses links when URL.canParse is unavailable", () => {
const original = Object.getOwnPropertyDescriptor(URL, "canParse")
Object.defineProperty(URL, "canParse", { configurable: true, value: undefined })
try {
- expect(parseDeepLink("opencode://open-project?directory=/tmp/demo")).toBe("/tmp/demo")
+ expect(parseDeepLink("codev://open-project?directory=/tmp/demo")).toBe("/tmp/demo")
} finally {
if (original) Object.defineProperty(URL, "canParse", original)
if (!original) Reflect.deleteProperty(URL, "canParse")
@@ -63,37 +63,37 @@ describe("layout deep links", () => {
})
test("ignores open-project deep links without directory", () => {
- expect(parseDeepLink("opencode://open-project")).toBeUndefined()
- expect(parseDeepLink("opencode://open-project?directory=")).toBeUndefined()
+ expect(parseDeepLink("codev://open-project")).toBeUndefined()
+ expect(parseDeepLink("codev://open-project?directory=")).toBeUndefined()
})
test("collects only valid open-project directories", () => {
const result = collectOpenProjectDeepLinks([
- "opencode://open-project?directory=/a",
- "opencode://other?directory=/b",
- "opencode://open-project?directory=/c",
+ "codev://open-project?directory=/a",
+ "codev://other?directory=/b",
+ "codev://open-project?directory=/c",
])
expect(result).toEqual(["/a", "/c"])
})
test("parses new-session deep links with optional prompt", () => {
- expect(parseNewSessionDeepLink("opencode://new-session?directory=/tmp/demo")).toEqual({ directory: "/tmp/demo" })
- expect(parseNewSessionDeepLink("opencode://new-session?directory=/tmp/demo&prompt=hello%20world")).toEqual({
+ expect(parseNewSessionDeepLink("codev://new-session?directory=/tmp/demo")).toEqual({ directory: "/tmp/demo" })
+ expect(parseNewSessionDeepLink("codev://new-session?directory=/tmp/demo&prompt=hello%20world")).toEqual({
directory: "/tmp/demo",
prompt: "hello world",
})
})
test("ignores new-session deep links without directory", () => {
- expect(parseNewSessionDeepLink("opencode://new-session")).toBeUndefined()
- expect(parseNewSessionDeepLink("opencode://new-session?directory=")).toBeUndefined()
+ expect(parseNewSessionDeepLink("codev://new-session")).toBeUndefined()
+ expect(parseNewSessionDeepLink("codev://new-session?directory=")).toBeUndefined()
})
test("collects only valid new-session deep links", () => {
const result = collectNewSessionDeepLinks([
- "opencode://new-session?directory=/a",
- "opencode://open-project?directory=/b",
- "opencode://new-session?directory=/c&prompt=ship%20it",
+ "codev://new-session?directory=/a",
+ "codev://open-project?directory=/b",
+ "codev://new-session?directory=/c&prompt=ship%20it",
])
expect(result).toEqual([{ directory: "/a" }, { directory: "/c", prompt: "ship it" }])
})
@@ -101,11 +101,11 @@ describe("layout deep links", () => {
test("drains global deep links once", () => {
const target = {
__CODEV__: {
- deepLinks: ["opencode://open-project?directory=/a"],
+ deepLinks: ["codev://open-project?directory=/a"],
},
} as unknown as Window & { __CODEV__?: { deepLinks?: string[] } }
- expect(drainPendingDeepLinks(target)).toEqual(["opencode://open-project?directory=/a"])
+ expect(drainPendingDeepLinks(target)).toEqual(["codev://open-project?directory=/a"])
expect(drainPendingDeepLinks(target)).toEqual([])
})
})
diff --git a/packages/codev-brand/test/brand.test.ts b/packages/codev-brand/test/brand.test.ts
index cb68bd0470..9afeed7675 100644
--- a/packages/codev-brand/test/brand.test.ts
+++ b/packages/codev-brand/test/brand.test.ts
@@ -27,7 +27,10 @@ describe("rebrand", () => {
'import type { Plugin } from "@opencode-ai/plugin"',
'"$schema": "https://opencode.ai/config.json"',
'"opencode-gemini-auth", "opencode-foo@1.2.3", ["opencode-bar", { "option": "value" }]',
- "the opencode:// deep link and the ai.opencode.desktop appId",
+ // The desktop appId and deep-link scheme used to be preserved here. They
+ // are now ai.codev.desktop and codev://, so upstream doc text mentioning
+ // opencode:// is stale rather than deliberately kept — rebrand still
+ // leaves it alone, which doc sync may want to revisit.
"state lives in opencode.db",
]
for (const line of preserved) expect(rebrand(line)).toBe(line)
diff --git a/packages/codev-sandbox/src/socket.test.ts b/packages/codev-sandbox/src/socket.test.ts
index dfefafede6..ee9d4be2e8 100644
--- a/packages/codev-sandbox/src/socket.test.ts
+++ b/packages/codev-sandbox/src/socket.test.ts
@@ -5,7 +5,7 @@ import { SANDBOX_SUN_PATH_MAX, sandboxSocketDir } from "./socket"
// The failure this guards against is silent: an over-long AF_UNIX path is
// truncated at bind time, so the socket appears where nobody is listening.
test("socket paths stay inside the AF_UNIX limit for realistic sandbox names", () => {
- const appData = "/Users/someone/Library/Application Support/ai.opencode.desktop.dev/sandboxes"
+ const appData = "/Users/someone/Library/Application Support/ai.codev.desktop.dev/sandboxes"
for (const name of ["a", "codev-work", "a-rather-long-sandbox-name-someone-might-type"]) {
for (const file of ["server.sock", "broker.sock"]) {
const path = join(sandboxSocketDir(join(appData, name)), file)
diff --git a/packages/desktop/electron-builder.config.test.ts b/packages/desktop/electron-builder.config.test.ts
index 27f363ec97..ffe15a931f 100644
--- a/packages/desktop/electron-builder.config.test.ts
+++ b/packages/desktop/electron-builder.config.test.ts
@@ -3,12 +3,10 @@ import type { Configuration } from "electron-builder"
import type { GenericServerOptions } from "builder-util-runtime"
import { GenericProvider } from "electron-updater/out/providers/GenericProvider"
-const legacyDesktopEntry = "resources/linux/opencode-desktop.desktop"
-
const channels = [
- { channel: "dev", appId: "ai.opencode.desktop.dev" },
- { channel: "beta", appId: "ai.opencode.desktop.beta" },
- { channel: "prod", appId: "ai.opencode.desktop" },
+ { channel: "dev", appId: "ai.codev.desktop.dev", packageName: "codev-dev" },
+ { channel: "beta", appId: "ai.codev.desktop.beta", packageName: "codev-beta" },
+ { channel: "prod", appId: "ai.codev.desktop", packageName: "codev" },
] as const
for (const channel of channels) {
@@ -26,6 +24,26 @@ for (const channel of channels) {
expect(config.extraMetadata?.desktopName).toBe(`${channel.appId}.desktop`)
expect(config.linux?.executableName).toBe(channel.appId)
expect(config.linux?.desktop?.entry?.StartupWMClass).toBe(channel.appId)
+ expect(config.rpm?.packageName).toBe(channel.packageName)
+ })
+}
+
+// The scheme the OS hands back to us on a deep link. It is registered here and
+// consumed by app.setAsDefaultProtocolClient plus parseUrl in the renderer's
+// deep-links module: all three have to agree or the link opens nothing.
+for (const channel of channels) {
+ test(`registers the codev deep-link scheme for ${channel.channel}`, async () => {
+ const previous = process.env.CODEV_CHANNEL
+ process.env.CODEV_CHANNEL = channel.channel
+
+ const module = await import(`./electron-builder.config.ts?scheme=${channel.channel}`)
+ const config = module.default as Configuration
+
+ if (previous === undefined) delete process.env.CODEV_CHANNEL
+ else process.env.CODEV_CHANNEL = previous
+
+ const protocols = config.protocols as { schemes: string[] }
+ expect(protocols.schemes).toEqual(["codev"])
})
}
@@ -145,22 +163,3 @@ test("ships and re-signs the codev-vmm helper on macOS", async () => {
expect(typeof config.afterSign).toBe("function")
})
-test("keeps a hidden prod launcher for old Linux pins", async () => {
- const previous = process.env.CODEV_CHANNEL
- process.env.CODEV_CHANNEL = "prod"
-
- const module = await import("./electron-builder.config.ts?compat=prod")
- const config = module.default as Configuration
-
- if (previous === undefined) delete process.env.CODEV_CHANNEL
- else process.env.CODEV_CHANNEL = previous
-
- expect(config.deb?.fpm?.[0]).toEndWith(`${legacyDesktopEntry}=/usr/share/applications/opencode-desktop.desktop`)
- expect(config.rpm?.fpm?.[0]).toEndWith(`${legacyDesktopEntry}=/usr/share/applications/opencode-desktop.desktop`)
-
- const desktop = await Bun.file(legacyDesktopEntry).text()
- expect(desktop).toContain("Exec=/opt/OpenCode/ai.opencode.desktop %U")
- expect(desktop).toContain("Icon=ai.opencode.desktop")
- expect(desktop).toContain("StartupWMClass=ai.opencode.desktop")
- expect(desktop).toContain("NoDisplay=true")
-})
diff --git a/packages/desktop/electron-builder.config.ts b/packages/desktop/electron-builder.config.ts
index 78db010d20..7176ff036e 100644
--- a/packages/desktop/electron-builder.config.ts
+++ b/packages/desktop/electron-builder.config.ts
@@ -10,11 +10,6 @@ const execFileAsync = promisify(execFile)
const packageDir = path.dirname(fileURLToPath(import.meta.url))
const rootDir = path.resolve(packageDir, "../..")
const signScript = path.join(rootDir, "script", "sign-windows.ps1")
-// The Electron 42 packaging update briefly installed Linux launchers/icons under
-// "opencode-desktop". Keep that hidden desktop entry around so existing GNOME/KDE
-// pins still resolve after the canonical app id changes back to ai.opencode.desktop.
-const legacyDesktopEntry = path.join(packageDir, "resources", "linux", "opencode-desktop.desktop")
-const legacyDesktopEntryFpm = `${legacyDesktopEntry}=/usr/share/applications/opencode-desktop.desktop`
async function signWindows(configuration: { path: string }) {
if (process.platform !== "win32") return
@@ -67,9 +62,9 @@ const channel = (() => {
})()
const APP_IDS = {
- dev: "ai.opencode.desktop.dev",
- beta: "ai.opencode.desktop.beta",
- prod: "ai.opencode.desktop",
+ dev: "ai.codev.desktop.dev",
+ beta: "ai.codev.desktop.beta",
+ prod: "ai.codev.desktop",
} as const
// Base64 (like packages/codev-brand/src/index.ts) to keep the host out of
@@ -102,8 +97,8 @@ const getBase = (appId: string): Configuration => ({
buildResources: "resources",
},
// Linux launchers are .desktop files, so this is the desktop file name,
- // not just the app id. For prod, app id "ai.opencode.desktop" becomes
- // "ai.opencode.desktop.desktop".
+ // not just the app id. For prod, app id "ai.codev.desktop" becomes
+ // "ai.codev.desktop.desktop".
// https://developer.gnome.org/documentation/guidelines/maintainer/integrating.html
// https://www.electron.build/docs/linux/
extraMetadata: {
@@ -162,7 +157,7 @@ const getBase = (appId: string): Configuration => ({
},
protocols: {
name: "CoDev Code",
- schemes: ["opencode"],
+ schemes: ["codev"],
},
win: {
icon: `resources/icons/icon.ico`,
@@ -203,7 +198,7 @@ function getConfig() {
...base,
appId,
productName: "CoDev Code Dev",
- rpm: { packageName: "opencode-dev" },
+ rpm: { packageName: "codev-dev" },
}
}
case "beta": {
@@ -211,13 +206,13 @@ function getConfig() {
...base,
appId,
productName: "CoDev Code Beta",
- protocols: { name: "CoDev Code Beta", schemes: ["opencode"] },
+ protocols: { name: "CoDev Code Beta", schemes: ["codev"] },
// The fork ships no beta installers, but UPDATER_ENABLED covers this
// channel, and a build with no publish config gets no app-update.yml at
// all — every check would fail with ENOENT. Sharing prod's feed keeps
// the unused channel functional rather than broken.
publish,
- rpm: { packageName: "opencode-beta" },
+ rpm: { packageName: "codev-beta" },
}
}
case "prod": {
@@ -225,10 +220,9 @@ function getConfig() {
...base,
appId,
productName: "CoDev Code",
- protocols: { name: "CoDev Code", schemes: ["opencode"] },
+ protocols: { name: "CoDev Code", schemes: ["codev"] },
publish,
- deb: { fpm: [legacyDesktopEntryFpm] },
- rpm: { packageName: "opencode", fpm: [legacyDesktopEntryFpm] },
+ rpm: { packageName: "codev" },
}
}
}
diff --git a/packages/desktop/resources/linux/opencode-desktop.desktop b/packages/desktop/resources/linux/opencode-desktop.desktop
deleted file mode 100644
index a5f677412c..0000000000
--- a/packages/desktop/resources/linux/opencode-desktop.desktop
+++ /dev/null
@@ -1,10 +0,0 @@
-[Desktop Entry]
-Name=OpenCode
-Exec=/opt/OpenCode/ai.opencode.desktop %U
-Terminal=false
-Type=Application
-Icon=ai.opencode.desktop
-StartupWMClass=ai.opencode.desktop
-NoDisplay=true
-Comment=Open source AI coding agent
-Categories=Development;
diff --git a/packages/desktop/scripts/copy-metainfo.ts b/packages/desktop/scripts/copy-metainfo.ts
index 00d4460d24..8773c912ea 100644
--- a/packages/desktop/scripts/copy-metainfo.ts
+++ b/packages/desktop/scripts/copy-metainfo.ts
@@ -3,7 +3,7 @@ import { resolveChannel } from "./utils"
const arg = process.argv[2]
const channel = arg === "dev" || arg === "beta" || arg === "prod" ? arg : resolveChannel()
-const appId = channel === "prod" ? "ai.opencode.desktop" : `ai.opencode.desktop.${channel}`
+const appId = channel === "prod" ? "ai.codev.desktop" : `ai.codev.desktop.${channel}`
const productName =
channel === "prod" ? "CoDev Code" : `CoDev Code ${channel.charAt(0).toUpperCase() + channel.slice(1)}`
const summary = `Open source AI coding agent${channel !== "prod" ? ` (${channel})` : ""}`
diff --git a/packages/desktop/src/main/index.ts b/packages/desktop/src/main/index.ts
index 96142d1503..0941c0a617 100644
--- a/packages/desktop/src/main/index.ts
+++ b/packages/desktop/src/main/index.ts
@@ -57,9 +57,9 @@ const APP_NAMES: Record = {
prod: "CoDev Code",
}
const APP_IDS: Record = {
- dev: "ai.opencode.desktop.dev",
- beta: "ai.opencode.desktop.beta",
- prod: "ai.opencode.desktop",
+ dev: "ai.codev.desktop.dev",
+ beta: "ai.codev.desktop.beta",
+ prod: "ai.codev.desktop",
}
const TEST_ONBOARDING = process.env.CODEV_TEST_ONBOARDING === "1"
const jsCallStackFeature = "DocumentPolicyIncludeJSCallStacksInCrashReports"
@@ -122,7 +122,7 @@ const main = Effect.gen(function* () {
process.env.CODEV_DISABLE_EMBEDDED_WEB_UI = "true"
- const appId = app.isPackaged ? APP_IDS[CHANNEL] : "ai.opencode.desktop.dev"
+ const appId = app.isPackaged ? APP_IDS[CHANNEL] : "ai.codev.desktop.dev"
const onboardingTestRoot = ((): string | undefined => {
if (!TEST_ONBOARDING) return
@@ -208,7 +208,7 @@ const main = Effect.gen(function* () {
preferAppEnv(app.getPath("userData"))
app.on("second-instance", (_event: Event, argv: string[]) => {
- const urls = argv.filter((arg: string) => arg.startsWith("opencode://"))
+ const urls = argv.filter((arg: string) => arg.startsWith("codev://"))
if (urls.length) {
logger.log("deep link received via second-instance", { urls })
emitDeepLinks(urls)
@@ -273,7 +273,7 @@ const main = Effect.gen(function* () {
}),
),
)
- app.setAsDefaultProtocolClient("opencode")
+ app.setAsDefaultProtocolClient("codev")
registerRendererProtocol()
setDockIcon()
const updater = setupAutoUpdater(stopSidecars)
diff --git a/packages/desktop/src/main/migrate.ts b/packages/desktop/src/main/migrate.ts
index 70e3dc9c75..ee787dd51a 100644
--- a/packages/desktop/src/main/migrate.ts
+++ b/packages/desktop/src/main/migrate.ts
@@ -21,7 +21,10 @@ function tauriDir(id: string) {
}
}
-// The Tauri app identifier changes between dev/beta/prod builds.
+// The Tauri app identifier changes between dev/beta/prod builds. These name the
+// directories the *old Tauri app* wrote, so they keep the ai.opencode.desktop
+// ids even though the Electron app is now ai.codev.desktop — renaming them here
+// would point the migration at a directory that never existed.
const TAURI_APP_IDS: Record = {
dev: "ai.opencode.desktop.dev",
beta: "ai.opencode.desktop.beta",
diff --git a/packages/desktop/src/main/store.ts b/packages/desktop/src/main/store.ts
index 687429ed43..69345b0458 100644
--- a/packages/desktop/src/main/store.ts
+++ b/packages/desktop/src/main/store.ts
@@ -11,7 +11,7 @@ const cache = new Map()
// We cannot instantiate the electron-store at module load time because
// module import hoisting causes this to run before app.setPath("userData", ...)
// in index.ts has executed, which would result in files being written to the default directory
-// (e.g. bad: %APPDATA%\@opencode-ai\desktop\opencode.settings vs good: %APPDATA%\ai.opencode.desktop.dev\opencode.settings).
+// (e.g. bad: %APPDATA%\@opencode-ai\desktop\opencode.settings vs good: %APPDATA%\ai.codev.desktop.dev\opencode.settings).
export function getStore(name = SETTINGS_STORE) {
const cached = cache.get(name)
if (cached) return cached