From 13de655fd7ee81f833258479feb8329e2258b4a6 Mon Sep 17 00:00:00 2001 From: Quickbeard Date: Mon, 24 Aug 2026 16:03:29 +0700 Subject: [PATCH] feat(desktop): rebrand the persisted store names to codev MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The desktop app kept its settings, recent servers and per-project UI state in files named after upstream — `opencode.settings`, `opencode.global.dat`, `opencode.workspace.*.dat` and friends — and the browser build used the same names as its localStorage prefixes. The troubleshooting docs name those files so a user can delete them to reset the app, which makes them a user-visible surface rather than an internal id. Rename them all to `codev.*`, and carry existing state across with two one-time migrations: `store-rebrand.ts` renames every pre-rebrand file in the desktop userData directory at startup — before the first store opens and before the onboarding scan, or a store would write a fresh file over the migration's target — and `storage-rebrand.ts` does the same for localStorage keys. The Tauri importer still reads Tauri's `opencode.*.dat` files, since that is what Tauri wrote, but rebrands each target store name on the way in. Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 12 +++ DIVERGENCES.md | 12 +-- .../performance/timeline-stability/fixture.ts | 2 +- .../timeline/timeline-test-helpers.ts | 4 +- .../regression/cross-server-tab-close.spec.ts | 8 +- .../file-browser-sidebar-tab-switch.spec.ts | 8 +- .../home-session-menu-first-click.spec.ts | 2 +- .../new-session-panel-corner.spec.ts | 4 +- .../new-session-reuses-draft.spec.ts | 6 +- .../remote-session-settings.spec.ts | 4 +- .../e2e/regression/remote-tab-busy.spec.ts | 4 +- .../e2e/regression/review-open-file.spec.ts | 8 +- .../review-state-persistence.spec.ts | 4 +- .../e2e/regression/review-tab-switch.spec.ts | 4 +- .../review-terminal-stacked.spec.ts | 2 +- .../regression/session-actions-menu.spec.ts | 2 +- .../session-delete-navigation.spec.ts | 2 +- .../session-list-path-loading.spec.ts | 2 +- .../session-menu-delete-item.spec.ts | 2 +- .../session-todo-dock-navigation.spec.ts | 4 +- .../regression/sidebar-persistence.spec.ts | 2 +- .../sidebar-pinned-sessions.spec.ts | 8 +- .../regression/sidebar-project-scope.spec.ts | 4 +- .../sidebar-section-collapse.spec.ts | 2 +- .../sidebar-session-menu-first-click.spec.ts | 2 +- .../regression/sidebar-toggle-search.spec.ts | 2 +- .../subagent-child-navigation.spec.ts | 4 +- .../regression/tab-navigate-mousedown.spec.ts | 2 +- .../terminal-composer-focus.spec.ts | 2 +- .../regression/terminal-tab-switch.spec.ts | 4 +- .../app/e2e/smoke/session-timeline.spec.ts | 6 +- .../prompt-input/build-request-parts.test.ts | 6 +- packages/app/src/context/language.tsx | 2 +- packages/app/src/entry.tsx | 4 +- packages/app/src/utils/persist.test.ts | 4 +- packages/app/src/utils/persist.ts | 14 ++-- .../app/src/utils/storage-rebrand.test.ts | 77 +++++++++++++++++++ packages/app/src/utils/storage-rebrand.ts | 36 +++++++++ packages/desktop/src/main/index.ts | 5 ++ .../desktop/src/main/install-state.test.ts | 4 +- packages/desktop/src/main/install-state.ts | 2 +- packages/desktop/src/main/migrate.ts | 18 ++--- .../desktop/src/main/store-cleanup.test.ts | 42 +++++----- packages/desktop/src/main/store-cleanup.ts | 4 +- packages/desktop/src/main/store-file.test.ts | 2 +- packages/desktop/src/main/store-keys.ts | 2 +- .../desktop/src/main/store-rebrand.test.ts | 60 +++++++++++++++ packages/desktop/src/main/store-rebrand.ts | 40 ++++++++++ packages/desktop/src/main/store.ts | 4 +- packages/desktop/src/main/updater.ts | 2 +- packages/desktop/src/main/windows.ts | 2 +- packages/desktop/src/renderer/i18n/index.ts | 2 +- packages/desktop/src/renderer/index.tsx | 2 +- 53 files changed, 353 insertions(+), 115 deletions(-) create mode 100644 packages/app/src/utils/storage-rebrand.test.ts create mode 100644 packages/app/src/utils/storage-rebrand.ts create mode 100644 packages/desktop/src/main/store-rebrand.test.ts create mode 100644 packages/desktop/src/main/store-rebrand.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 21b5ab8cb7..751d1871d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -438,6 +438,18 @@ already in use` and no way forward short of finding the file in a temporary ### Desktop / Web UI +- **The app's saved state is filed under CoDev's own name** — the desktop app + kept its settings, recent servers and per-project UI state in files named + after upstream (`opencode.settings`, `opencode.global.dat`, + `opencode.workspace.*.dat`) in its data directory, and the browser build did + the same in local storage. Since the troubleshooting docs tell you to delete + those files by name to reset the app, you were reading someone else's product + name out of your own folder. They are now `codev.settings`, + `codev.global.dat`, `codev.workspace.*.dat` and so on. Upgrading renames the + files you already have on first launch, so nothing is lost and nothing needs + to be re-entered. + ([#317](https://github.com/quickbeard/codev-code/pull/317)) + - **Switching between Code and Work brings the project back too** — the switch restored the session you were last in, and its tabs, but left the sidebar on no project at all: nothing highlighted under Projects, and Pinned and Recents diff --git a/DIVERGENCES.md b/DIVERGENCES.md index 0d7ce4ea11..e6f1c76170 100644 --- a/DIVERGENCES.md +++ b/DIVERGENCES.md @@ -188,7 +188,8 @@ 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"; "OpenCode Documentation" item → "CoDev Documentation" (href `opencode.ai/docs` stays — real URL); trigger `aria-label` "OpenCode menu" → "CoDev 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" (+ 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" (originally "CoDev Code"; the "Code" suffix was dropped in 2026-08 once the app carried a Work section beside 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` → `<name>CoDev</name>` 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 <minhnh49@viettel.com.vn>`; 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: `<developer><name>CoDev</name>` and `<url type="homepage">`). 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 `<xdgData>/opencode/log` → `/codev/log` (a real bug fix — the fork CLI writes to `<xdgData>/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. | +| 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" (originally "CoDev Code"; the "Code" suffix was dropped in 2026-08 once the app carried a Work section beside Code): crash dialogs, window `<title>`, 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` → `<name>CoDev</name>` 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 <minhnh49@viettel.com.vn>`; 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: `<developer><name>CoDev</name>` and `<url type="homepage">`). 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 `<xdgData>/opencode/log` → `/codev/log` (a real bug fix — the fork CLI writes to `<xdgData>/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 `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. The persisted store names were preserved here originally and are now rebranded with a migration — see the persisted-store-name row below. | +| Persisted store names: `packages/app/src/utils/persist.ts`, `packages/app/src/{entry.tsx,context/language.tsx}`, `packages/desktop/src/main/{index,store-keys,store,store-cleanup,install-state,windows,updater,migrate}.ts`, `packages/desktop/src/renderer/{index.tsx,i18n/index.ts}`, plus the tests and `packages/app/e2e` specs that seed them | Every persisted store the app names is `codev.*` instead of `opencode.*`: the desktop store files (`codev.settings`, `codev.updater`, `codev.{global,workspace,draft,window}.*.dat` under `%APPDATA%\ai.codev.desktop` and its per-OS equivalents) and the browser build's localStorage keys, which prefix the same store names (`codev.global.dat:server`). `persist.ts`'s `GLOBAL_STORAGE`, `WINDOW_STORAGE`, `LOCAL_PREFIX` (the eviction filter) and the `workspaceStorage`/`draftStorage` name builders carry the new prefix; `store-cleanup.ts`'s `storeKind` regexes, `install-state.ts`'s settings-file check and `windows.ts`'s `windowDataFile` follow. Existing state is carried across by two fork-only one-time migrations: `packages/desktop/src/main/store-rebrand.ts` renames every `opencode.*` file in `userData` on startup — called from `main/index.ts` immediately after `app.setPath("userData", …)`, before the first `getStore()` and before the onboarding directory scan — and `packages/app/src/utils/storage-rebrand.ts` does the same for localStorage keys, called from `persist.ts` module init and from `entry.tsx`'s pre-render read. Both keep an existing `codev.*` target and leave the leftover alone rather than overwriting live state. The Tauri importer in `migrate.ts` keeps reading Tauri's `opencode.*.dat` files (that is what Tauri wrote) but rebrands each target store name through `rebrandStoreName`. | The desktop troubleshooting docs tell users to delete these files by name to reset the app, so they are user-visible, not an internal id — a user following that page should not be reading upstream's product name out of their own app data directory. This reverses the original decision to preserve them, which rested on the orphaned-state cost; the migrations are what make the rename safe. | Keep the new names and both migration modules together — the names without the migrations silently reset every existing install. Take upstream's surrounding changes. The startup call site is load-bearing: do not move `renameLegacyStoreFiles` below `initializeOldLayoutEligibility` or any other `getStore()` caller, or the store opens under the new name first, writes a fresh file, and the rename then finds its target occupied and strands the user's real state. Same for the `persist.ts` module-init call. The migrations can be deleted once no supported upgrade path still has pre-rebrand files — not before. `migrate.ts`'s source filenames stay `opencode.*`. | | `packages/app/src/i18n/{en,es}.ts`, `packages/app/src/pages/layout/deep-links.ts` | App-self-reference "OpenCode" → "CoDev" 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 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" (Dev/Beta variants too) | Desktop app display name ("Code" dropped once the app carried a Work section beside Code). | Keep our names. | @@ -367,10 +368,11 @@ from the squash message first, or use a plain merge. the user-visible one are now decoupled (see that table row). The install directory has since stopped deriving from it at all — the assisted installer names it after `productFilename` (`CoDev`) — but the updater cache still does, so the override stays load-bearing. The desktop app's persisted - store file names (`opencode.settings`, `opencode.{draft,workspace,global,window,updater}.*` - in `%APPDATA%\ai.codev.desktop`) are the same shape of leak and are still upstream-branded - deliberately — unlike a derived name, renaming those orphans existing state, so it needs a - migration rather than a rename (see the desktop-rebrand row). + store file names (`%APPDATA%\ai.codev.desktop\opencode.settings` and friends) were the same + shape of leak and stayed upstream-branded until the troubleshooting docs had to name them to + users. They are now `codev.*`, which — unlike a derived name — orphans existing state unless + it is carried across, so the rename ships with a one-time migration (see the + persisted-store-name row in the table). - **VS Code extension**: ships as a locally built `.vsix` (`cd sdks/vscode && bun run vsix`), not a Marketplace listing. Fully rebranded for that path: `codev.*` command IDs + CoDev Code titles, `codev` terminal launching `codev --port`, rebranded README body, and the CLI's IDE diff --git a/packages/app/e2e/performance/timeline-stability/fixture.ts b/packages/app/e2e/performance/timeline-stability/fixture.ts index e63445e87d..9828687920 100644 --- a/packages/app/e2e/performance/timeline-stability/fixture.ts +++ b/packages/app/e2e/performance/timeline-stability/fixture.ts @@ -139,7 +139,7 @@ export async function setupTimeline( }, input.settings ?? {}) if (input.locale) { await page.addInitScript((locale) => { - localStorage.setItem("opencode.global.dat:language", JSON.stringify({ locale })) + localStorage.setItem("codev.global.dat:language", JSON.stringify({ locale })) }, input.locale) } if (input.reducedMotion) await page.emulateMedia({ reducedMotion: "reduce" }) diff --git a/packages/app/e2e/performance/timeline/timeline-test-helpers.ts b/packages/app/e2e/performance/timeline/timeline-test-helpers.ts index 5028373c55..275ef9cca9 100644 --- a/packages/app/e2e/performance/timeline/timeline-test-helpers.ts +++ b/packages/app/e2e/performance/timeline/timeline-test-helpers.ts @@ -42,14 +42,14 @@ export async function installStressSessionTabs(page: Page, input?: { draftID?: s await page.addInitScript( ({ directory, sessionIDs, dirBase64, server, draftID }) => { localStorage.setItem( - "opencode.global.dat:server", + "codev.global.dat:server", JSON.stringify({ projects: { local: [{ worktree: directory, expanded: true }] }, lastProject: { local: directory }, }), ) localStorage.setItem( - "opencode.window.browser.dat:tabs", + "codev.window.browser.dat:tabs", JSON.stringify([ ...sessionIDs.map((sessionId) => ({ type: "session", diff --git a/packages/app/e2e/regression/cross-server-tab-close.spec.ts b/packages/app/e2e/regression/cross-server-tab-close.spec.ts index 159b5a5067..a3206a2254 100644 --- a/packages/app/e2e/regression/cross-server-tab-close.spec.ts +++ b/packages/app/e2e/regression/cross-server-tab-close.spec.ts @@ -12,9 +12,9 @@ test("closing the active server's last tab opens the remaining server tab", asyn await page.addInitScript( ({ serverB, sessionA, sessionB }) => { localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } })) - localStorage.setItem("opencode.global.dat:server", JSON.stringify({ list: [serverB] })) + localStorage.setItem("codev.global.dat:server", JSON.stringify({ list: [serverB] })) localStorage.setItem( - "opencode.window.browser.dat:tabs", + "codev.window.browser.dat:tabs", JSON.stringify([ { type: "session", server: "http://127.0.0.1:4096", sessionId: sessionA }, { type: "session", server: serverB, sessionId: sessionB }, @@ -50,9 +50,9 @@ test("legacy session routes preserve an existing tab's server", async ({ page }) await page.addInitScript( ({ serverB, sessionB }) => { localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } })) - localStorage.setItem("opencode.global.dat:server", JSON.stringify({ list: [serverB] })) + localStorage.setItem("codev.global.dat:server", JSON.stringify({ list: [serverB] })) localStorage.setItem( - "opencode.window.browser.dat:tabs", + "codev.window.browser.dat:tabs", JSON.stringify([{ type: "session", server: serverB, sessionId: sessionB }]), ) }, diff --git a/packages/app/e2e/regression/file-browser-sidebar-tab-switch.spec.ts b/packages/app/e2e/regression/file-browser-sidebar-tab-switch.spec.ts index 136232c211..aeeaed072e 100644 --- a/packages/app/e2e/regression/file-browser-sidebar-tab-switch.spec.ts +++ b/packages/app/e2e/regression/file-browser-sidebar-tab-switch.spec.ts @@ -126,22 +126,22 @@ async function setup(page: Page) { ({ directory, server, sessionID }) => { localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } })) localStorage.setItem( - "opencode.global.dat:server", + "codev.global.dat:server", JSON.stringify({ projects: { local: [{ worktree: directory, expanded: true }] }, lastProject: { local: directory }, }), ) localStorage.setItem( - "opencode.global.dat:layout", + "codev.global.dat:layout", JSON.stringify({ review: { diffStyle: "split", panelOpened: true } }), ) localStorage.setItem( - "opencode.global.dat:review-panel-v2", + "codev.global.dat:review-panel-v2", JSON.stringify({ sidebarOpened: true, sidebarWidth: 240, expandMode: "collapse" }), ) localStorage.setItem( - "opencode.window.browser.dat:tabs", + "codev.window.browser.dat:tabs", JSON.stringify([{ type: "session", server, sessionId: sessionID }]), ) }, diff --git a/packages/app/e2e/regression/home-session-menu-first-click.spec.ts b/packages/app/e2e/regression/home-session-menu-first-click.spec.ts index cc03afa187..d5b588d532 100644 --- a/packages/app/e2e/regression/home-session-menu-first-click.spec.ts +++ b/packages/app/e2e/regression/home-session-menu-first-click.spec.ts @@ -21,7 +21,7 @@ async function boot(page: Page) { }) await page.addInitScript((directory) => { localStorage.setItem( - "opencode.global.dat:server", + "codev.global.dat:server", JSON.stringify({ projects: { local: [{ worktree: directory, expanded: true }] }, lastProject: { local: directory }, diff --git a/packages/app/e2e/regression/new-session-panel-corner.spec.ts b/packages/app/e2e/regression/new-session-panel-corner.spec.ts index a17b8c0817..b1e5667d6f 100644 --- a/packages/app/e2e/regression/new-session-panel-corner.spec.ts +++ b/packages/app/e2e/regression/new-session-panel-corner.spec.ts @@ -32,14 +32,14 @@ test("matches the rounded panel corners to the dark new-session background", asy localStorage.setItem("opencode-theme-id", "oc-2") localStorage.setItem("opencode-color-scheme", "dark") localStorage.setItem( - "opencode.global.dat:server", + "codev.global.dat:server", JSON.stringify({ projects: { local: [{ worktree: directory, expanded: true }] }, lastProject: { local: directory }, }), ) localStorage.setItem( - "opencode.window.browser.dat:tabs", + "codev.window.browser.dat:tabs", JSON.stringify([{ type: "draft", draftID, server, directory }]), ) }, diff --git a/packages/app/e2e/regression/new-session-reuses-draft.spec.ts b/packages/app/e2e/regression/new-session-reuses-draft.spec.ts index 69e6f3aefe..9191975117 100644 --- a/packages/app/e2e/regression/new-session-reuses-draft.spec.ts +++ b/packages/app/e2e/regression/new-session-reuses-draft.spec.ts @@ -17,7 +17,7 @@ async function boot(page: Page) { }) await page.addInitScript((directory) => { localStorage.setItem( - "opencode.global.dat:server", + "codev.global.dat:server", JSON.stringify({ projects: { local: [{ worktree: directory, expanded: true }] }, lastProject: { local: directory }, @@ -115,13 +115,13 @@ test("a new draft opens at the head of the tab strip, not after the sessions", a await page.addInitScript( ({ directory, seeded }) => { localStorage.setItem( - "opencode.global.dat:server", + "codev.global.dat:server", JSON.stringify({ projects: { local: [{ worktree: directory, expanded: true }] }, lastProject: { local: directory }, }), ) - localStorage.setItem("opencode.window.browser.dat:tabs", JSON.stringify(seeded)) + localStorage.setItem("codev.window.browser.dat:tabs", JSON.stringify(seeded)) }, { directory: fixture.directory, diff --git a/packages/app/e2e/regression/remote-session-settings.spec.ts b/packages/app/e2e/regression/remote-session-settings.spec.ts index 2669c5badb..f02c0f25bc 100644 --- a/packages/app/e2e/regression/remote-session-settings.spec.ts +++ b/packages/app/e2e/regression/remote-session-settings.spec.ts @@ -165,8 +165,8 @@ async function configureServers(page: Page, tabs: { type: "session"; server: str await page.addInitScript( ({ serverB, tabs }) => { localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } })) - localStorage.setItem("opencode.global.dat:server", JSON.stringify({ list: [serverB] })) - localStorage.setItem("opencode.window.browser.dat:tabs", JSON.stringify(tabs)) + localStorage.setItem("codev.global.dat:server", JSON.stringify({ list: [serverB] })) + localStorage.setItem("codev.window.browser.dat:tabs", JSON.stringify(tabs)) }, { serverB, tabs }, ) diff --git a/packages/app/e2e/regression/remote-tab-busy.spec.ts b/packages/app/e2e/regression/remote-tab-busy.spec.ts index 119fc7ee2d..eaf22121c4 100644 --- a/packages/app/e2e/regression/remote-tab-busy.spec.ts +++ b/packages/app/e2e/regression/remote-tab-busy.spec.ts @@ -11,9 +11,9 @@ test("tab busy indicator reflects the tab server's own session status", async ({ await page.addInitScript( ({ serverA, serverB, sessionA, sessionB }) => { localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } })) - localStorage.setItem("opencode.global.dat:server", JSON.stringify({ list: [serverB] })) + localStorage.setItem("codev.global.dat:server", JSON.stringify({ list: [serverB] })) localStorage.setItem( - "opencode.window.browser.dat:tabs", + "codev.window.browser.dat:tabs", JSON.stringify([ { type: "session", server: serverA, sessionId: sessionA }, { type: "session", server: serverB, sessionId: sessionB }, diff --git a/packages/app/e2e/regression/review-open-file.spec.ts b/packages/app/e2e/regression/review-open-file.spec.ts index 25ebd3a37a..0f10503e57 100644 --- a/packages/app/e2e/regression/review-open-file.spec.ts +++ b/packages/app/e2e/regression/review-open-file.spec.ts @@ -64,22 +64,22 @@ test("opens and searches project files inline", async ({ page }) => { ({ directory, server, sessionID }) => { localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } })) localStorage.setItem( - "opencode.global.dat:server", + "codev.global.dat:server", JSON.stringify({ projects: { local: [{ worktree: directory, expanded: true }] }, lastProject: { local: directory }, }), ) localStorage.setItem( - "opencode.global.dat:layout", + "codev.global.dat:layout", JSON.stringify({ review: { diffStyle: "split", panelOpened: true } }), ) localStorage.setItem( - "opencode.global.dat:review-panel-v2", + "codev.global.dat:review-panel-v2", JSON.stringify({ sidebarOpened: false, sidebarWidth: 240, expandMode: "collapse" }), ) localStorage.setItem( - "opencode.window.browser.dat:tabs", + "codev.window.browser.dat:tabs", JSON.stringify([{ type: "session", server, sessionId: sessionID }]), ) }, diff --git a/packages/app/e2e/regression/review-state-persistence.spec.ts b/packages/app/e2e/regression/review-state-persistence.spec.ts index aa42f1bb51..e9ef4f5ffa 100644 --- a/packages/app/e2e/regression/review-state-persistence.spec.ts +++ b/packages/app/e2e/regression/review-state-persistence.spec.ts @@ -110,14 +110,14 @@ async function setup(page: Page) { ({ directory, server, sessions }) => { localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } })) localStorage.setItem( - "opencode.global.dat:server", + "codev.global.dat:server", JSON.stringify({ projects: { local: [{ worktree: directory, expanded: true }] }, lastProject: { local: directory }, }), ) localStorage.setItem( - "opencode.window.browser.dat:tabs", + "codev.window.browser.dat:tabs", JSON.stringify(sessions.map((sessionId: string) => ({ type: "session", server, sessionId }))), ) }, diff --git a/packages/app/e2e/regression/review-tab-switch.spec.ts b/packages/app/e2e/regression/review-tab-switch.spec.ts index 8634166e51..bad8963b76 100644 --- a/packages/app/e2e/regression/review-tab-switch.spec.ts +++ b/packages/app/e2e/regression/review-tab-switch.spec.ts @@ -105,14 +105,14 @@ async function setup(page: Page) { ({ directory, server, sessions }) => { localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } })) localStorage.setItem( - "opencode.global.dat:server", + "codev.global.dat:server", JSON.stringify({ projects: { local: [{ worktree: directory, expanded: true }] }, lastProject: { local: directory }, }), ) localStorage.setItem( - "opencode.window.browser.dat:tabs", + "codev.window.browser.dat:tabs", JSON.stringify(sessions.map((sessionId: string) => ({ type: "session", server, sessionId }))), ) }, diff --git a/packages/app/e2e/regression/review-terminal-stacked.spec.ts b/packages/app/e2e/regression/review-terminal-stacked.spec.ts index 154bab48c4..316002137c 100644 --- a/packages/app/e2e/regression/review-terminal-stacked.spec.ts +++ b/packages/app/e2e/regression/review-terminal-stacked.spec.ts @@ -100,7 +100,7 @@ test("keeps the review tree and terminal sized when both panels are open", async await page.addInitScript(() => { localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } })) localStorage.setItem( - "opencode.global.dat:layout", + "codev.global.dat:layout", JSON.stringify({ review: { diffStyle: "split", panelOpened: true } }), ) }) diff --git a/packages/app/e2e/regression/session-actions-menu.spec.ts b/packages/app/e2e/regression/session-actions-menu.spec.ts index 5154d04782..560e7e9e05 100644 --- a/packages/app/e2e/regression/session-actions-menu.spec.ts +++ b/packages/app/e2e/regression/session-actions-menu.spec.ts @@ -53,7 +53,7 @@ async function boot(page: Page, options?: { share?: string }) { } await page.addInitScript((directory) => { localStorage.setItem( - "opencode.global.dat:server", + "codev.global.dat:server", JSON.stringify({ projects: { local: [{ worktree: directory, expanded: true }] }, lastProject: { local: directory }, diff --git a/packages/app/e2e/regression/session-delete-navigation.spec.ts b/packages/app/e2e/regression/session-delete-navigation.spec.ts index 8a0bcff669..bf52eace49 100644 --- a/packages/app/e2e/regression/session-delete-navigation.spec.ts +++ b/packages/app/e2e/regression/session-delete-navigation.spec.ts @@ -42,7 +42,7 @@ async function boot(page: Page) { }) await page.addInitScript((directory) => { localStorage.setItem( - "opencode.global.dat:server", + "codev.global.dat:server", JSON.stringify({ projects: { local: [{ worktree: directory, expanded: true }] }, lastProject: { local: directory }, diff --git a/packages/app/e2e/regression/session-list-path-loading.spec.ts b/packages/app/e2e/regression/session-list-path-loading.spec.ts index 4a3855122a..4c0602d64e 100644 --- a/packages/app/e2e/regression/session-list-path-loading.spec.ts +++ b/packages/app/e2e/regression/session-list-path-loading.spec.ts @@ -24,7 +24,7 @@ test("shows loaded sessions before the directory path request resolves", async ( await page.addInitScript((directory) => { localStorage.setItem( - "opencode.global.dat:server", + "codev.global.dat:server", JSON.stringify({ projects: { local: [{ worktree: directory, expanded: true }] }, lastProject: { local: directory }, diff --git a/packages/app/e2e/regression/session-menu-delete-item.spec.ts b/packages/app/e2e/regression/session-menu-delete-item.spec.ts index d31bb337bc..74387a3935 100644 --- a/packages/app/e2e/regression/session-menu-delete-item.spec.ts +++ b/packages/app/e2e/regression/session-menu-delete-item.spec.ts @@ -19,7 +19,7 @@ async function boot(page: Page) { }) await page.addInitScript((directory) => { localStorage.setItem( - "opencode.global.dat:server", + "codev.global.dat:server", JSON.stringify({ projects: { local: [{ worktree: directory, expanded: true }] }, lastProject: { local: directory }, diff --git a/packages/app/e2e/regression/session-todo-dock-navigation.spec.ts b/packages/app/e2e/regression/session-todo-dock-navigation.spec.ts index 603c411d55..48230f95ac 100644 --- a/packages/app/e2e/regression/session-todo-dock-navigation.spec.ts +++ b/packages/app/e2e/regression/session-todo-dock-navigation.spec.ts @@ -138,14 +138,14 @@ async function configurePage(page: Page) { ({ directory, dirBase64, server, sessionIDs }) => { localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } })) localStorage.setItem( - "opencode.global.dat:server", + "codev.global.dat:server", JSON.stringify({ projects: { local: [{ worktree: directory, expanded: true }] }, lastProject: { local: directory }, }), ) localStorage.setItem( - "opencode.window.browser.dat:tabs", + "codev.window.browser.dat:tabs", JSON.stringify(sessionIDs.map((sessionId) => ({ type: "session", server, dirBase64, sessionId }))), ) }, diff --git a/packages/app/e2e/regression/sidebar-persistence.spec.ts b/packages/app/e2e/regression/sidebar-persistence.spec.ts index 853ddda6a6..bcbc1d6863 100644 --- a/packages/app/e2e/regression/sidebar-persistence.spec.ts +++ b/packages/app/e2e/regression/sidebar-persistence.spec.ts @@ -25,7 +25,7 @@ async function boot(page: Page) { }) await page.addInitScript((directory) => { localStorage.setItem( - "opencode.global.dat:server", + "codev.global.dat:server", JSON.stringify({ projects: { local: [{ worktree: directory, expanded: true }] }, lastProject: { local: directory }, diff --git a/packages/app/e2e/regression/sidebar-pinned-sessions.spec.ts b/packages/app/e2e/regression/sidebar-pinned-sessions.spec.ts index f35001f0e3..79330f6470 100644 --- a/packages/app/e2e/regression/sidebar-pinned-sessions.spec.ts +++ b/packages/app/e2e/regression/sidebar-pinned-sessions.spec.ts @@ -29,7 +29,7 @@ async function boot(page: Page) { }) await page.addInitScript((directory) => { localStorage.setItem( - "opencode.global.dat:server", + "codev.global.dat:server", JSON.stringify({ projects: { local: [{ worktree: directory, expanded: true }] }, lastProject: { local: directory }, @@ -176,7 +176,7 @@ async function bootWith(page: Page, sessions: ReturnType<typeof manySessions>) { }) await page.addInitScript((directory) => { localStorage.setItem( - "opencode.global.dat:server", + "codev.global.dat:server", JSON.stringify({ projects: { local: [{ worktree: directory, expanded: true }] }, lastProject: { local: directory }, @@ -270,7 +270,7 @@ test("deleting a pinned session drops its pin", async ({ page }) => { // The id must leave storage too: left behind, it would silently re-pin a // future session that reused it. await expect - .poll(async () => page.evaluate(() => localStorage.getItem("opencode.global.dat:sidebar.pinned") ?? "")) + .poll(async () => page.evaluate(() => localStorage.getItem("codev.global.dat:sidebar.pinned") ?? "")) .not.toContain("ses_pin_0") }) @@ -283,7 +283,7 @@ test("pins are stored per server, not in one flat list", async ({ page }) => { // onto a different server. Asserting the exact key, not just "some key" — // anything constant would satisfy the looser check. const serverKey = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}` - const stored = await page.evaluate(() => localStorage.getItem("opencode.global.dat:sidebar.pinned")) + const stored = await page.evaluate(() => localStorage.getItem("codev.global.dat:sidebar.pinned")) expect(stored).not.toBeNull() expect(JSON.parse(stored!)).toEqual({ sessions: { [serverKey]: [fixture.targetID] } }) }) diff --git a/packages/app/e2e/regression/sidebar-project-scope.spec.ts b/packages/app/e2e/regression/sidebar-project-scope.spec.ts index 3c059bdce7..673b5cc2bc 100644 --- a/packages/app/e2e/regression/sidebar-project-scope.spec.ts +++ b/packages/app/e2e/regression/sidebar-project-scope.spec.ts @@ -48,7 +48,7 @@ test("lists only the last project's sessions on a fresh window", async ({ page } await page.addInitScript( ([a, b]) => { localStorage.setItem( - "opencode.global.dat:server", + "codev.global.dat:server", JSON.stringify({ projects: { local: [ @@ -91,7 +91,7 @@ test("shows no sessions once the selected project is deselected", async ({ page await page.addInitScript( ([a, b]) => { localStorage.setItem( - "opencode.global.dat:server", + "codev.global.dat:server", JSON.stringify({ projects: { local: [ diff --git a/packages/app/e2e/regression/sidebar-section-collapse.spec.ts b/packages/app/e2e/regression/sidebar-section-collapse.spec.ts index 290d58d41a..633141daa6 100644 --- a/packages/app/e2e/regression/sidebar-section-collapse.spec.ts +++ b/packages/app/e2e/regression/sidebar-section-collapse.spec.ts @@ -22,7 +22,7 @@ async function boot(page: Page) { }) await page.addInitScript((directory) => { localStorage.setItem( - "opencode.global.dat:server", + "codev.global.dat:server", JSON.stringify({ projects: { local: [{ worktree: directory, expanded: true }] }, lastProject: { local: directory }, diff --git a/packages/app/e2e/regression/sidebar-session-menu-first-click.spec.ts b/packages/app/e2e/regression/sidebar-session-menu-first-click.spec.ts index a4c6f3b078..852733349b 100644 --- a/packages/app/e2e/regression/sidebar-session-menu-first-click.spec.ts +++ b/packages/app/e2e/regression/sidebar-session-menu-first-click.spec.ts @@ -17,7 +17,7 @@ async function boot(page: Page) { }) await page.addInitScript((directory) => { localStorage.setItem( - "opencode.global.dat:server", + "codev.global.dat:server", JSON.stringify({ projects: { local: [{ worktree: directory, expanded: true }] }, lastProject: { local: directory }, diff --git a/packages/app/e2e/regression/sidebar-toggle-search.spec.ts b/packages/app/e2e/regression/sidebar-toggle-search.spec.ts index 3baa38a2e5..7bbeb356e1 100644 --- a/packages/app/e2e/regression/sidebar-toggle-search.spec.ts +++ b/packages/app/e2e/regression/sidebar-toggle-search.spec.ts @@ -24,7 +24,7 @@ async function boot(page: Page) { }) await page.addInitScript((directory) => { localStorage.setItem( - "opencode.global.dat:server", + "codev.global.dat:server", JSON.stringify({ projects: { local: [{ worktree: directory, expanded: true }] }, lastProject: { local: directory }, diff --git a/packages/app/e2e/regression/subagent-child-navigation.spec.ts b/packages/app/e2e/regression/subagent-child-navigation.spec.ts index 19d2c29af0..af3727d304 100644 --- a/packages/app/e2e/regression/subagent-child-navigation.spec.ts +++ b/packages/app/e2e/regression/subagent-child-navigation.spec.ts @@ -182,13 +182,13 @@ async function configurePage(page: Page) { ({ directory, server, sessionId }) => { localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } })) localStorage.setItem( - "opencode.global.dat:server", + "codev.global.dat:server", JSON.stringify({ projects: { local: [{ worktree: directory, expanded: true }] }, lastProject: { local: directory }, }), ) - localStorage.setItem("opencode.window.browser.dat:tabs", JSON.stringify([{ type: "session", server, sessionId }])) + localStorage.setItem("codev.window.browser.dat:tabs", JSON.stringify([{ type: "session", server, sessionId }])) }, { directory, server, sessionId: parentID }, ) diff --git a/packages/app/e2e/regression/tab-navigate-mousedown.spec.ts b/packages/app/e2e/regression/tab-navigate-mousedown.spec.ts index 94afbc9a9d..628866c27d 100644 --- a/packages/app/e2e/regression/tab-navigate-mousedown.spec.ts +++ b/packages/app/e2e/regression/tab-navigate-mousedown.spec.ts @@ -11,7 +11,7 @@ test("pressing mouse down on a tab navigates before mouse up", async ({ page }) ({ server, sessionA, sessionB }) => { localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } })) localStorage.setItem( - "opencode.window.browser.dat:tabs", + "codev.window.browser.dat:tabs", JSON.stringify([ { type: "session", server, sessionId: sessionA }, { type: "session", server, sessionId: sessionB }, diff --git a/packages/app/e2e/regression/terminal-composer-focus.spec.ts b/packages/app/e2e/regression/terminal-composer-focus.spec.ts index f672602782..13bfc3c434 100644 --- a/packages/app/e2e/regression/terminal-composer-focus.spec.ts +++ b/packages/app/e2e/regression/terminal-composer-focus.spec.ts @@ -195,7 +195,7 @@ test("focuses a terminal created from the new-terminal button", async ({ page }) function seedCachedTerminal(page: Page) { return page.addInitScript( ({ terminalKey, ptyID }) => { - localStorage.setItem("opencode.global.dat:layout", JSON.stringify({ terminal: { height: 320, opened: true } })) + localStorage.setItem("codev.global.dat:layout", JSON.stringify({ terminal: { height: 320, opened: true } })) localStorage.setItem( terminalKey, JSON.stringify({ diff --git a/packages/app/e2e/regression/terminal-tab-switch.spec.ts b/packages/app/e2e/regression/terminal-tab-switch.spec.ts index cbb72958ad..5c3e32ae52 100644 --- a/packages/app/e2e/regression/terminal-tab-switch.spec.ts +++ b/packages/app/e2e/regression/terminal-tab-switch.spec.ts @@ -112,14 +112,14 @@ async function setup(page: Page) { ({ directory, server, sessions }) => { localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } })) localStorage.setItem( - "opencode.global.dat:server", + "codev.global.dat:server", JSON.stringify({ projects: { local: [{ worktree: directory, expanded: true }] }, lastProject: { local: directory }, }), ) localStorage.setItem( - "opencode.window.browser.dat:tabs", + "codev.window.browser.dat:tabs", JSON.stringify(sessions.map((sessionId: string) => ({ type: "session", server, sessionId }))), ) }, diff --git a/packages/app/e2e/smoke/session-timeline.spec.ts b/packages/app/e2e/smoke/session-timeline.spec.ts index bdf3f55bdc..f97d757d67 100644 --- a/packages/app/e2e/smoke/session-timeline.spec.ts +++ b/packages/app/e2e/smoke/session-timeline.spec.ts @@ -127,7 +127,7 @@ test.describe("smoke: session timeline", () => { await page.addInitScript( ({ dirBase64, sourceID, targetID }) => { localStorage.setItem( - "opencode.window.browser.dat:tabs", + "codev.window.browser.dat:tabs", JSON.stringify( [sourceID, targetID].map((sessionId) => ({ type: "session", @@ -253,7 +253,7 @@ test.describe("smoke: session timeline", () => { await page.addInitScript( ({ dirBase64, sourceID, targetID }) => { localStorage.setItem( - "opencode.window.browser.dat:tabs", + "codev.window.browser.dat:tabs", JSON.stringify( [sourceID, targetID].map((sessionId) => ({ type: "session", @@ -369,7 +369,7 @@ async function configureSmokePage(page: Page, directory: string) { await page.addInitScript((directory) => { localStorage.setItem( - "opencode.global.dat:server", + "codev.global.dat:server", JSON.stringify({ projects: { local: [{ worktree: directory, expanded: true }], 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 5581d0a23d..a03d6a7e4d 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 @@ -83,8 +83,8 @@ describe("buildRequestParts", () => { { type: "image", id: "img_external", - filename: "opencode.global.dat", - sourcePath: "C:\\Users\\Luke\\AppData\\Roaming\\ai.codev.desktop.beta\\opencode.global.dat", + filename: "codev.global.dat", + sourcePath: "C:\\Users\\Luke\\AppData\\Roaming\\ai.codev.desktop.beta\\codev.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.codev.desktop.beta\\opencode.global.dat", + "C:\\Users\\Luke\\AppData\\Roaming\\ai.codev.desktop.beta\\codev.global.dat", ) }) diff --git a/packages/app/src/context/language.tsx b/packages/app/src/context/language.tsx index 92d56a6b93..df9610b765 100644 --- a/packages/app/src/context/language.tsx +++ b/packages/app/src/context/language.tsx @@ -83,7 +83,7 @@ export function normalizeLocale(value: string): Locale { function readStoredLocale() { if (typeof localStorage !== "object") return try { - const raw = localStorage.getItem("opencode.global.dat:language") + const raw = localStorage.getItem("codev.global.dat:language") if (!raw) return const next = JSON.parse(raw) as { locale?: string } if (typeof next?.locale !== "string") return diff --git a/packages/app/src/entry.tsx b/packages/app/src/entry.tsx index d3c62018ac..af0274fe1b 100644 --- a/packages/app/src/entry.tsx +++ b/packages/app/src/entry.tsx @@ -8,11 +8,12 @@ import { dict as en } from "@/i18n/en" import { dict as es } from "@/i18n/es" import { dict as vi } from "@/i18n/vi" import { handleNotificationClick } from "@/utils/notification-click" +import { renameLegacyLocalStorage } from "@/utils/storage-rebrand" import { authFromToken } from "@/utils/server" import pkg from "../package.json" import { ServerConnection } from "./context/server" -const DEFAULT_SERVER_URL_KEY = "opencode.settings.dat:defaultServerUrl" +const DEFAULT_SERVER_URL_KEY = "codev.settings.dat:defaultServerUrl" const getLocale = () => { if (typeof navigator !== "object") return "en" as const @@ -35,6 +36,7 @@ const getRootNotFoundError = () => { const getStorage = (key: string) => { if (typeof localStorage === "undefined") return null + renameLegacyLocalStorage() try { return localStorage.getItem(key) } catch { diff --git a/packages/app/src/utils/persist.test.ts b/packages/app/src/utils/persist.test.ts index d8b822d856..b4b381c4ba 100644 --- a/packages/app/src/utils/persist.test.ts +++ b/packages/app/src/utils/persist.test.ts @@ -115,7 +115,7 @@ describe("persist localStorage resilience", () => { test("workspace storage sanitizes Windows filename characters", () => { const result = persistTesting.workspaceStorage("C:\\Users\\foo") - expect(result).toStartWith("opencode.workspace.") + expect(result).toStartWith("codev.workspace.") expect(result.endsWith(".dat")).toBeTrue() expect(/[:\\/]/.test(result)).toBeFalse() }) @@ -200,7 +200,7 @@ describe("persist localStorage resilience", () => { test("server global target preserves local key and isolates remote keys", () => { expect(Persist.serverGlobal(ServerScope.local, "notification")).toEqual(Persist.global("notification")) expect(Persist.serverGlobal("https://debian.example" as ServerScope, "notification")).toEqual({ - storage: "opencode.global.dat", + storage: "codev.global.dat", key: "https://debian.example\0notification", }) }) diff --git a/packages/app/src/utils/persist.ts b/packages/app/src/utils/persist.ts index a2daae4866..56c5d1e6cd 100644 --- a/packages/app/src/utils/persist.ts +++ b/packages/app/src/utils/persist.ts @@ -5,6 +5,7 @@ import { createResource, type Accessor } from "solid-js" import type { SetStoreFunction, Store } from "solid-js/store" import { pathKey } from "@/utils/path-key" import { ScopedKey, ServerScope, type ServerScope as ServerScopeValue } from "@/utils/server-scope" +import { renameLegacyLocalStorage } from "@/utils/storage-rebrand" type InitType = Promise<string> | string | null type PersistedWithReady<T> = [ @@ -24,11 +25,14 @@ type PersistTarget = { } const LEGACY_STORAGE = "default.dat" -const GLOBAL_STORAGE = "opencode.global.dat" -const WINDOW_STORAGE = "opencode.window" -const LOCAL_PREFIX = "opencode." +const GLOBAL_STORAGE = "codev.global.dat" +const WINDOW_STORAGE = "codev.window" +const LOCAL_PREFIX = "codev." const fallback = new Map<string, boolean>() +// Before any store reads a key: the names above were `opencode.*` until the rebrand. +renameLegacyLocalStorage() + const CACHE_MAX_ENTRIES = 500 const CACHE_MAX_BYTES = 8 * 1024 * 1024 @@ -340,13 +344,13 @@ async function migrateLegacyAsync(input: { function workspaceStorage(dir: string) { const head = (dir.slice(0, 12) || "workspace").replace(/[^a-zA-Z0-9._-]/g, "-") const sum = checksum(dir) ?? "0" - return `opencode.workspace.${head}.${sum}.dat` + return `codev.workspace.${head}.${sum}.dat` } function draftStorage(draftID: string) { const head = (draftID.slice(0, 12) || "draft").replace(/[^a-zA-Z0-9._-]/g, "-") const sum = checksum(draftID) ?? "0" - return `opencode.draft.${head}.${sum}.dat` + return `codev.draft.${head}.${sum}.dat` } function windowStorage(windowID: string) { diff --git a/packages/app/src/utils/storage-rebrand.test.ts b/packages/app/src/utils/storage-rebrand.test.ts new file mode 100644 index 0000000000..571f7854b1 --- /dev/null +++ b/packages/app/src/utils/storage-rebrand.test.ts @@ -0,0 +1,77 @@ +import { beforeEach, describe, expect, test } from "bun:test" +import { renameLegacyLocalStorage } from "./storage-rebrand" + +class MemoryStorage implements Storage { + private values = new Map<string, string>() + + clear() { + this.values.clear() + } + + get length() { + return this.values.size + } + + key(index: number) { + return Array.from(this.values.keys())[index] ?? null + } + + getItem(key: string) { + return this.values.get(key) ?? null + } + + setItem(key: string, value: string) { + this.values.set(key, value) + } + + removeItem(key: string) { + this.values.delete(key) + } +} + +const storage = new MemoryStorage() + +beforeEach(() => { + storage.clear() + Object.defineProperty(globalThis, "localStorage", { value: storage, configurable: true }) +}) + +describe("storage rebrand", () => { + test("renames pre-rebrand keys and leaves the rest alone", () => { + storage.setItem("opencode.global.dat:server", '{"list":[]}') + storage.setItem("opencode.workspace.home-foo.abc.dat:workspace:layout", '"wide"') + storage.setItem("opencode.settings.dat:defaultServerUrl", "http://localhost:4096") + storage.setItem("legacy.direct.key", '{"value":1}') + + renameLegacyLocalStorage() + + expect(Array.from({ length: storage.length }, (_, index) => storage.key(index)).sort()).toEqual([ + "codev.global.dat:server", + "codev.settings.dat:defaultServerUrl", + "codev.workspace.home-foo.abc.dat:workspace:layout", + "legacy.direct.key", + ]) + expect(storage.getItem("codev.global.dat:server")).toBe('{"list":[]}') + expect(storage.getItem("legacy.direct.key")).toBe('{"value":1}') + }) + + test("keeps the rebranded value when both keys exist", () => { + storage.setItem("opencode.global.dat:server", '{"list":["old"]}') + storage.setItem("codev.global.dat:server", '{"list":["new"]}') + + renameLegacyLocalStorage() + + expect(storage.length).toBe(1) + expect(storage.getItem("codev.global.dat:server")).toBe('{"list":["new"]}') + }) + + test("is a no-op once the sweep has run", () => { + storage.setItem("codev.global.dat:server", '{"list":[]}') + + renameLegacyLocalStorage() + renameLegacyLocalStorage() + + expect(storage.getItem("codev.global.dat:server")).toBe('{"list":[]}') + expect(storage.length).toBe(1) + }) +}) diff --git a/packages/app/src/utils/storage-rebrand.ts b/packages/app/src/utils/storage-rebrand.ts new file mode 100644 index 0000000000..cec99c8984 --- /dev/null +++ b/packages/app/src/utils/storage-rebrand.ts @@ -0,0 +1,36 @@ +const LEGACY_PREFIX = "opencode." +const PREFIX = "codev." + +// The browser build keeps every persisted store in localStorage under a +// `<store>:<key>` name, which before the rebrand meant `opencode.global.dat:server` +// and friends. Rename them in place so an upgrading tab keeps its recent servers and +// per-project UI state. +// +// Callers are the entry points that read persisted state, not a module side effect: +// the language dictionary warms up from localStorage while modules are still +// importing, so ordering between module initializers cannot be relied on. Once the +// sweep has run there is nothing left to match, so repeat calls only walk the key +// list. +export function renameLegacyLocalStorage() { + if (typeof localStorage !== "object") return + + try { + const legacy = Array.from({ length: localStorage.length }, (_, index) => localStorage.key(index)).filter( + (name): name is string => !!name && name.startsWith(LEGACY_PREFIX), + ) + + for (const name of legacy) { + const value = localStorage.getItem(name) + // Remove first: a quota-bound store cannot hold both copies, and the value is + // already in hand. + localStorage.removeItem(name) + if (value === null) continue + const renamed = PREFIX + name.slice(LEGACY_PREFIX.length) + if (localStorage.getItem(renamed) !== null) continue + localStorage.setItem(renamed, value) + } + } catch { + // Storage can be unavailable outright (private mode, blocked cookies), which the + // persisted stores already handle by falling back to their in-memory cache. + } +} diff --git a/packages/desktop/src/main/index.ts b/packages/desktop/src/main/index.ts index b01ee3a414..2ab632fc24 100644 --- a/packages/desktop/src/main/index.ts +++ b/packages/desktop/src/main/index.ts @@ -56,6 +56,7 @@ import { createGatewaySsoController } from "./gateway-sso/controller" import { registerGatewaySsoIpcHandlers } from "./gateway-sso/ipc" import { migrate } from "./migrate" import { cleanupStoreFiles } from "./store-cleanup" +import { renameLegacyStoreFiles } from "./store-rebrand" const APP_NAMES: Record<string, string> = { dev: "CoDev Dev", @@ -146,6 +147,9 @@ const main = Effect.gen(function* () { onboardingTestRoot ? join(onboardingTestRoot, "desktop") : join(app.getPath("appData"), appId), ) if (onboardingTestRoot) app.setPath("sessionData", join(onboardingTestRoot, "session")) + // Before the first store opens (initializeOldLayoutEligibility below is one), and + // before the onboarding scan reads the directory listing. + const rebrandedStores = renameLegacyStoreFiles(app.getPath("userData")) initializeOldLayoutEligibility(app.getPath("userData")) // Out of `/` (where macOS launches apps, which breaks ripgrep) but not into // the home directory: the sidecar inherits this cwd, and server requests @@ -161,6 +165,7 @@ const main = Effect.gen(function* () { process.chdir(fallbackCwd) } catch {} logger = initLogging() + if (rebrandedStores.length > 0) logger.log("renamed pre-rebrand store files", { files: rebrandedStores }) initCrashReporter() const wslServers = createWslServersController( diff --git a/packages/desktop/src/main/install-state.test.ts b/packages/desktop/src/main/install-state.test.ts index d9300f63a9..0e2472f8a0 100644 --- a/packages/desktop/src/main/install-state.test.ts +++ b/packages/desktop/src/main/install-state.test.ts @@ -11,8 +11,8 @@ describe("hasExistingAppState", () => { }) test("recognizes state written by an earlier CoDev launch", () => { - expect(hasExistingAppState([file("opencode.settings")])).toBe(true) - expect(hasExistingAppState([file("opencode.global.dat")])).toBe(true) + expect(hasExistingAppState([file("codev.settings")])).toBe(true) + expect(hasExistingAppState([file("codev.global.dat")])).toBe(true) expect(hasExistingAppState([file("window-state-abc.json")])).toBe(true) expect(hasExistingAppState([directory("opencode")])).toBe(true) }) diff --git a/packages/desktop/src/main/install-state.ts b/packages/desktop/src/main/install-state.ts index 32f1df3640..3e90ca4772 100644 --- a/packages/desktop/src/main/install-state.ts +++ b/packages/desktop/src/main/install-state.ts @@ -1,6 +1,6 @@ export function hasExistingAppState(entries: Array<{ name: string; isDirectory: () => boolean }>) { return entries.some((entry) => { - if (entry.name === "opencode.settings") return true + if (entry.name === "codev.settings") return true if (entry.name.endsWith(".dat")) return true if (/^window-state-.+\.json$/.test(entry.name)) return true return entry.isDirectory() && entry.name === "opencode" diff --git a/packages/desktop/src/main/migrate.ts b/packages/desktop/src/main/migrate.ts index ee787dd51a..5d2bd5a61e 100644 --- a/packages/desktop/src/main/migrate.ts +++ b/packages/desktop/src/main/migrate.ts @@ -5,6 +5,8 @@ import { homedir } from "node:os" import { join } from "node:path" import { CHANNEL } from "./constants" import { getStore } from "./store" +import { rebrandStoreName } from "./store-rebrand" +import { SETTINGS_STORE } from "./store-keys" const TAURI_MIGRATED_KEY = "tauriMigrated" @@ -34,11 +36,12 @@ function tauriAppId() { return app.isPackaged ? TAURI_APP_IDS[CHANNEL] : "ai.opencode.desktop.dev" } -// Migrate a single Tauri .dat file into the corresponding electron-store. -// `opencode.settings.dat` is special: it maps to the `opencode.settings` store -// (the electron-store name without the `.dat` extension). All other .dat files -// keep their full filename as the electron-store name so they match what the -// renderer already passes via IPC (e.g. `"default.dat"`, `"opencode.global.dat"`). +// Migrate a single Tauri .dat file into the corresponding store. Tauri wrote +// pre-rebrand names, so every target name is rebranded on the way in. +// `opencode.settings.dat` is special: it maps to the settings store (the store +// name without the `.dat` extension). All other .dat files keep their rebranded +// filename as the store name so they match what the renderer passes via IPC +// (e.g. `"default.dat"`, `"codev.global.dat"`). function migrateFile(datPath: string, filename: string) { let data: Record<string, unknown> try { @@ -48,10 +51,7 @@ function migrateFile(datPath: string, filename: string) { return } - // opencode.settings.dat → the electron settings store ("opencode.settings"). - // All other .dat files keep their full filename as the store name so they match - // what the renderer passes via IPC (e.g. "default.dat", "opencode.global.dat"). - const storeName = filename === "opencode.settings.dat" ? "opencode.settings" : filename + const storeName = filename === "opencode.settings.dat" ? SETTINGS_STORE : rebrandStoreName(filename) const target = getStore(storeName) const migrated: string[] = [] const skipped: string[] = [] diff --git a/packages/desktop/src/main/store-cleanup.test.ts b/packages/desktop/src/main/store-cleanup.test.ts index c63dd0b0f5..235aaff183 100644 --- a/packages/desktop/src/main/store-cleanup.test.ts +++ b/packages/desktop/src/main/store-cleanup.test.ts @@ -25,37 +25,37 @@ describe("store cleanup", () => { test("removes empty scoped stores and leaves global stores alone", async () => { const root = await tempRoot() const now = new Date("2026-07-01T00:00:00.000Z") - await writeStore(root, "opencode.draft.empty.dat", "{}", now) - await writeStore(root, "opencode.workspace.empty.dat", "{\n}", now) - await writeStore(root, "opencode.global.dat", "{}", now) - await writeStore(root, "opencode.workspace.empty.dat.json", "{}", now) + await writeStore(root, "codev.draft.empty.dat", "{}", now) + await writeStore(root, "codev.workspace.empty.dat", "{\n}", now) + await writeStore(root, "codev.global.dat", "{}", now) + await writeStore(root, "codev.workspace.empty.dat.json", "{}", now) const result = await cleanupStoreFiles(root, now.getTime()) - expect(result.deleted.sort()).toEqual(["opencode.draft.empty.dat", "opencode.workspace.empty.dat"]) - expect((await readdir(root)).sort()).toEqual(["opencode.global.dat", "opencode.workspace.empty.dat.json"]) + expect(result.deleted.sort()).toEqual(["codev.draft.empty.dat", "codev.workspace.empty.dat"]) + expect((await readdir(root)).sort()).toEqual(["codev.global.dat", "codev.workspace.empty.dat.json"]) }) test("removes stale drafts by age without removing non-empty workspace stores", async () => { const root = await tempRoot() const now = new Date("2026-07-01T00:00:00.000Z") - await writeStore(root, "opencode.draft.old.dat", '{"draft:prompt":"hello"}', new Date("2026-05-01T00:00:00.000Z")) - await writeStore(root, "opencode.draft.recent.dat", '{"draft:prompt":"hello"}', now) + await writeStore(root, "codev.draft.old.dat", '{"draft:prompt":"hello"}', new Date("2026-05-01T00:00:00.000Z")) + await writeStore(root, "codev.draft.recent.dat", '{"draft:prompt":"hello"}', now) await writeStore( root, - "opencode.workspace.old.dat", + "codev.workspace.old.dat", '{"workspace:layout":"wide"}', new Date("2025-01-01T00:00:00.000Z"), ) - await writeStore(root, "opencode.workspace.recent.dat", '{"workspace:layout":"wide"}', now) + await writeStore(root, "codev.workspace.recent.dat", '{"workspace:layout":"wide"}', now) const result = await cleanupStoreFiles(root, now.getTime()) - expect(result.deleted).toEqual(["opencode.draft.old.dat"]) + expect(result.deleted).toEqual(["codev.draft.old.dat"]) expect((await readdir(root)).sort()).toEqual([ - "opencode.draft.recent.dat", - "opencode.workspace.old.dat", - "opencode.workspace.recent.dat", + "codev.draft.recent.dat", + "codev.workspace.old.dat", + "codev.workspace.recent.dat", ]) }) @@ -66,7 +66,7 @@ describe("store cleanup", () => { Array.from({ length: 102 }, (_, index) => writeStore( root, - `opencode.draft.${index}.dat`, + `codev.draft.${index}.dat`, '{"draft:prompt":"hello"}', new Date(now.getTime() - index * 1000), ), @@ -77,17 +77,17 @@ describe("store cleanup", () => { const remaining = await readdir(root) - expect(result.deleted.sort()).toEqual(["opencode.draft.100.dat", "opencode.draft.101.dat"]) + expect(result.deleted.sort()).toEqual(["codev.draft.100.dat", "codev.draft.101.dat"]) expect(remaining).toHaveLength(100) }) test("removes a scoped store immediately when it becomes empty", async () => { const root = await tempRoot() - await writeStore(root, "opencode.draft.empty.dat", "{}", new Date("2026-07-01T00:00:00.000Z")) - await writeStore(root, "opencode.global.dat", "{}", new Date("2026-07-01T00:00:00.000Z")) + await writeStore(root, "codev.draft.empty.dat", "{}", new Date("2026-07-01T00:00:00.000Z")) + await writeStore(root, "codev.global.dat", "{}", new Date("2026-07-01T00:00:00.000Z")) - expect(await deleteStoreFileIfEmpty(root, "opencode.draft.empty.dat")).toBe(true) - expect(await deleteStoreFileIfEmpty(root, "opencode.global.dat")).toBe(false) - expect(await readdir(root)).toEqual(["opencode.global.dat"]) + expect(await deleteStoreFileIfEmpty(root, "codev.draft.empty.dat")).toBe(true) + expect(await deleteStoreFileIfEmpty(root, "codev.global.dat")).toBe(false) + expect(await readdir(root)).toEqual(["codev.global.dat"]) }) }) diff --git a/packages/desktop/src/main/store-cleanup.ts b/packages/desktop/src/main/store-cleanup.ts index 4d592ebc4b..f79c8f1536 100644 --- a/packages/desktop/src/main/store-cleanup.ts +++ b/packages/desktop/src/main/store-cleanup.ts @@ -74,8 +74,8 @@ export async function deleteStoreFileIfEmpty(userDataPath: string, name: string) } function storeKind(name: string): StoreKind | undefined { - if (/^opencode\.draft\..+\.dat$/.test(name)) return "draft" - if (/^opencode\.workspace\..+\.dat$/.test(name)) return "workspace" + if (/^codev\.draft\..+\.dat$/.test(name)) return "draft" + if (/^codev\.workspace\..+\.dat$/.test(name)) return "workspace" } async function isEmptyStore(file: string, size: number) { diff --git a/packages/desktop/src/main/store-file.test.ts b/packages/desktop/src/main/store-file.test.ts index 42cfa8ce5c..87ad1dfc33 100644 --- a/packages/desktop/src/main/store-file.test.ts +++ b/packages/desktop/src/main/store-file.test.ts @@ -34,7 +34,7 @@ test("a set on one key never drops a still-queued write to another", async () => test("loads an existing electron-store-format file as-is", async () => { const dir = await scratch() - const path = join(dir, "opencode.settings") + const path = join(dir, "codev.settings") // electron-store with fileExtension "" wrote flat keys as tab-indented JSON. await writeFile(path, `{\n\t"defaultServerUrl": "sandbox:codev-work",\n\t"sandboxes": {\n\t\t"sandboxes": []\n\t}\n}`) const store = createFileStore(path) diff --git a/packages/desktop/src/main/store-keys.ts b/packages/desktop/src/main/store-keys.ts index 0a59f2d53d..1aafc3df52 100644 --- a/packages/desktop/src/main/store-keys.ts +++ b/packages/desktop/src/main/store-keys.ts @@ -1,4 +1,4 @@ -export const SETTINGS_STORE = "opencode.settings" +export const SETTINGS_STORE = "codev.settings" export const DEFAULT_SERVER_URL_KEY = "defaultServerUrl" export const FIRST_LAUNCH_ONBOARDING_COMPLETE_KEY = "firstLaunchOnboardingComplete" export const OLD_LAYOUT_ELIGIBLE_KEY = "oldLayoutEligible" diff --git a/packages/desktop/src/main/store-rebrand.test.ts b/packages/desktop/src/main/store-rebrand.test.ts new file mode 100644 index 0000000000..b0b33bbbd5 --- /dev/null +++ b/packages/desktop/src/main/store-rebrand.test.ts @@ -0,0 +1,60 @@ +import { afterEach, describe, expect, test } from "bun:test" +import { mkdir, mkdtemp, readdir, readFile, rm, writeFile } from "node:fs/promises" +import { tmpdir } from "node:os" +import { join } from "node:path" +import { rebrandStoreName, renameLegacyStoreFiles } from "./store-rebrand" + +const roots: string[] = [] + +async function tempRoot() { + const root = await mkdtemp(join(tmpdir(), "codev-store-rebrand-")) + roots.push(root) + return root +} + +afterEach(async () => { + await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true }))) +}) + +describe("store rebrand", () => { + test("renames pre-rebrand store files and leaves everything else alone", async () => { + const root = await tempRoot() + await writeFile(join(root, "opencode.settings"), '{"defaultServerUrl":"http://localhost:4096"}') + await writeFile(join(root, "opencode.global.dat"), '{"server":{}}') + await writeFile(join(root, "opencode.workspace.home-foo.abc.dat"), '{"workspace:layout":"wide"}') + await writeFile(join(root, "default.dat"), "{}") + await writeFile(join(root, "window-state-main.json"), "{}") + await mkdir(join(root, "opencode")) + + const renamed = renameLegacyStoreFiles(root) + + expect(renamed.sort()).toEqual(["codev.global.dat", "codev.settings", "codev.workspace.home-foo.abc.dat"]) + expect((await readdir(root)).sort()).toEqual([ + "codev.global.dat", + "codev.settings", + "codev.workspace.home-foo.abc.dat", + "default.dat", + "opencode", + "window-state-main.json", + ]) + expect(await readFile(join(root, "codev.settings"), "utf8")).toBe('{"defaultServerUrl":"http://localhost:4096"}') + }) + + test("keeps the rebranded file when both names exist", async () => { + const root = await tempRoot() + await writeFile(join(root, "opencode.global.dat"), '{"server":"old"}') + await writeFile(join(root, "codev.global.dat"), '{"server":"new"}') + + expect(renameLegacyStoreFiles(root)).toEqual([]) + expect(await readFile(join(root, "codev.global.dat"), "utf8")).toBe('{"server":"new"}') + }) + + test("does nothing on a first run", async () => { + expect(renameLegacyStoreFiles(join(await tempRoot(), "missing"))).toEqual([]) + }) + + test("rebrands only the legacy prefix", () => { + expect(rebrandStoreName("opencode.workspace.a.b.dat")).toBe("codev.workspace.a.b.dat") + expect(rebrandStoreName("default.dat")).toBe("default.dat") + }) +}) diff --git a/packages/desktop/src/main/store-rebrand.ts b/packages/desktop/src/main/store-rebrand.ts new file mode 100644 index 0000000000..455986e89d --- /dev/null +++ b/packages/desktop/src/main/store-rebrand.ts @@ -0,0 +1,40 @@ +import { existsSync, readdirSync, renameSync } from "node:fs" +import { join } from "node:path" + +const LEGACY_PREFIX = "opencode." +const PREFIX = "codev." + +// Pre-rebrand store files were named after the upstream product (`opencode.settings`, +// `opencode.global.dat`, `opencode.workspace.<dir>.<sum>.dat`, …). Names without that +// prefix — Tauri's `default.dat`, the `window-state-*.json` files — are left alone. +export function rebrandStoreName(name: string) { + if (!name.startsWith(LEGACY_PREFIX)) return name + return PREFIX + name.slice(LEGACY_PREFIX.length) +} + +// Rename every pre-rebrand store file in place, once, so an upgrading install keeps +// its settings, recent servers and per-project UI state instead of starting empty. +// MUST run before the first getStore() and before the onboarding scan: a store opened +// under the new name would write a fresh file, and the rename would then find the +// target occupied and leave the user's real state stranded under the old name. +export function renameLegacyStoreFiles(userDataPath: string) { + const entries = existsSync(userDataPath) ? readdirSync(userDataPath, { withFileTypes: true }) : [] + return entries.flatMap((entry) => { + if (!entry.isFile()) return [] + if (!entry.name.startsWith(LEGACY_PREFIX)) return [] + + const renamed = rebrandStoreName(entry.name) + // Both names present means a rebranded run already wrote state; that file is the + // live one, so the leftover stays untouched rather than overwriting it. + if (existsSync(join(userDataPath, renamed))) return [] + + try { + renameSync(join(userDataPath, entry.name), join(userDataPath, renamed)) + } catch { + // A locked or vanished file must not stop the app from launching. The store + // opens empty for this run and the next launch retries the rename. + return [] + } + return [renamed] + }) +} diff --git a/packages/desktop/src/main/store.ts b/packages/desktop/src/main/store.ts index 8016b4edc2..6d0aa76efa 100644 --- a/packages/desktop/src/main/store.ts +++ b/packages/desktop/src/main/store.ts @@ -11,8 +11,8 @@ const cache = new Map<string, FileStore>() // We cannot instantiate a 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.codev.desktop.dev\opencode.settings). +// directory (e.g. bad: %APPDATA%\@opencode-ai\desktop\codev.settings vs +// good: %APPDATA%\ai.codev.desktop.dev\codev.settings). // // Stores write asynchronously (see store-file.ts): a synchronous fsync on the // main thread froze the whole app when a kernel journal stall never woke it, diff --git a/packages/desktop/src/main/updater.ts b/packages/desktop/src/main/updater.ts index c58fc17567..a30b2d128a 100644 --- a/packages/desktop/src/main/updater.ts +++ b/packages/desktop/src/main/updater.ts @@ -27,7 +27,7 @@ export function setupAutoUpdater(stop: () => Promise<void>) { currentVersion: app.getVersion(), }) - const store = getStore("opencode.updater") + const store = getStore("codev.updater") // Same store as the ready record: one file for everything the updater // remembers between launches. const readPrefs = () => { diff --git a/packages/desktop/src/main/windows.ts b/packages/desktop/src/main/windows.ts index 50e6f6076b..66e9694725 100644 --- a/packages/desktop/src/main/windows.ts +++ b/packages/desktop/src/main/windows.ts @@ -247,7 +247,7 @@ function windowStateFile(id: string) { // Mirrors windowStorage() in packages/app/src/utils/persist.ts, which names // the per-window renderer store this window persists its tabs into. function windowDataFile(id: string) { - return `opencode.window.${id.replace(/[^a-zA-Z0-9._-]/g, "-")}.dat` + return `codev.window.${id.replace(/[^a-zA-Z0-9._-]/g, "-")}.dat` } export function registerRendererProtocol() { diff --git a/packages/desktop/src/renderer/i18n/index.ts b/packages/desktop/src/renderer/i18n/index.ts index e69dedbd19..f795168206 100644 --- a/packages/desktop/src/renderer/i18n/index.ts +++ b/packages/desktop/src/renderer/i18n/index.ts @@ -88,7 +88,7 @@ export function initI18n(): Promise<Locale> { if (cached) return cached const promise = (async () => { - const raw = await window.api.storeGet("opencode.global.dat", "language").catch(() => null) + const raw = await window.api.storeGet("codev.global.dat", "language").catch(() => null) const value = parseStored(raw) const next = pickLocale(value) ?? state.locale diff --git a/packages/desktop/src/renderer/index.tsx b/packages/desktop/src/renderer/index.tsx index 0d2587d6a8..488ab1773e 100644 --- a/packages/desktop/src/renderer/index.tsx +++ b/packages/desktop/src/renderer/index.tsx @@ -359,7 +359,7 @@ function LoadingSplash() { function DesktopRoot(props: { windowState: DesktopWindowState }) { const platform = createPlatform(props.windowState) const loadLocale = async () => { - const current = await platform.storage?.("opencode.global.dat").getItem("language") + const current = await platform.storage?.("codev.global.dat").getItem("language") const legacy = current ? undefined : await platform.storage?.().getItem("language.v1") const raw = current ?? legacy if (!raw) return