Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ file is the human-readable summary of what changed and why.

### Desktop / Web UI

- **A new sandbox's project no longer flashes "workspace"** — a sandbox's
project folder is mounted inside the guest at a fixed path, and until the
app finished renaming the project after your folder — a round trip through
the sandbox's server — the sidebar showed that guest path's name,
"workspace", instead. The app already knows which folder each guest path
came from, so an unnamed sandbox project now shows the real folder name from
the first moment; the rename still happens underneath and any name you set
yourself still wins.
- **Deleting a sandbox now asks for confirmation and spells out what is lost**
— Delete in a sandbox's menu acted immediately, and it is nothing like
removing an ordinary server entry: it erases the sandbox's disk, taking
Expand Down
1 change: 1 addition & 0 deletions DIVERGENCES.md
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,7 @@ from the squash message first, or use a plain merge.
| `packages/opencode/src/command/template/initialize.txt` | "OpenCode" → "CoDev Code" (×2) | `/init` writes this guidance into the user's `AGENTS.md`. Edited in place rather than wrapped because the file is already fork-divergent (config-file rename row) and barely churns. | Keep our two strings; take upstream's surrounding text and rebrand any new "OpenCode" mention. |
| `packages/core/package.json` | Added dependency `"@codev/brand": "workspace:*"` | The built-in skill registration rebrands its body through the fork-only `packages/codev-brand`. | Keep our dependency line; take upstream's other fields. |
| `turbo.json` | Added task `"@codev/sandbox#test"` (no deps, no outputs) | CI's `bun turbo test` only runs test tasks registered here; the fork-only sandbox package's tests must gate PRs. | Keep our task entry; take upstream's other tasks. |
| Sandbox project label fallback: `packages/app/src/context/global.tsx` | `enrich()` (the per-server project-list decorator) gains one fork line: when the merged project record has no `name` yet, it fills it from the fork-only `sandboxProjectHostName(conn, worktree, primary)` (`packages/app/src/sandbox/projects.ts`), which maps a sandbox connection's guest paths back to host folder basenames — `conn.projectDir` for the primary (matched by the synced primary directory, or the `/workspace` literal before sync loads) and `conn.mounts[].path` for extras. Non-sandbox connections and named projects are untouched (the helper returns `undefined` / the branch is skipped). | A fresh sandbox project renders `displayName()` = name-or-worktree-basename, and inside the guest the primary worktree is `/workspace`, so every new sandbox flashed the label "workspace" until `adoptFolderName`'s server-side rename round-tripped (guest reports the project → `project.update` → `project.updated` event). The fallback is display-only; the durable rename through the server is unchanged and takes precedence the moment it lands. | Keep the one `if (!base.name)` line after upstream's `base` construction and take upstream's surrounding `enrich` changes. If upstream renames or restructures `enrich`, re-apply the fallback wherever project records are decorated for the sidebar list. The helper stays in the fork-only sandbox module — do not inline it. |
| Sandbox/WSL delete cleanup + confirmation: `packages/app/src/context/server.tsx`, `packages/app/src/components/dialog-select-server.tsx`, `packages/app/src/i18n/{en,es}.ts` | `server.tsx` gains `forget(key)` beside `remove(key)`: it deletes the persisted per-server `projects`/`lastProject`/`recentlyClosed` map entries for one key. `dialog-select-server.tsx` `handleRemove` (already diverged, see the renderer-slice row) additionally: for `sandbox:` and `wsl:` keys, awaits a confirmation through the fork-only `packages/app/src/sandbox/dialog-delete.tsx` / `packages/app/src/wsl/dialog-delete.tsx` (lazy-imported, pushed with `dialog.push` so it stacks on whatever dialog opened the menu) and returns without touching anything when declined; and calls `server.forget(key)` right after `server.remove(key)` for both key kinds. The controller gains a `useDialog()` binding for this. i18n gains the `sandbox.delete.*` and `wsl.delete.*` families. The two dialogs state different consequences on purpose: deleting a sandbox erases its disk (sessions, terminals, guest files — permanently; mounted host folders survive), while removing a WSL server destroys nothing — the app never unregisters distributions — it only forgets the app-side project state. | `remove()` only edits the stored HTTP server list; the project maps are keyed by server key and outlive the server, which is right for an HTTP server that still exists remotely but wrong for a deleted sandbox or removed distro — a recreated sandbox with the same name inherited the old projects (`server.projects["sandbox:<name>"]`) and, via the leftover `recentlyClosed`, silently refused to seed folders closed in the previous sandbox's life; a re-added distro likewise inherited its previous project list. And deleting a sandbox erases guest data permanently, so it must not be a one-click menu action; the confirmations live in `handleRemove` because the settings rows and the generic server list all funnel through it. | Keep the confirm-and-forget block at the top of `handleRemove` and the `forget` calls between `server.remove(key)` and the awaited platform removals; take upstream's surrounding changes. `forget` is fork-only API — if upstream reworks the persisted `server` record shape, re-point the three deletes at the new maps. Do not gate `forget` behind the platform removal succeeding: the tab/workspace cleanup ahead of it already treats the server as gone. HTTP removals deliberately keep their maps — the server still exists remotely and re-adding it should restore its projects. If the app ever gains real distro unregistration, the WSL dialog's "nothing is deleted" wording must change with it. |

