Skip to content

Commit 76de08c

Browse files
quickbeardclaude
andcommitted
refactor(desktop): name the sandbox Work creates default-sandbox
Renamed from codev-sandbox. It says what the sandbox is — the one the app made without asking — rather than repeating the product name, which every sandbox on the machine shares. The name is not only a label: it becomes the sandbox id (sandbox:default-sandbox), its data directory, its placeholder project folder, and on Windows the WSL distribution name. Nothing migrates, because nothing has shipped: a sandbox created from a build before this keeps the old name and goes on working, and only deleting and re-adding it picks the new one up. Left alone: the bubblewrap guest hostname and the codev-sandbox package, docker image tags and os-release ID, none of which name this sandbox. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent d2a3981 commit 76de08c

5 files changed

Lines changed: 5 additions & 5 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ file is the human-readable summary of what changed and why.
2929
CPU count and a memory size, all before anything could happen. The folder
3030
question was the worst of them, because at that point you have not decided
3131
what you want to work on. Work now asks for none of it. It creates a sandbox
32-
called `codev-sandbox` with the standard image and the standard limits, and
32+
called `default-sandbox` with the standard image and the standard limits, and
3333
the only thing you can still choose is a guest image file of your own, for a
3434
machine that cannot reach the download. Once it is ready, Work asks you to
3535
mount a project — the folder on this computer you want to work on, which the

