diff --git a/CHANGELOG.md b/CHANGELOG.md index 86941e35f6..e0de5c5509 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -111,6 +111,24 @@ file is the human-readable summary of what changed and why. ### Sandbox +- **The sandbox setup dialog explains what a sandbox buys you, and stops + asking for an image file** — the dialog Work opens when you have no sandbox + yet described the download and the wait, but never why any of it was + happening. It now leads with what the sandbox is: an isolated environment on + this computer that the agent can only reach through the folders you mount + into it, so it can run commands without touching the rest of your computer. + The optional "sandbox image file" picker below it is gone from beta and + release builds — the published image is the only one those builds can use, so + the field was a question with one answer. It stays in dev builds, where + supplying an unpublished image is how a sandbox build gets tried. + Settings → Servers loses the same field, for the same reason. + ([#304](https://github.com/quickbeard/codev-code/pull/304)) +- **Windows 10 no longer warns before you set a sandbox up** — setting up a + sandbox on Windows 10 22H2 said its support "is not fully verified" and + suggested upgrading to Windows 11. It has since been run there and works, so + the warning is gone. Builds older than 22H2 still warn: those Windows + versions are out of support and may not run WSL2 at all. + ([#304](https://github.com/quickbeard/codev-code/pull/304)) - **Work says it is loading your sandbox, instead of offering a session it cannot run** — while the sandbox that runs your sessions was still coming up, Work showed a search box over "Nothing here yet / Create a session to get diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index 21104b0c3d..d6709e2c79 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -393,7 +393,7 @@ export const dict = { "sandbox.add.workIntro": "Work requires a sandbox environment to run sessions.", "sandbox.addDefault.title": "Set up your sandbox", "sandbox.addDefault.intro": - "Work runs your sessions in a sandbox: an isolated environment on this computer. Setting one up downloads its image, which takes a few minutes. You choose which of your folders it can reach once it is ready.", + "Work runs your sessions in a sandbox: an isolated environment on this computer, with its own tools and files. The agent can only reach the folders you mount into it, so it can run commands without touching the rest of your computer. Setting one up downloads its image, which takes a few minutes; you pick the folders once it is ready.", "sandbox.addDefault.create": "Set up", "sandbox.wsl.installAction": "Install WSL", "sandbox.wsl.installRetry": "Try again", diff --git a/packages/app/src/i18n/es.ts b/packages/app/src/i18n/es.ts index 727a7c69f2..8bfdc49e48 100644 --- a/packages/app/src/i18n/es.ts +++ b/packages/app/src/i18n/es.ts @@ -384,7 +384,7 @@ export const dict = { "sandbox.add.workIntro": "Trabajo requiere un entorno de sandbox para ejecutar sesiones.", "sandbox.addDefault.title": "Configura tu sandbox", "sandbox.addDefault.intro": - "Trabajo ejecuta tus sesiones en un sandbox: un entorno aislado en este equipo. Configurarlo descarga su imagen, lo que tarda unos minutos. Tú eliges a qué carpetas tuyas puede acceder cuando esté listo.", + "Trabajo ejecuta tus sesiones en un sandbox: un entorno aislado en este equipo, con sus propias herramientas y archivos. El agente solo puede acceder a las carpetas que montes en él, así que puede ejecutar comandos sin tocar el resto de tu equipo. Configurarlo descarga su imagen, lo que tarda unos minutos; eliges las carpetas cuando esté listo.", "sandbox.addDefault.create": "Configurar", "sandbox.wsl.installAction": "Instalar WSL", "sandbox.wsl.installRetry": "Reintentar", diff --git a/packages/app/src/sandbox/image-field.tsx b/packages/app/src/sandbox/image-field.tsx index e9608885fd..1c9ede7af1 100644 --- a/packages/app/src/sandbox/image-field.tsx +++ b/packages/app/src/sandbox/image-field.tsx @@ -7,6 +7,14 @@ import { useLanguage } from "@/context/language" import { usePlatform } from "@/context/platform" import type { SandboxImageProblem } from "./types" +// Dev only: supplying your own image is how a sandbox build gets tried before +// it is published, which is not a choice a beta or release user has any way to +// make — the published image is the only one they can get. Gated here rather +// than at the two call sites so neither dialog can forget it; with the field +// hidden the path stays empty, which is the "download the published image" +// path both dialogs already take. +const CHANNEL = import.meta.env.VITE_CODEV_CHANNEL + // The optional custom-image field, shared by both create dialogs: the full one // in Settings and the one Work opens when there is no sandbox yet. It is the // only field the two have in common, and the only one with behaviour worth @@ -80,52 +88,53 @@ export function createSandboxImageField() { path: () => store.path || undefined, // An unchecked image is not an approved one. usable: () => !store.path || (store.checkedPath === store.path && !store.problem), - Field: (props: { disabled: boolean }) => ( - <> - {/* Optional: most people want the published image downloaded for them, + Field: (props: { disabled: boolean }) => + CHANNEL !== "dev" ? null : ( + <> + {/* Optional: most people want the published image downloaded for them, so the hint leads with that and explains what the field is for before offering the picker. */} -