## Known rebrand gaps
Expand Down
7 changes: 6 additions & 1 deletion packages/app/src/context/global.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { pathKey } from "@/utils/path-key"
import { useServerHealth } from "@/utils/server-health"
import { createServerSdkContext } from "./server-sdk"
import { createServerSyncContext } from "./server-sync"
import { seedSandboxProjects } from "@/sandbox/projects"
import { sandboxProjectHostName, seedSandboxProjects } from "@/sandbox/projects"
import { getOwner } from "solid-js/web"
import { QueryClient } from "@tanstack/solid-query"
import type { ServerScope } from "@/utils/server-scope"
Expand Down Expand Up @@ -124,6 +124,11 @@ function createServerCtx(
// Without this, different subdirectories of the same git repo would share the same
// icon from the database instead of using their individual overrides.
const base = { ...metadata, ...project }
// Fork: an unnamed sandbox project would render its guest path's basename
// ("workspace") until the seeding rename lands; show the host folder's
// name the connection already carries instead.
if (!base.name)
base.name = sandboxProjectHostName(conn, project.worktree, sync.data.path.directory || sync.data.path.worktree)
if (childStore.icon) {
return { ...base, icon: { ...base.icon, override: childStore.icon } }
}
Expand Down
38 changes: 38 additions & 0 deletions packages/app/src/sandbox/projects.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { expect, test } from "bun:test"
import type { ServerConnection } from "@/context/server"
import { sandboxProjectHostName } from "./projects"

const sandbox = (input?: Partial<{ projectDir: string; mounts: { path: string; guestPath: string }[] }>) =>
({
type: "sidecar",
variant: "sandbox",
name: "codev-work",
projectDir: "/home/user/repos/openwork",
http: { url: "http://127.0.0.1:4096" },
...input,
}) satisfies ServerConnection.Any

test("names the primary folder after the host project dir", () => {
const conn = sandbox()
expect(sandboxProjectHostName(conn, "/workspace", "/workspace")).toBe("openwork")
// WSL reports the translated host path as the primary directory.
expect(sandboxProjectHostName(conn, "/mnt/c/repos/openwork", "/mnt/c/repos/openwork")).toBe("openwork")
})

test("covers /workspace before sync has reported the primary directory", () => {
expect(sandboxProjectHostName(sandbox(), "/workspace", undefined)).toBe("openwork")
expect(sandboxProjectHostName(sandbox(), "/workspace", "")).toBe("openwork")
})

test("names extra mounts after their host folder", () => {
const conn = sandbox({
mounts: [{ path: "/home/user/works/vibe-coding", guestPath: "/mnt/vibe-coding" }],
})
expect(sandboxProjectHostName(conn, "/mnt/vibe-coding", "/workspace")).toBe("vibe-coding")
})

test("returns nothing for unknown guest paths and non-sandbox servers", () => {
expect(sandboxProjectHostName(sandbox(), "/mnt/unknown", "/workspace")).toBeUndefined()
const http: ServerConnection.Any = { type: "http", http: { url: "http://example.com" } }
expect(sandboxProjectHostName(http, "/workspace", "/workspace")).toBeUndefined()
})
22 changes: 22 additions & 0 deletions packages/app/src/sandbox/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,28 @@ type SandboxServerCtx = {
}
}

// The guest only ever sees its mount points, so until adoptFolderName's
// rename (below) lands, an unnamed sandbox project's label falls back to the
// guest path's basename — "workspace" for every primary folder. The app
// already knows which host folder each guest path came from; label with its
// name in the meantime. Display-only: the durable rename still goes through
// the server.
export function sandboxProjectHostName(
conn: ServerConnection.Any,
worktree: string,
primaryDirectory: string | undefined,
) {
if (conn.type !== "sidecar" || conn.variant !== "sandbox") return undefined
// bwrap/vz guests mount the project folder at /workspace; WSL mounts it at
// the translated host path, whose basename already matches the host folder.
// The synced primary covers any backend; the /workspace literal covers the
// moment before sync has loaded on a relaunch.
const primary = worktree === "/workspace" || (!!primaryDirectory && worktree === primaryDirectory)
if (primary) return getFilename(conn.projectDir ?? "") || undefined
const mount = conn.mounts?.find((entry) => entry.guestPath === worktree)
return mount ? getFilename(mount.path) || undefined : undefined
}

// A sandbox exposes the project folder it was created with — mounted at a
// fixed guest path: /workspace on macOS and Linux, the translated Windows
// path on WSL — plus any extra folders mounted later, each at its own guest
Expand Down
Loading