diff --git a/CHANGELOG.md b/CHANGELOG.md index ccd9cef9b1..7275f96002 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,23 @@ file is the human-readable summary of what changed and why. ### Desktop / Web UI +- **The Mount project dialog can no longer silently unmount everything** — + opened moments after launch, before the sandbox state had loaded, the dialog + showed the mounted folders as an empty list: the list was captured once when + the dialog opened and never updated, so it stayed empty even after the state + arrived a beat later. Saving from that dialog then wrote the empty list back, + quietly unmounting every extra folder — while their projects stayed in the + sidebar, still looking mounted. The dialog now waits for the sandbox's state + before it builds the folder list. +- **The Work sidebar's folder button now mounts a folder into the sandbox** — + the button next to **Projects** on the Work side used to open a directory + picker over the sandbox's own filesystem, the same "Add project" it is on + Code — where folders reach a sandbox by being mounted, not picked. It now + opens the mount dialog for the sandbox, the same one as the server's menu + item in Settings, which is itself renamed from **Folders...** to **Mount + project** to match. The dialog's title follows suit (it was **Sandbox + folders**), and its intro line no longer warns that the agent edits mounted + files without asking. Code keeps the existing Add project behavior. - **First-run setup on the desktop app** — with no AI provider configured, the desktop app now opens a setup dialog that cannot be dismissed until a provider exists, offering the same three paths as the CLI's `codev diff --git a/packages/app/src/components/app-sidebar.tsx b/packages/app/src/components/app-sidebar.tsx index 062294d532..cf87ac375b 100644 --- a/packages/app/src/components/app-sidebar.tsx +++ b/packages/app/src/components/app-sidebar.tsx @@ -102,16 +102,33 @@ export function AppSidebar(props: { class?: string; debugTools?: { visible: bool !(model.projects().length === 0 && model.recentlyClosed().length > 0) } > - + {/* Fork: on Work this mounts a host folder into the sandbox — the + same dialog as the server row's Mount project item. The Code + behavior (a picker over the server's own filesystem) would + browse the guest here, which is not how folders reach a + sandbox. */} + } disabled={global.servers.health[ServerConnection.key(sideServers()[0]!)]?.healthy === false} - onClick={() => model.chooseProject(sideServers()[0]!)} - aria-label={language.t("home.project.add")} + onClick={() => { + const conn = sideServers()[0]! + if (side() === "work") { + void import("@/sandbox/dialog-mounts").then((module) => { + dialog.show(() => ) + }) + return + } + model.chooseProject(conn) + }} + aria-label={language.t(side() === "work" ? "sandbox.server.mounts" : "home.project.add")} /> diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index 06152e3f8d..b79a42317e 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -386,7 +386,7 @@ export const dict = { "sandbox.server.start": "Start", "sandbox.server.stop": "Stop", "sandbox.server.reset": "Reset", - "sandbox.server.mounts": "Folders...", + "sandbox.server.mounts": "Mount project", "sandbox.status.starting": "Starting...", "sandbox.status.ready": "Ready", "sandbox.status.stopped": "Stopped", @@ -425,9 +425,8 @@ export const dict = { "sandbox.add.resourcesInvalid": "CPUs must be {{minCpus}}\u2013{{maxCpus}} and memory {{minMemory}}\u2013{{maxMemory}} GB.", "sandbox.add.downloadProgress": "Downloading image… {{done}}/{{total}} MB", "sandbox.add.creating": "Creating the sandbox\u2026 this takes a few minutes.", - "sandbox.mounts.title": "Sandbox folders", - "sandbox.mounts.hint": - "Folders mounted here are the only ones on this computer the sandbox can reach, and the agent edits files in them without asking.", + "sandbox.mounts.title": "Mount project", + "sandbox.mounts.hint": "Folders mounted here are the only ones on this computer the sandbox can reach.", "sandbox.mounts.primary": "Project folder", "sandbox.mounts.empty": "No extra folders are mounted.", "sandbox.mounts.add": "Add folder...", diff --git a/packages/app/src/i18n/es.ts b/packages/app/src/i18n/es.ts index 91bbd368d5..5a7f627b76 100644 --- a/packages/app/src/i18n/es.ts +++ b/packages/app/src/i18n/es.ts @@ -377,7 +377,7 @@ export const dict = { "sandbox.server.start": "Iniciar", "sandbox.server.stop": "Detener", "sandbox.server.reset": "Restablecer", - "sandbox.server.mounts": "Carpetas...", + "sandbox.server.mounts": "Montar proyecto", "sandbox.status.starting": "Iniciando...", "sandbox.status.ready": "Listo", "sandbox.status.stopped": "Detenido", @@ -416,9 +416,8 @@ export const dict = { "sandbox.add.resourcesInvalid": "Las CPU deben ser {{minCpus}}\u2013{{maxCpus}} y la memoria {{minMemory}}\u2013{{maxMemory}} GB.", "sandbox.add.downloadProgress": "Descargando imagen… {{done}}/{{total}} MB", "sandbox.add.creating": "Creando el sandbox\u2026 esto tarda unos minutos.", - "sandbox.mounts.title": "Carpetas del sandbox", - "sandbox.mounts.hint": - "Las carpetas montadas aquí son las únicas de este equipo a las que el sandbox puede acceder, y el agente edita archivos en ellas sin preguntar.", + "sandbox.mounts.title": "Montar proyecto", + "sandbox.mounts.hint": "Las carpetas montadas aquí son las únicas de este equipo a las que el sandbox puede acceder.", "sandbox.mounts.primary": "Carpeta del proyecto", "sandbox.mounts.empty": "No hay carpetas adicionales montadas.", "sandbox.mounts.add": "Añadir carpeta...", diff --git a/packages/app/src/sandbox/dialog-mounts.tsx b/packages/app/src/sandbox/dialog-mounts.tsx index a3622b75af..60487a6f4e 100644 --- a/packages/app/src/sandbox/dialog-mounts.tsx +++ b/packages/app/src/sandbox/dialog-mounts.tsx @@ -18,6 +18,35 @@ import { validateSandboxMounts } from "./mounts-model" // else can be added and removed, applied hot on Windows and by restarting the // sandbox elsewhere. export function DialogSandboxMounts(props: { id: string }) { + const language = useLanguage() + const sandboxes = useSandboxes() + const item = () => sandboxes.state.data?.sandboxes.find((x) => x.config.id === props.id) + // The editor snapshots the mount list when it is created, so it must not be + // created before the sandbox state has loaded: a snapshot taken while the + // query is still empty freezes as an empty list, reads as a pending removal + // of every mount once the state arrives, and saving it silently unmounts + // everything. + return ( + + + {language.t("sandbox.mounts.title")} + + + + + + + } + > + + + ) +} + +function SandboxMountsEditor(props: { id: string }) { const language = useLanguage() const platform = usePlatform() const dialog = useDialog()