From 7f38be8ebbde1b6e0436c0af538a0a35d6d73dde Mon Sep 17 00:00:00 2001 From: Quickbeard Date: Mon, 17 Aug 2026 14:47:27 +0700 Subject: [PATCH] fix(app): confirm WSL server removal and clear its remembered projects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same treatment as sandbox deletion, adapted to what removal actually does: the app never unregisters distributions, so removing a WSL server destroys nothing — the distro, its files and its sessions stay on the machine. The confirmation dialog says exactly that, and server.forget now runs for wsl: keys too, so re-adding the distribution starts with a clean project list instead of inheriting the removed server's projects and recently-closed folders. Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 7 +++ DIVERGENCES.md | 2 +- .../src/components/dialog-select-server.tsx | 23 +++++++-- packages/app/src/i18n/en.ts | 6 +++ packages/app/src/i18n/es.ts | 7 +++ packages/app/src/wsl/dialog-delete.tsx | 47 +++++++++++++++++++ 6 files changed, 86 insertions(+), 6 deletions(-) create mode 100644 packages/app/src/wsl/dialog-delete.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index f203ee2a60..917c9f826e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/DIVERGENCES.md b/DIVERGENCES.md index 307eadc793..d33edb966b 100644 --- a/DIVERGENCES.md +++ b/DIVERGENCES.md @@ -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:"]`) 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:"]`) 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 diff --git a/packages/app/src/components/dialog-select-server.tsx b/packages/app/src/components/dialog-select-server.tsx index 874fee847d..157cad5a26 100644 --- a/packages/app/src/components/dialog-select-server.tsx +++ b/packages/app/src/components/dialog-select-server.tsx @@ -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) { @@ -528,9 +529,21 @@ export function useServerManagementController(options: { onSelect?: () => void; }) } + function confirmWslDelete(key: ServerConnection.Key) { + return new Promise((resolve) => { + void import("@/wsl/dialog-delete").then((module) => { + dialog.push( + () => 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 @@ -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:` 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:` / + // `wsl:` 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) { diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index 739b8dd3ab..bacaa6d2ad 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -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", diff --git a/packages/app/src/i18n/es.ts b/packages/app/src/i18n/es.ts index eaee26154e..f67bd1443c 100644 --- a/packages/app/src/i18n/es.ts +++ b/packages/app/src/i18n/es.ts @@ -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", diff --git a/packages/app/src/wsl/dialog-delete.tsx b/packages/app/src/wsl/dialog-delete.tsx new file mode 100644 index 0000000000..3cd3662642 --- /dev/null +++ b/packages/app/src/wsl/dialog-delete.tsx @@ -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 ( + + + {language.t("wsl.delete.title")} + + + +

{language.t("wsl.delete.confirm", { name: props.distro })}

+
    +
  • {language.t("wsl.delete.distro")}
  • +
  • {language.t("wsl.delete.projects")}
  • +
+
+ + dialog.close()}> + {language.t("common.cancel")} + + { + props.onConfirm() + dialog.close() + }} + > + {language.t("wsl.delete.button")} + + +
+ ) +}