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
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/i18n/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
89 changes: 49 additions & 40 deletions packages/app/src/sandbox/image-field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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. */}
<p class="text-12 text-v2-text-text-muted">{language.t("sandbox.add.imageHint")}</p>
<div class="flex items-center gap-2">
<TextInputV2
class="flex-1"
appearance="large"
placeholder={language.t("sandbox.add.imagePlaceholder")}
value={store.path}
disabled={props.disabled}
onInput={(event) => setStore({ path: event.currentTarget.value, problem: null, checking: false })}
onChange={(event) => void check(event.currentTarget.value)}
/>
<ButtonV2 variant="neutral" disabled={props.disabled} onClick={pick}>
{language.t("sandbox.add.choose")}
</ButtonV2>
<Show when={store.path && !props.disabled}>
<ButtonV2
variant="neutral"
onClick={() => setStore({ path: "", problem: null, checking: false, checkedPath: null })}
>
{language.t("sandbox.add.imageClear")}
<p class="text-12 text-v2-text-text-muted">{language.t("sandbox.add.imageHint")}</p>
<div class="flex items-center gap-2">
<TextInputV2
class="flex-1"
appearance="large"
placeholder={language.t("sandbox.add.imagePlaceholder")}
value={store.path}
disabled={props.disabled}
onInput={(event) => setStore({ path: event.currentTarget.value, problem: null, checking: false })}
onChange={(event) => void check(event.currentTarget.value)}
/>
<ButtonV2 variant="neutral" disabled={props.disabled} onClick={pick}>
{language.t("sandbox.add.choose")}
</ButtonV2>
<Show when={store.path && !props.disabled}>
<ButtonV2
variant="neutral"
onClick={() => setStore({ path: "", problem: null, checking: false, checkedPath: null })}
>
{language.t("sandbox.add.imageClear")}
</ButtonV2>
</Show>
</div>
<Show when={store.checking}>
<p class="text-12 text-v2-text-text-muted">{language.t("sandbox.add.imageChecking")}</p>
</Show>
</div>
<Show when={store.checking}>
<p class="text-12 text-v2-text-text-muted">{language.t("sandbox.add.imageChecking")}</p>
</Show>
{/* 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. */}
<Show when={message()}>
{(text) => (
<p role="alert" class="text-12 text-v2-state-fg-danger">
{text()}
</p>
)}
</Show>
<Show when={store.checkedPath === store.path && store.path && !store.problem}>
<p class="text-12 text-v2-text-text-muted">{language.t("sandbox.add.imageOk")}</p>
</Show>
</>
),
<Show when={message()}>
{(text) => (
<p role="alert" class="text-12 text-v2-state-fg-danger">
{text()}
</p>
)}
</Show>
<Show when={store.checkedPath === store.path && store.path && !store.problem}>
<p class="text-12 text-v2-text-text-muted">{language.t("sandbox.add.imageOk")}</p>
</Show>
</>
),
}
}
9 changes: 4 additions & 5 deletions packages/codev-sandbox/src/policy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 })
Expand Down Expand Up @@ -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 })
})

Expand Down
16 changes: 7 additions & 9 deletions packages/codev-sandbox/src/policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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."

Expand Down Expand Up @@ -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
Expand Down
10 changes: 4 additions & 6 deletions packages/codev-sandbox/src/wsl/provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
})

Expand Down Expand Up @@ -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 })
})

Expand Down
Loading