‎DIVERGENCES.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ from the squash message first, or use a plain merge.
209209
| 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. |
210210
| `packages/app/src/components/server/server-row-menu.tsx` | The trailing **Delete** item carries `style={{ "--menu-v2-fg": "var(--v2-state-fg-danger)" }}`. Nothing else changes — same `disabled={builtin}`, same `handleRemove(key)`, same position after the separator. | Delete rendered in the same colour as Edit and Set as default, so the one irreversible action in the menu carried no more weight than the rest. The session actions menu already colours its Delete this way; this brings the server menu in line. It has to be the CSS variable rather than a text utility: `menu-v2.css` colours the item **and** its `menu-v2-item-content` label span from `--menu-v2-fg`, so a class on the item never reaches the label. | Keep the style prop. On conflict take upstream's item and re-apply it. Do not swap it for a Tailwind text colour — that silently only recolours half the item. The disabled state dims by `opacity` alone, so the red survives it. |
211211
| 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` and, on the desktop variant, `openFilePathDialog` (a path-only file picker — the existing attachment picker reads the file's bytes, which is unusable for a multi-gigabyte guest image), implemented in `renderer/index.tsx` over the existing `open-file-picker` channel and releasing the read token immediately; `server.tsx` adds the third `ServerConnection.Sidecar` variant `{ variant: "sandbox"; name; projectDir?; mounts? }` with key `sandbox:<name>` (`projectDir` is the host folder mounted into the guest and `mounts` the extra host folders with their resolved guest paths — the guest only ever sees its own mount points, so this is the only way the app can name a sandbox's projects after the folders 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; `sandbox.add.projectDirHint`, the standing explanation above the folder field; the `sandbox.mounts.*` family for the manage-folders dialog that adds/removes extra mounts on an existing sandbox; `sandbox.job.queued`, shown while a sandbox's create/reset/destroy waits its turn behind another sandbox's job; `sandbox.server.stop` for the menu entry that shuts a sandbox down until it is explicitly started again; the `sandbox.add.image*` / `sandbox.add.name*` / `sandbox.arch.*` families for the optional custom-image field and the sandbox-name checks; and `sandbox.add.resourcesHint` plus interpolated `sandbox.add.{cpus,memory,resourcesInvalid}`, which quote the vz defaults and ranges from the constants rather than restating them in prose); 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:<name>` 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. |
212-
| One-click sandbox for Work: `packages/app/src/context/server.tsx`, `packages/app/src/components/app-sidebar.tsx`, `packages/app/src/i18n/{en,es}.ts`, `packages/desktop/src/preload/index.ts` | Work's "you need a sandbox" gate opens the fork-only `src/sandbox/dialog-add-default.tsx` instead of `DialogAddSandbox`: it asks only for an optional custom image, and the main process decides everything else — the name (`codev-sandbox`), the project folder, and the limits, which it leaves unset so the backend default applies. That folder is a placeholder the app invents (fork-only `src/main/sandbox/workspace.ts`, under `%LOCALAPPDATA%` on Windows because a redirected `%APPDATA%` can be a UNC path and `wslpath` cannot translate one), so `SandboxConfig` carries `placeholderProject` and the upstream files here keep it out of the user's way: `server.tsx` adds `placeholderProject` to the `Sidecar` sandbox variant, and `app-sidebar.tsx` renders the fork-only `<SandboxMountPrompt>` in place of upstream's `HomeProjectEmpty` for such a sandbox — upstream's picker browses the server it is pointed at, which for a sandbox is the guest's own home. The fork-only `dialog-mounts.tsx` hides its primary-folder row for the same sandbox: that path is app data the user did not choose and cannot remove, and the dialog would otherwise be the one place they meet it. `preload/index.ts` exposes `sandbox-add-default` and `sandbox-cancel-default` (its own channel because a sandbox mid-create is not in the list the renderer can see, so there is no name to cancel by). i18n gains `sandbox.addDefault.*` and `sandbox.mountPrompt.*`. Settings → Servers still opens the full `DialogAddSandbox`, unchanged. | A user reaching Work for the first time was asked for a name, a folder, an image, a CPU count and a memory size before they could do anything, and the folder question has no good answer yet — they have not decided what to work on. Deciding it for them and asking for a folder afterwards, as a mount, matches what the sandbox actually is: an environment whose reach into the machine is the folders you give it. | Keep the wiring and take upstream's surrounding changes. If upstream reworks the empty-projects fallback in `app-sidebar.tsx`, keep the placeholder branch ahead of it. Do not "simplify" the placeholder away by pointing the sandbox at the user's home or an existing project: the guest gets read-write access to its project folder. `DEFAULT_SANDBOX_NAME` is persisted as a sandbox id (`sandbox:codev-sandbox`) and as a WSL distribution name — changing it strands existing sandboxes. |
212+
| One-click sandbox for Work: `packages/app/src/context/server.tsx`, `packages/app/src/components/app-sidebar.tsx`, `packages/app/src/i18n/{en,es}.ts`, `packages/desktop/src/preload/index.ts` | Work's "you need a sandbox" gate opens the fork-only `src/sandbox/dialog-add-default.tsx` instead of `DialogAddSandbox`: it asks only for an optional custom image, and the main process decides everything else — the name (`default-sandbox`), the project folder, and the limits, which it leaves unset so the backend default applies. That folder is a placeholder the app invents (fork-only `src/main/sandbox/workspace.ts`, under `%LOCALAPPDATA%` on Windows because a redirected `%APPDATA%` can be a UNC path and `wslpath` cannot translate one), so `SandboxConfig` carries `placeholderProject` and the upstream files here keep it out of the user's way: `server.tsx` adds `placeholderProject` to the `Sidecar` sandbox variant, and `app-sidebar.tsx` renders the fork-only `<SandboxMountPrompt>` in place of upstream's `HomeProjectEmpty` for such a sandbox — upstream's picker browses the server it is pointed at, which for a sandbox is the guest's own home. The fork-only `dialog-mounts.tsx` hides its primary-folder row for the same sandbox: that path is app data the user did not choose and cannot remove, and the dialog would otherwise be the one place they meet it. `preload/index.ts` exposes `sandbox-add-default` and `sandbox-cancel-default` (its own channel because a sandbox mid-create is not in the list the renderer can see, so there is no name to cancel by). i18n gains `sandbox.addDefault.*` and `sandbox.mountPrompt.*`. Settings → Servers still opens the full `DialogAddSandbox`, unchanged. | A user reaching Work for the first time was asked for a name, a folder, an image, a CPU count and a memory size before they could do anything, and the folder question has no good answer yet — they have not decided what to work on. Deciding it for them and asking for a folder afterwards, as a mount, matches what the sandbox actually is: an environment whose reach into the machine is the folders you give it. | Keep the wiring and take upstream's surrounding changes. If upstream reworks the empty-projects fallback in `app-sidebar.tsx`, keep the placeholder branch ahead of it. Do not "simplify" the placeholder away by pointing the sandbox at the user's home or an existing project: the guest gets read-write access to its project folder. `DEFAULT_SANDBOX_NAME` is persisted as a sandbox id (`sandbox:default-sandbox`) and as a WSL distribution name — changing it strands existing sandboxes. |
213213
| Sandbox guest durability: `packages/core/src/database/database.ts`, `packages/core/src/flag/flag.ts` | `PRAGMA synchronous` is no longer hardcoded to `NORMAL`: it reads `Flag.CODEV_SQLITE_SYNCHRONOUS`, validated against a whitelist (`OFF`/`NORMAL`/`FULL`/`EXTRA`) because the value lands in a PRAGMA, and falls back to `NORMAL` — so nothing changes for any process that does not set it. The sandbox sets it to `FULL` for guests (fork-only `packages/codev-sandbox/src/boot.ts`). | WAL + `synchronous = NORMAL` is durable across a crashed process but not across a machine that disappears, because those commits sit in the page cache until a checkpoint. A sandbox guest is exactly that machine: `VZVirtualMachine.stop()` is documented as pulling the power cable, and the guest gets no chance to sync — so stopping a sandbox, restarting one to apply a mount, or force quitting the app dropped every session written in the preceding seconds. Measured on a real vz guest: a file written and left unsynced is gone after a plain Stop/Start; with `FULL` a session created two seconds before `kill -9` of the whole app survives. The knob lives in core because the guest runs the host's own server bundle — there is no sandbox-only build to patch. | Keep the flag and the whitelist; take upstream's other pragmas. If upstream makes synchronous configurable its own way, drop ours for theirs. Never widen the whitelist to pass the value through unchecked. The default must stay `NORMAL` — raising it for every install would make the host pay for a guarantee only the sandbox needs. |
214214
| `packages/desktop/src/renderer/index.tsx` | Desktop notification icon `https://opencode.ai/favicon-96x96-v3.png` → the relative `./favicon-96x96-v3.png`. The renderer's `publicDir` is `packages/app/public`, whose `favicon-96x96-v3.png` symlinks to the rebranded `packages/ui/src/assets/favicon/` mark, so the file is emitted next to `index.html` and resolves against the renderer origin (`oc://renderer/` packaged, the vite dev server otherwise) — the same asset and the same relative form `src/renderer/index.html` already uses for `<link rel="icon">`. | A desktop app should not reach out to a remote host to draw its own notification icon (it fails offline and leaks a request per notification), and the remote file is upstream-branded while the bundled one is the codev mark. | Keep our relative path. If upstream changes the URL, re-point it at the bundled asset rather than taking a new remote host. `packages/app/src/entry.tsx` and `pages/layout/helpers.ts` keep their remote `opencode.ai/favicon*` sources — same bug class, not yet fixed. |
215215