{language.t("sandbox.add.imageHint")}

-
- setStore({ path: event.currentTarget.value, problem: null, checking: false })} - onChange={(event) => void check(event.currentTarget.value)} - /> - - {language.t("sandbox.add.choose")} - - - setStore({ path: "", problem: null, checking: false, checkedPath: null })} - > - {language.t("sandbox.add.imageClear")} +

{language.t("sandbox.add.imageHint")}

+
+ setStore({ path: event.currentTarget.value, problem: null, checking: false })} + onChange={(event) => void check(event.currentTarget.value)} + /> + + {language.t("sandbox.add.choose")} + + setStore({ path: "", problem: null, checking: false, checkedPath: null })} + > + {language.t("sandbox.add.imageClear")} + + +
+ +

{language.t("sandbox.add.imageChecking")}

-
- -

{language.t("sandbox.add.imageChecking")}

-
- {/* The only line here that reports a problem, so it is the only one + {/* The only line here that reports a problem, so it is the only one in the danger colour — "checking" and "this image can be used" sit beside it in the muted one. role="alert" matches how the other v2 dialogs announce an inline validation failure. */} - - {(text) => ( - - )} - - -

{language.t("sandbox.add.imageOk")}

-
- - ), + + {(text) => ( + + )} + + +

{language.t("sandbox.add.imageOk")}

