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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ file is the human-readable summary of what changed and why.
now states exactly that before anything happens — and clarifies that folders
mounted from this computer are not deleted, the sandbox just loses access to
them.
- **Removing a WSL server asks for confirmation and forgets its projects** —
the same treatment as sandboxes, adapted to what actually happens: removing
a WSL server deletes nothing — the distribution, its files and its sessions
stay on this computer, and it can be added back at any time — but the app
now says so before removing it, and clears the server's remembered projects
and recently closed folders so adding the distribution back starts with a
clean list instead of inheriting its previous life.
- **A recreated sandbox no longer inherits its predecessor's projects** —
deleting a sandbox left its project list behind, keyed by the sandbox's
name, so creating a new sandbox with the same name brought back every old
Expand Down
2 changes: 1 addition & 1 deletion DIVERGENCES.md
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +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 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:` keys, awaits a confirmation through the fork-only `packages/app/src/sandbox/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)`. The controller gains a `useDialog()` binding for this. i18n gains the `sandbox.delete.*` family. | `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 sandbox whose disk was just destroyed — 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. And deleting a sandbox erases sessions, terminals and guest files permanently, so it must not be a one-click menu action; the confirmation lives in `handleRemove` because the sandbox settings row and the generic server list both funnel through it. | Keep the confirm-and-forget block at the top of `handleRemove` and the `forget` call 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 sandbox as gone. HTTP and WSL removals deliberately keep their maps (an HTTP server still exists remotely; extending this to WSL distro deletion is an open follow-up). |
| 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
23 changes: 18 additions & 5 deletions packages/app/src/components/dialog-select-server.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -512,7 +512,8 @@ export function useServerManagementController(options: { onSelect?: () => void;
resetEdit()
})

// Fork: deleting a sandbox erases its disk, so it is confirmed with the
// Fork: deleting a sandbox erases its disk, and removing a WSL server
// forgets its remembered projects, so both are confirmed with the
// consequences spelled out. Confirmation lives here rather than at the menu
// items because every delete entry point funnels through handleRemove.
function confirmSandboxDelete(key: ServerConnection.Key) {
Expand All @@ -528,9 +529,21 @@ export function useServerManagementController(options: { onSelect?: () => void;
})
}

function confirmWslDelete(key: ServerConnection.Key) {
return new Promise<boolean>((resolve) => {
void import("@/wsl/dialog-delete").then((module) => {
dialog.push(
() => <module.DialogDeleteWslServer distro={key.slice("wsl:".length)} onConfirm={() => resolve(true)} />,
() => resolve(false),
)
})
})
}

async function handleRemove(key: ServerConnection.Key) {
try {
if (key.startsWith("sandbox:") && !(await confirmSandboxDelete(key))) return
if (key.startsWith("wsl:") && !(await confirmWslDelete(key))) return
// Close the tabs first: destroying a sandbox or a WSL distro takes as
// long as deleting its disk, and the connection leaves the list as soon
// as it completes. Cleaning up afterwards leaves tabs pointing at a
Expand All @@ -541,10 +554,10 @@ export function useServerManagementController(options: { onSelect?: () => void;
// rather than after a delete that takes as long as erasing a disk.
workspace.removedServer(key)
server.remove(key)
// Fork: a deleted sandbox's disk is gone, but the per-server project
// maps are keyed by `sandbox:<name>` and outlive it, so a recreated
// sandbox with the same name would inherit the old projects.
if (key.startsWith("sandbox:")) server.forget(key)
// Fork: the per-server project maps are keyed by `sandbox:<name>` /
// `wsl:<distro>` and outlive the server, so a recreated or re-added
// server with the same name would inherit the old projects.
if (key.startsWith("sandbox:") || key.startsWith("wsl:")) server.forget(key)
if (key.startsWith("wsl:")) await platform.wslServers?.removeServer(key)
if (key.startsWith("sandbox:")) await platform.sandboxes?.remove(key)
if ((await platform.getDefaultServer?.()) === key) {
Expand Down
6 changes: 6 additions & 0 deletions packages/app/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,12 @@ export const dict = {
"wsl.server.label": "WSL",
"wsl.server.menu.label": "WSL server",
"wsl.server.retryStart": "Retry start",
"wsl.delete.title": "Remove WSL server",
"wsl.delete.confirm": "Remove {{name}}?",
"wsl.delete.distro":
"The distribution is not deleted: its files and sessions stay on this computer, and you can add it back at any time.",
"wsl.delete.projects": "Its remembered projects and tabs are removed; adding it back starts with a clean project list.",
"wsl.delete.button": "Remove server",
"wsl.server.updating": "Updating...",
"workspace.code": "Code",
"workspace.work": "Work",
Expand Down
7 changes: 7 additions & 0 deletions packages/app/src/i18n/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,13 @@ export const dict = {
"wsl.server.label": "WSL",
"wsl.server.menu.label": "Servidor WSL",
"wsl.server.retryStart": "Reintentar el inicio",
"wsl.delete.title": "Quitar servidor WSL",
"wsl.delete.confirm": "¿Quitar {{name}}?",
"wsl.delete.distro":
"La distribución no se elimina: sus archivos y sesiones permanecen en este equipo, y puedes volver a añadirla en cualquier momento.",
"wsl.delete.projects":
"Sus proyectos y pestañas recordados se quitan; al volver a añadirla se empieza con una lista de proyectos limpia.",
"wsl.delete.button": "Quitar servidor",
"wsl.server.updating": "Actualizando...",
"workspace.code": "Código",
"workspace.work": "Trabajo",
Expand Down
47 changes: 47 additions & 0 deletions packages/app/src/wsl/dialog-delete.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
import { Dialog, DialogBody, DialogFooter, DialogHeader, DialogTitle } from "@opencode-ai/ui/v2/dialog-v2"
import { DividerV2 } from "@opencode-ai/ui/v2/divider-v2"
import { useLanguage } from "@/context/language"

// Confirms removing a WSL server with the consequences spelled out. Unlike
// deleting a sandbox this destroys nothing — the app never unregisters
// distributions, so the distro's files and sessions stay on the machine — but
// the app forgets the server's remembered projects and tabs, so adding the
// distribution back starts with a clean list rather than inheriting its
// previous life. Reached through useServerManagementController.handleRemove,
// like the sandbox confirmation.
export function DialogDeleteWslServer(props: { distro: string; onConfirm: () => void }) {
const language = useLanguage()
const dialog = useDialog()

return (
<Dialog fit>
<DialogHeader hideClose={true}>
<DialogTitle>{language.t("wsl.delete.title")}</DialogTitle>
</DialogHeader>
<DividerV2 />
<DialogBody class="flex max-w-[440px] flex-col gap-3 px-4 pt-3 pb-1">
<p class="text-14">{language.t("wsl.delete.confirm", { name: props.distro })}</p>
<ul class="flex list-disc flex-col gap-2 pl-5 text-12 text-v2-text-text-muted">
<li>{language.t("wsl.delete.distro")}</li>
<li>{language.t("wsl.delete.projects")}</li>
</ul>
</DialogBody>
<DialogFooter>
<ButtonV2 variant="neutral" onClick={() => dialog.close()}>
{language.t("common.cancel")}
</ButtonV2>
<ButtonV2
variant="danger"
onClick={() => {
props.onConfirm()
dialog.close()
}}
>
{language.t("wsl.delete.button")}
</ButtonV2>
</DialogFooter>
</Dialog>
)
}
Loading