‎packages/app/src/sandbox/forget-folder.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { expect, test } from "bun:test"
22
import type { ServerConnection } from "@/context/server"
33
import { forgetSandboxFolder } from "./forget-folder"
44

5-
const server = "sandbox:codev-sandbox" as unknown as ServerConnection.Key
5+
const server = "sandbox:default-sandbox" as unknown as ServerConnection.Key
66

77
function recorder() {
88
const removedProjects: string[] = []

‎packages/app/src/workspace/tab-directory.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { ServerConnection } from "@/context/server"
33
import type { Tab } from "@/context/tabs"
44
import { tabsInDirectory } from "./tab-directory"
55

6-
const server = "sandbox:codev-sandbox" as ServerConnection.Key
6+
const server = "sandbox:default-sandbox" as ServerConnection.Key
77

88
describe("tabs of a mounted folder", () => {
99
const other = "sandbox:other" as ServerConnection.Key

‎packages/desktop/src/main/sandbox/workspace.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { posix, win32 } from "node:path"
55
// rather than generated: there is only ever one of these, the user sees it in
66
// the server list, and a name it keeps across machines is easier to talk about
77
// than "sandbox-3f2a".
8-
export const DEFAULT_SANDBOX_NAME = "codev-sandbox"
8+
export const DEFAULT_SANDBOX_NAME = "default-sandbox"
99

1010
// The project folders the app invents for itself. A sandbox cannot boot without
1111
// a working directory and the simple flow does not ask for one, so this is

0 commit comments

Comments
 (0)