+
+ + ), } } diff --git a/packages/codev-sandbox/src/policy.test.ts b/packages/codev-sandbox/src/policy.test.ts index c7a4997294..c94f723c6b 100644 --- a/packages/codev-sandbox/src/policy.test.ts +++ b/packages/codev-sandbox/src/policy.test.ts @@ -2,7 +2,6 @@ import { expect, test } from "bun:test" import { SANDBOX_CPUS_RANGE, SANDBOX_MEMORY_MIB_RANGE, sandboxResourcesValid } from "./provider" import { MACOS_UNSUPPORTED_MESSAGE, - WINDOWS_10_WARNING, WINDOWS_OLD_WARNING, linuxSandboxAvailability, macosSandboxAvailability, @@ -25,7 +24,7 @@ test("allows the sandbox on macOS 12 and later", () => { test("never blocks Windows but tiers the warnings", () => { expect(windowsSandboxAvailability(26100)).toEqual({ supported: true, warning: null }) expect(windowsSandboxAvailability(22000)).toEqual({ supported: true, warning: null }) - expect(windowsSandboxAvailability(19045)).toEqual({ supported: true, warning: WINDOWS_10_WARNING }) + expect(windowsSandboxAvailability(19045)).toEqual({ supported: true, warning: null }) expect(windowsSandboxAvailability(19044)).toEqual({ supported: true, warning: WINDOWS_OLD_WARNING }) expect(windowsSandboxAvailability(17763)).toEqual({ supported: true, warning: WINDOWS_OLD_WARNING }) expect(windowsSandboxAvailability(null)).toEqual({ supported: true, warning: WINDOWS_OLD_WARNING }) @@ -68,9 +67,9 @@ test("warns against the installer's own minimum build when the machine is below }) test("a machine above the installer's minimum keeps the tier it was already in", () => { - // Satisfying what WSL needs to install says nothing about the sandbox on - // Windows 10, so that warning stands. - expect(windowsSandboxAvailability(19045, 19041)).toEqual({ supported: true, warning: WINDOWS_10_WARNING }) + // Windows 10 22H2 has been run and works, so clearing the installer's floor + // there leaves nothing to say. + expect(windowsSandboxAvailability(19045, 19041)).toEqual({ supported: true, warning: null }) expect(windowsSandboxAvailability(22631, 19041)).toEqual({ supported: true, warning: null }) }) diff --git a/packages/codev-sandbox/src/policy.ts b/packages/codev-sandbox/src/policy.ts index 26369debee..427ca7126a 100644 --- a/packages/codev-sandbox/src/policy.ts +++ b/packages/codev-sandbox/src/policy.ts @@ -7,13 +7,12 @@ export const MACOS_MINIMUM_MAJOR = 12 // Windows is warn-never-block: old builds may still work, and a genuine // incompatibility surfaces as a clear install error rather than a preflight -// refusal. 22000 is the first Windows 11 build, 19045 is Windows 10 22H2. +// refusal. 22000 is the first Windows 11 build; 19045 is Windows 10 22H2, the +// oldest build the sandbox has been tried on, and it works there, so nothing +// between the two warns. export const WINDOWS_11_FIRST_BUILD = 22000 export const WINDOWS_10_22H2_BUILD = 19045 -export const WINDOWS_10_WARNING = - "Windows 10 support for the sandbox is not fully verified. Installation will proceed, but if it fails, consider upgrading to Windows 11." - export const WINDOWS_OLD_WARNING = "This Windows version is out of support and may not support WSL2 at all. Installation is likely to fail; upgrading Windows is strongly recommended." @@ -47,15 +46,14 @@ export function windowsSandboxAvailability( // incompatibility surfaces as a clear install error rather than a refusal. if (recommendedMinBuild && buildNumber < recommendedMinBuild) return { supported: true, warning: windowsInstallerBuildWarning(buildNumber, recommendedMinBuild) } - if (buildNumber >= WINDOWS_11_FIRST_BUILD) return { supported: true, warning: null } - // Kept even when the installer's own floor is satisfied: this one is about - // the sandbox on Windows 10, not about what WSL will install on. - if (buildNumber >= WINDOWS_10_22H2_BUILD) return { supported: true, warning: WINDOWS_10_WARNING } + // Windows 10 22H2 included: the sandbox has been run there, so it gets the + // same silence Windows 11 does. + if (buildNumber >= WINDOWS_10_22H2_BUILD) return { supported: true, warning: null } return { supported: true, warning: WINDOWS_OLD_WARNING } } // os.release() on Windows looks like "10.0.19045"; the third component is the -// build number that distinguishes Windows 10 tiers from Windows 11. +// build number the availability tiers are decided on. export function windowsBuildNumber(osRelease: string) { const build = Number.parseInt(osRelease.split(".")[2] ?? "", 10) return Number.isFinite(build) ? build : null diff --git a/packages/codev-sandbox/src/wsl/provider.test.ts b/packages/codev-sandbox/src/wsl/provider.test.ts index e43163f87b..ee7bed2cdc 100644 --- a/packages/codev-sandbox/src/wsl/provider.test.ts +++ b/packages/codev-sandbox/src/wsl/provider.test.ts @@ -137,12 +137,10 @@ test("availability refuses non-Windows hosts", async () => { await rm(dir, { recursive: true, force: true }) }) -test("availability warns on Windows 10 22H2 but stays supported", async () => { +test("availability is clean on Windows 10 22H2", async () => { const dir = await mkdtemp(join(tmpdir(), "wsl-test-")) const provider = createWslProvider(await preparedOptions(dir, { osRelease: () => "10.0.19045" })) - const availability = await provider.availability() - expect(availability.supported).toBe(true) - expect(availability.supported && availability.warning).toContain("Windows 10") + expect(await provider.availability()).toEqual({ supported: true, warning: null }) await rm(dir, { recursive: true, force: true }) }) @@ -191,14 +189,14 @@ test("availability survives an unreachable installer manifest", async () => { const dir = await mkdtemp(join(tmpdir(), "wsl-test-")) const provider = createWslProvider( await preparedOptions(dir, { - osRelease: () => "10.0.19045", + osRelease: () => "10.0.18363", resolveInstallerMinBuild: async () => { throw new Error("offline") }, }), ) const availability = await provider.availability() - expect(availability.supported && availability.warning).toContain("Windows 10") + expect(availability.supported && availability.warning).toContain("out of support") await rm(dir, { recursive: true, force: true }) })