diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index cb7aff19f..45c072e9a 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -30,6 +30,23 @@ Auto-detection can exist as a convenience, but should be tiered and explainable: The readiness check should be a clear, single command (e.g. `docker info`) and the UI should show the exact error output when it fails. +### Example: MicroSandbox-backed sandboxes (desktop) + +When enabling MicroSandbox-backed sandbox mode, prefer an explicit override for the `msb` binary: + +- `OPENWORK_MICROSANDBOX_BIN` (absolute path to `msb`) + +This keeps the desktop app predictable on systems where GUI PATH resolution differs from shell PATH. + +Default expectations: + +1. Honor `OPENWORK_MICROSANDBOX_BIN` if set. +2. Try the process PATH. +3. On macOS, try the login PATH from `/usr/libexec/path_helper`. +4. Fall back to well-known locations like `~/.microsandbox/bin/msb`. + +The readiness check should be a clear, single command (for example `msb ls --format json`) and the UI should show the exact error output when it fails. On macOS, MicroSandbox should be treated as Apple Silicon-only. On Linux, it should be treated as requiring hardware virtualization (KVM). + ## Minimal use of Tauri We move most of the functionality to the openwork server which interfaces mostly with FS and proxies to opencode. diff --git a/apps/app/src/app/app.tsx b/apps/app/src/app/app.tsx index 8b352ff35..4e79a2943 100644 --- a/apps/app/src/app/app.tsx +++ b/apps/app/src/app/app.tsx @@ -2463,6 +2463,13 @@ export default function App() { ? bundlesStore.handleCreateSandboxConfirm : undefined } + onConfirmExtraWorker={ + isTauriRuntime() + ? bundlesStore.handleCreateMicrosandboxConfirm + : undefined + } + workerLabel="Create Docker sandbox" + extraWorkerLabel="Create MicroSandbox" workerDisabled={(() => { if (!isTauriRuntime()) return true; if (workspaceStore.sandboxDoctorBusy?.()) return true; @@ -2516,7 +2523,15 @@ export default function App() { onWorkerRetry={() => { void workspaceStore.refreshSandboxDoctor?.(); }} - workerSubmitting={workspaceStore.sandboxPreflightBusy?.() ?? false} + workerSubmitting={ + (workspaceStore.sandboxPreflightBusy?.() ?? false) && + workspaceStore.sandboxActiveBackend?.() !== "microsandbox" + } + extraWorkerSubmitting={ + ((workspaceStore.sandboxPreflightBusy?.() ?? false) || + (workspaceStore.sandboxCreatePhase?.() ?? "idle") !== "idle") && + workspaceStore.sandboxActiveBackend?.() === "microsandbox" + } localDisabled={!isTauriRuntime()} localDisabledReason={ !isTauriRuntime() diff --git a/apps/app/src/app/bundles/skill-destination-modal.tsx b/apps/app/src/app/bundles/skill-destination-modal.tsx index c63e1caf6..59b3c27ae 100644 --- a/apps/app/src/app/bundles/skill-destination-modal.tsx +++ b/apps/app/src/app/bundles/skill-destination-modal.tsx @@ -51,7 +51,8 @@ export default function SkillDestinationModal(props: { const workspaceBadge = (workspace: WorkspaceInfo) => { if ( workspace.workspaceType === "remote" && - (workspace.sandboxBackend === "docker" || + ((workspace.sandboxBackend === "docker" || + workspace.sandboxBackend === "microsandbox") || Boolean(workspace.sandboxRunId?.trim()) || Boolean(workspace.sandboxContainerName?.trim())) ) { @@ -80,7 +81,8 @@ export default function SkillDestinationModal(props: { const isSandboxWorkspace = (workspace: WorkspaceInfo) => workspace.workspaceType === "remote" && - (workspace.sandboxBackend === "docker" || + ((workspace.sandboxBackend === "docker" || + workspace.sandboxBackend === "microsandbox") || Boolean(workspace.sandboxRunId?.trim()) || Boolean(workspace.sandboxContainerName?.trim())); diff --git a/apps/app/src/app/bundles/store.ts b/apps/app/src/app/bundles/store.ts index 4799f5d8b..24277baa6 100644 --- a/apps/app/src/app/bundles/store.ts +++ b/apps/app/src/app/bundles/store.ts @@ -662,6 +662,7 @@ export function createBundlesStore(options: { const badge = workspace.workspaceType === "remote" ? workspace.sandboxBackend === "docker" || + workspace.sandboxBackend === "microsandbox" || Boolean(workspace.sandboxRunId?.trim()) || Boolean(workspace.sandboxContainerName?.trim()) ? "Sandbox" @@ -805,9 +806,14 @@ export function createBundlesStore(options: { } }; - const handleCreateSandboxConfirm = async (preset: WorkspacePreset, folder: string | null) => { + const handleCreateManagedSandboxConfirm = async ( + backend: "docker" | "microsandbox", + preset: WorkspacePreset, + folder: string | null, + ) => { const request = createWorkspaceRequest(); const ok = await options.workspaceStore.createSandboxFlow( + backend, preset, folder, request @@ -843,6 +849,16 @@ export function createBundlesStore(options: { } }; + const handleCreateSandboxConfirm = async ( + preset: WorkspacePreset, + folder: string | null, + ) => handleCreateManagedSandboxConfirm("docker", preset, folder); + + const handleCreateMicrosandboxConfirm = async ( + preset: WorkspacePreset, + folder: string | null, + ) => handleCreateManagedSandboxConfirm("microsandbox", preset, folder); + return { queueBundleLink, openDebugBundleRequest, @@ -866,6 +882,7 @@ export function createBundlesStore(options: { openRemoteConnectFromSkillDestination, handleCreateWorkspaceConfirm, handleCreateSandboxConfirm, + handleCreateMicrosandboxConfirm, dismissUntrustedBundleWarning, confirmUntrustedBundleWarning, bundleImportChoice, diff --git a/apps/app/src/app/components/session/sidebar.tsx b/apps/app/src/app/components/session/sidebar.tsx index 639cc22b5..412442b67 100644 --- a/apps/app/src/app/components/session/sidebar.tsx +++ b/apps/app/src/app/components/session/sidebar.tsx @@ -292,7 +292,8 @@ export default function SessionSidebar(props: SidebarProps) { const detailLabel = () => workspaceDetailLabel(group.workspace); const isSandboxWorkspace = () => group.workspace.workspaceType === "remote" && - (group.workspace.sandboxBackend === "docker" || + ((group.workspace.sandboxBackend === "docker" || + group.workspace.sandboxBackend === "microsandbox") || Boolean(group.workspace.sandboxRunId?.trim()) || Boolean(group.workspace.sandboxContainerName?.trim())); const sessions = () => group.sessions; diff --git a/apps/app/src/app/components/session/workspace-session-list.tsx b/apps/app/src/app/components/session/workspace-session-list.tsx index 520e10c31..12032a1b0 100644 --- a/apps/app/src/app/components/session/workspace-session-list.tsx +++ b/apps/app/src/app/components/session/workspace-session-list.tsx @@ -165,6 +165,7 @@ const workspaceLabel = (workspace: WorkspaceInfo) => const workspaceKindLabel = (workspace: WorkspaceInfo) => workspace.workspaceType === "remote" ? workspace.sandboxBackend === "docker" || + workspace.sandboxBackend === "microsandbox" || Boolean(workspace.sandboxRunId?.trim()) || Boolean(workspace.sandboxContainerName?.trim()) ? "Sandbox" diff --git a/apps/app/src/app/components/workspace-chip.tsx b/apps/app/src/app/components/workspace-chip.tsx index 0856fa87c..82d75e576 100644 --- a/apps/app/src/app/components/workspace-chip.tsx +++ b/apps/app/src/app/components/workspace-chip.tsx @@ -24,7 +24,8 @@ export default function WorkspaceChip(props: { : props.workspace.path; const isSandboxWorkspace = props.workspace.workspaceType === "remote" && - (props.workspace.sandboxBackend === "docker" || + ((props.workspace.sandboxBackend === "docker" || + props.workspace.sandboxBackend === "microsandbox") || Boolean(props.workspace.sandboxRunId?.trim()) || Boolean(props.workspace.sandboxContainerName?.trim())); const translate = (key: string) => t(key, currentLocale()); diff --git a/apps/app/src/app/context/workspace.ts b/apps/app/src/app/context/workspace.ts index aeebde2d3..193f7b63f 100644 --- a/apps/app/src/app/context/workspace.ts +++ b/apps/app/src/app/context/workspace.ts @@ -83,14 +83,18 @@ export type WorkspaceDebugEvent = { export type SandboxCreateProgressStepStatus = "pending" | "active" | "done" | "error"; +export type LocalSandboxBackend = "docker" | "microsandbox"; + export type SandboxCreateProgressStep = { - key: "docker" | "workspace" | "sandbox" | "health" | "connect"; + key: "runtime" | "workspace" | "sandbox" | "health" | "connect"; label: string; status: SandboxCreateProgressStepStatus; detail?: string | null; }; export type SandboxCreateProgressState = { + backend: LocalSandboxBackend; + backendLabel: string; runId: string; startedAt: number; stage: string; @@ -269,6 +273,7 @@ export function createWorkspaceStore(options: { const [sandboxDoctorBusy, setSandboxDoctorBusy] = createSignal(false); const [sandboxPreflightBusy, setSandboxPreflightBusy] = createSignal(false); const [sandboxCreatePhase, setSandboxCreatePhase] = createSignal("idle"); + const [sandboxActiveBackend, setSandboxActiveBackend] = createSignal(null); const [sandboxCreateProgress, setSandboxCreateProgress] = createSignal(null); const [lastSandboxCreateProgress, setLastSandboxCreateProgress] = @@ -315,6 +320,17 @@ export function createWorkspaceStore(options: { setSandboxCreateProgress((prev) => (prev ? { ...prev, error: value } : prev)); }; + const sandboxBackendLabel = (backend: LocalSandboxBackend) => + backend === "microsandbox" ? "MicroSandbox" : "Docker"; + + const sandboxReadinessLabel = (backend: LocalSandboxBackend) => + `${sandboxBackendLabel(backend)} ready`; + + const sandboxUnavailableMessage = (backend: LocalSandboxBackend) => + backend === "microsandbox" + ? "MicroSandbox is required for this worker. Install it and try again." + : "Docker is required for sandboxes. Install Docker Desktop, start it, then retry."; + const makeRunId = () => { if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") { return crypto.randomUUID(); @@ -1315,17 +1331,21 @@ export function createWorkspaceStore(options: { } } - async function refreshSandboxDoctor() { + async function refreshManagedSandboxDoctor(backend: LocalSandboxBackend) { if (!isTauriRuntime()) { - setSandboxDoctorResult(null); - setSandboxDoctorCheckedAt(Date.now()); + if (backend === "docker") { + setSandboxDoctorResult(null); + setSandboxDoctorCheckedAt(Date.now()); + } return null; } if (sandboxDoctorBusy()) return sandboxDoctorResult(); setSandboxDoctorBusy(true); try { - const result = await sandboxDoctor(); - setSandboxDoctorResult(result); + const result = await sandboxDoctor(backend); + if (backend === "docker") { + setSandboxDoctorResult(result); + } return result; } catch (e) { const message = e instanceof Error ? e.message : safeStringify(e); @@ -1336,14 +1356,22 @@ export function createWorkspaceStore(options: { ready: false, error: message, }; - setSandboxDoctorResult(fallback); + if (backend === "docker") { + setSandboxDoctorResult(fallback); + } return fallback; } finally { - setSandboxDoctorCheckedAt(Date.now()); + if (backend === "docker") { + setSandboxDoctorCheckedAt(Date.now()); + } setSandboxDoctorBusy(false); } } + async function refreshSandboxDoctor() { + return await refreshManagedSandboxDoctor("docker"); + } + async function activateWorkspace( workspaceId: string, hint?: { prevProjectDir?: string }, @@ -2241,6 +2269,7 @@ export function createWorkspaceStore(options: { } async function createSandboxFlow( + backend: LocalSandboxBackend, preset: WorkspacePreset, folder: string | null, input?: { onReady?: () => Promise | void }, @@ -2257,22 +2286,31 @@ export function createWorkspaceStore(options: { const runId = makeRunId(); const startedAt = Date.now(); - setSandboxCreatePhase("preflight"); - setSandboxPreflightBusy(true); + const backendLabel = sandboxBackendLabel(backend); + const usesRuntimeDoctor = backend === "docker"; + setSandboxActiveBackend(backend); + setSandboxCreatePhase(usesRuntimeDoctor ? "preflight" : "provisioning"); + setSandboxPreflightBusy(usesRuntimeDoctor); options.setError(null); clearSandboxCreateProgress(); - const doctor = await refreshSandboxDoctor(); + const doctor = usesRuntimeDoctor + ? await refreshManagedSandboxDoctor(backend) + : null; setSandboxPreflightBusy(false); setSandboxCreatePhase("provisioning"); setSandboxCreateProgress({ + backend, + backendLabel, runId, startedAt, - stage: "Checking Docker...", + stage: usesRuntimeDoctor + ? `Checking ${backendLabel}...` + : `Preparing ${backendLabel} runtime...`, error: null, logs: [], steps: [ - { key: "docker", label: "Docker ready", status: "active", detail: null }, + { key: "runtime", label: sandboxReadinessLabel(backend), status: "active", detail: null }, { key: "workspace", label: "Prepare worker", status: "pending", detail: null }, { key: "sandbox", label: "Start sandbox services", status: "pending", detail: null }, { key: "health", label: "Wait for OpenWork", status: "pending", detail: null }, @@ -2283,36 +2321,43 @@ export function createWorkspaceStore(options: { if (doctor?.debug) { const selectedBin = doctor.debug.selectedBin?.trim(); if (selectedBin) { - pushSandboxCreateLog(`Docker binary: ${selectedBin}`); + pushSandboxCreateLog(`${backendLabel} binary: ${selectedBin}`); } const candidates = (doctor.debug.candidates ?? []).filter((item) => item?.trim()); if (candidates.length) { - pushSandboxCreateLog(`Docker candidates: ${candidates.join(", ")}`); + pushSandboxCreateLog(`${backendLabel} candidates: ${candidates.join(", ")}`); } const versionDebug = doctor.debug.versionCommand; if (versionDebug) { - pushSandboxCreateLog(`docker --version exit=${versionDebug.status}`); - if (versionDebug.stderr?.trim()) pushSandboxCreateLog(`docker --version stderr: ${versionDebug.stderr.trim()}`); + const versionLabel = backend === "microsandbox" ? "msb --version" : "docker --version"; + pushSandboxCreateLog(`${versionLabel} exit=${versionDebug.status}`); + if (versionDebug.stderr?.trim()) pushSandboxCreateLog(`${versionLabel} stderr: ${versionDebug.stderr.trim()}`); } const infoDebug = doctor.debug.infoCommand; if (infoDebug) { - pushSandboxCreateLog(`docker info exit=${infoDebug.status}`); - if (infoDebug.stderr?.trim()) pushSandboxCreateLog(`docker info stderr: ${infoDebug.stderr.trim()}`); + const infoLabel = backend === "microsandbox" ? "msb ls --format json" : "docker info"; + pushSandboxCreateLog(`${infoLabel} exit=${infoDebug.status}`); + if (infoDebug.stderr?.trim()) pushSandboxCreateLog(`${infoLabel} stderr: ${infoDebug.stderr.trim()}`); } } - if (!doctor?.ready) { - const detail = - doctor?.error?.trim() || - "Docker is required for sandboxes. Install Docker Desktop, start it, then retry."; + if (usesRuntimeDoctor && !doctor?.ready) { + const detail = doctor?.error?.trim() || sandboxUnavailableMessage(backend); options.setError(detail); - setSandboxStep("docker", { status: "error", detail }); + setSandboxStep("runtime", { status: "error", detail }); setSandboxError(detail); - setSandboxStage("Docker not ready"); + setSandboxStage(`${backendLabel} not ready`); setSandboxCreatePhase("idle"); return false; } - setSandboxStep("docker", { status: "done", detail: doctor.serverVersion ?? null }); - setSandboxStage("Preparing worker..."); + if (usesRuntimeDoctor) { + setSandboxStep("runtime", { + status: "done", + detail: doctor?.serverVersion ?? doctor?.clientVersion ?? null, + }); + setSandboxStage("Preparing worker..."); + } else { + pushSandboxCreateLog("MicroSandbox runtime is managed by OpenWork."); + } try { const resolvedFolder = await resolveWorkspacePath(folder); @@ -2391,20 +2436,40 @@ export function createWorkspaceStore(options: { } } - if (stage === "docker.config") { - const selected = String(payload.payload?.openworkDockerBin ?? "").trim(); + if (stage === "docker.config" || stage === "microsandbox.config") { + const selected = String( + stage === "microsandbox.config" + ? payload.payload?.openworkMicrosandboxBin + : payload.payload?.openworkDockerBin ?? "", + ).trim(); if (selected) { - pushSandboxCreateLog(`OPENWORK_DOCKER_BIN=${selected}`); + pushSandboxCreateLog( + stage === "microsandbox.config" + ? `OPENWORK_MICROSANDBOX_BIN=${selected}` + : `OPENWORK_DOCKER_BIN=${selected}`, + ); } - const resolved = String(payload.payload?.resolvedDockerBin ?? "").trim(); + const resolved = String( + stage === "microsandbox.config" + ? payload.payload?.resolvedMicrosandboxBin + : payload.payload?.resolvedDockerBin ?? "", + ).trim(); if (resolved) { - pushSandboxCreateLog(`Resolved docker: ${resolved}`); + pushSandboxCreateLog( + stage === "microsandbox.config" + ? `Resolved MicroSandbox: ${resolved}` + : `Resolved docker: ${resolved}`, + ); } const candidates = Array.isArray(payload.payload?.candidates) ? payload.payload.candidates.filter((item: unknown) => String(item ?? "").trim()) : []; if (candidates.length) { - pushSandboxCreateLog(`Docker probe paths: ${candidates.join(", ")}`); + pushSandboxCreateLog( + stage === "microsandbox.config" + ? `MicroSandbox probe paths: ${candidates.join(", ")}` + : `Docker probe paths: ${candidates.join(", ")}`, + ); } } @@ -2442,9 +2507,12 @@ export function createWorkspaceStore(options: { const host = await orchestratorStartDetached({ workspacePath: resolvedFolder, - sandboxBackend: "docker", + sandboxBackend: backend, runId, }); + if (!usesRuntimeDoctor) { + setSandboxStep("runtime", { status: "done", detail: "Managed by OpenWork" }); + } setSandboxStep("sandbox", { status: "done", detail: host.sandboxContainerName ?? null }); setSandboxStep("health", { status: "done" }); setSandboxStage("Connecting to sandbox..."); @@ -2458,7 +2526,7 @@ export function createWorkspaceStore(options: { openworkHostToken: host.hostToken, directory: resolvedFolder, displayName: name, - sandboxBackend: host.sandboxBackend ?? "docker", + sandboxBackend: host.sandboxBackend ?? backend, sandboxRunId: host.sandboxRunId ?? runId, sandboxContainerName: host.sandboxContainerName ?? null, manageBusy: false, @@ -2491,12 +2559,16 @@ export function createWorkspaceStore(options: { } catch (e) { const message = e instanceof Error ? e.message : safeStringify(e); options.setError(addOpencodeCacheHint(message)); + if (!usesRuntimeDoctor) { + setSandboxStep("runtime", { status: "error", detail: message }); + } setSandboxError(message); setSandboxStage("Sandbox failed"); return false; } finally { setSandboxPreflightBusy(false); setSandboxCreatePhase("idle"); + setSandboxActiveBackend(null); } } @@ -2511,7 +2583,7 @@ export function createWorkspaceStore(options: { closeModal?: boolean; // Sandbox lifecycle metadata (desktop-managed) - sandboxBackend?: "docker" | null; + sandboxBackend?: LocalSandboxBackend | null; sandboxRunId?: string | null; sandboxContainerName?: string | null; }) { @@ -2567,7 +2639,7 @@ export function createWorkspaceStore(options: { } catch (error) { // Sandbox workers can report healthy before listWorkspaces is fully ready. // Fall back to host-level OpenCode URL so the worker can still be registered. - if (input.sandboxBackend !== "docker") { + if (!input.sandboxBackend) { throw error; } wsDebug("sandbox:openwork-resolve-fallback:error", { @@ -2582,7 +2654,7 @@ export function createWorkspaceStore(options: { openworkWorkspace = resolved.workspace; resolvedHostUrl = resolved.hostUrl; resolvedAuth = resolved.auth; - } else if (input.sandboxBackend === "docker") { + } else if (input.sandboxBackend) { resolvedHostUrl = hostUrl; resolvedBaseUrl = `${hostUrl.replace(/\/+$/, "")}/opencode`; resolvedDirectory = directory || resolvedDirectory; @@ -2982,7 +3054,9 @@ export function createWorkspaceStore(options: { } const isSandboxWorkspace = - workspace.sandboxBackend === "docker" || Boolean(workspace.sandboxContainerName?.trim()); + workspace.sandboxBackend === "docker" || + workspace.sandboxBackend === "microsandbox" || + Boolean(workspace.sandboxContainerName?.trim()); if (!isSandboxWorkspace) { return Boolean(await reconnect()); @@ -3005,17 +3079,20 @@ export function createWorkspaceStore(options: { return false; } - const doctor = await refreshSandboxDoctor(); - if (!doctor?.ready) { - const detail = - doctor?.error?.trim() || - "Docker needs to be running before we can get this worker back online."; - throw new Error(detail); + const backend = workspace.sandboxBackend ?? "docker"; + if (backend === "docker") { + const backendLabel = sandboxBackendLabel(backend); + const doctor = await refreshManagedSandboxDoctor(backend); + if (!doctor?.ready) { + const detail = + doctor?.error?.trim() || `${backendLabel} needs to be running before we can get this worker back online.`; + throw new Error(detail); + } } const host = await orchestratorStartDetached({ workspacePath, - sandboxBackend: "docker", + sandboxBackend: backend, runId: workspace.sandboxRunId?.trim() || null, openworkToken: workspace.openworkClientToken?.trim() || @@ -3046,7 +3123,7 @@ export function createWorkspaceStore(options: { openworkHostToken: host.hostToken, openworkWorkspaceId: resolved.workspace.id, openworkWorkspaceName: resolved.workspace.name ?? workspace.openworkWorkspaceName ?? null, - sandboxBackend: host.sandboxBackend ?? "docker", + sandboxBackend: host.sandboxBackend ?? backend, sandboxRunId: host.sandboxRunId ?? workspace.sandboxRunId ?? null, sandboxContainerName: host.sandboxContainerName ?? workspace.sandboxContainerName ?? null, }); @@ -3086,6 +3163,7 @@ export function createWorkspaceStore(options: { const workspace = workspaces().find((entry) => entry.id === id) ?? null; const containerName = workspace?.sandboxContainerName?.trim() ?? ""; + const backend = workspace?.sandboxBackend === "microsandbox" ? "microsandbox" : "docker"; if (!containerName) { options.setError("Sandbox container name missing."); return; @@ -3097,7 +3175,7 @@ export function createWorkspaceStore(options: { options.setError(null); try { - const result = await sandboxStop(containerName); + const result = await sandboxStop(containerName, backend); if (!result.ok) { const details = [result.stderr?.trim(), result.stdout?.trim()] .filter(Boolean) @@ -4067,6 +4145,7 @@ export function createWorkspaceStore(options: { sandboxDoctorBusy, sandboxPreflightBusy, sandboxCreatePhase, + sandboxActiveBackend, projectDir, workspaces, selectedWorkspaceId, diff --git a/apps/app/src/app/lib/openwork-server.ts b/apps/app/src/app/lib/openwork-server.ts index 7ad57bf43..f6562ef74 100644 --- a/apps/app/src/app/lib/openwork-server.ts +++ b/apps/app/src/app/lib/openwork-server.ts @@ -15,7 +15,10 @@ export type OpenworkServerCapabilities = { mcp: { read: boolean; write: boolean }; commands: { read: boolean; write: boolean }; config: { read: boolean; write: boolean }; - sandbox?: { enabled: boolean; backend: "none" | "docker" | "container" }; + sandbox?: { + enabled: boolean; + backend: "none" | "docker" | "container" | "microsandbox"; + }; proxy?: { opencode: boolean; opencodeRouter: boolean }; toolProviders?: { browser?: { diff --git a/apps/app/src/app/lib/tauri.ts b/apps/app/src/app/lib/tauri.ts index fd82800cd..dec83e733 100644 --- a/apps/app/src/app/lib/tauri.ts +++ b/apps/app/src/app/lib/tauri.ts @@ -124,7 +124,7 @@ export type WorkspaceInfo = { openworkWorkspaceName?: string | null; // Sandbox lifecycle metadata (desktop-managed) - sandboxBackend?: "docker" | null; + sandboxBackend?: "docker" | "microsandbox" | null; sandboxRunId?: string | null; sandboxContainerName?: string | null; }; @@ -210,7 +210,7 @@ export async function workspaceCreateRemote(input: { openworkWorkspaceName?: string | null; // Sandbox lifecycle metadata (desktop-managed) - sandboxBackend?: "docker" | null; + sandboxBackend?: "docker" | "microsandbox" | null; sandboxRunId?: string | null; sandboxContainerName?: string | null; }): Promise { @@ -245,7 +245,7 @@ export async function workspaceUpdateRemote(input: { openworkWorkspaceName?: string | null; // Sandbox lifecycle metadata (desktop-managed) - sandboxBackend?: "docker" | null; + sandboxBackend?: "docker" | "microsandbox" | null; sandboxRunId?: string | null; sandboxContainerName?: string | null; }): Promise { @@ -441,14 +441,14 @@ export type OrchestratorDetachedHost = { ownerToken?: string | null; hostToken: string; port: number; - sandboxBackend?: "docker" | null; + sandboxBackend?: "docker" | "microsandbox" | null; sandboxRunId?: string | null; sandboxContainerName?: string | null; }; export async function orchestratorStartDetached(input: { workspacePath: string; - sandboxBackend?: "none" | "docker" | null; + sandboxBackend?: "none" | "docker" | "microsandbox" | null; runId?: string | null; openworkToken?: string | null; openworkHostToken?: string | null; @@ -486,12 +486,22 @@ export type SandboxDoctorResult = { } | null; }; -export async function sandboxDoctor(): Promise { - return invoke("sandbox_doctor"); +export async function sandboxDoctor( + sandboxBackend?: "docker" | "microsandbox" | null, +): Promise { + return invoke("sandbox_doctor", { + sandboxBackend: sandboxBackend ?? null, + }); } -export async function sandboxStop(containerName: string): Promise { - return invoke("sandbox_stop", { containerName }); +export async function sandboxStop( + containerName: string, + sandboxBackend?: "docker" | "microsandbox" | null, +): Promise { + return invoke("sandbox_stop", { + containerName, + sandboxBackend: sandboxBackend ?? null, + }); } export type OpenworkDockerCleanupResult = { diff --git a/apps/app/src/app/shell/settings-shell.tsx b/apps/app/src/app/shell/settings-shell.tsx index 07d802fc7..b9c076994 100644 --- a/apps/app/src/app/shell/settings-shell.tsx +++ b/apps/app/src/app/shell/settings-shell.tsx @@ -301,6 +301,7 @@ export default function SettingsShell(props: SettingsShellProps) { const workspaceKindLabel = (workspace: WorkspaceInfo) => workspace.workspaceType === "remote" ? workspace.sandboxBackend === "docker" || + workspace.sandboxBackend === "microsandbox" || Boolean(workspace.sandboxRunId?.trim()) || Boolean(workspace.sandboxContainerName?.trim()) ? "Sandbox" diff --git a/apps/app/src/app/utils/index.ts b/apps/app/src/app/utils/index.ts index 7ba440f60..d55bb8b3e 100644 --- a/apps/app/src/app/utils/index.ts +++ b/apps/app/src/app/utils/index.ts @@ -336,7 +336,8 @@ const SANDBOX_NETWORK_HINTS = [ export function isSandboxWorkspace(workspace: WorkspaceInfo) { return ( workspace.workspaceType === "remote" && - (workspace.sandboxBackend === "docker" || + ((workspace.sandboxBackend === "docker" || + workspace.sandboxBackend === "microsandbox") || Boolean(workspace.sandboxRunId?.trim()) || Boolean(workspace.sandboxContainerName?.trim())) ); @@ -369,7 +370,10 @@ export function getWorkspaceTaskLoadErrorDisplay(workspace: WorkspaceInfo, error }; } - const message = "Sandbox is offline. Start Docker Desktop, then test connection."; + const message = + workspace.sandboxBackend === "microsandbox" + ? "Sandbox is offline. Start MicroSandbox, then test connection." + : "Sandbox is offline. Start Docker Desktop, then test connection."; return { tone: "offline" as const, label: "Offline", diff --git a/apps/app/src/app/workspace/create-workspace-local-panel.tsx b/apps/app/src/app/workspace/create-workspace-local-panel.tsx index 2f733fcc2..3e536eb32 100644 --- a/apps/app/src/app/workspace/create-workspace-local-panel.tsx +++ b/apps/app/src/app/workspace/create-workspace-local-panel.tsx @@ -41,8 +41,11 @@ export default function CreateWorkspaceLocalPanel(props: { confirmLabel?: string; workerLabel?: string; onConfirmWorker?: (preset: WorkspacePreset, folder: string | null) => void; + extraWorkerLabel?: string; + onConfirmExtraWorker?: (preset: WorkspacePreset, folder: string | null) => void; preset: WorkspacePreset; workerSubmitting: boolean; + extraWorkerSubmitting: boolean; workerDisabled: boolean; workerDisabledReason: string | null; workerCtaLabel?: string; @@ -52,6 +55,8 @@ export default function CreateWorkspaceLocalPanel(props: { onWorkerRetry?: () => void; workerDebugLines: string[]; progress: { + backend?: "docker" | "microsandbox"; + backendLabel?: string; runId: string; startedAt: number; stage: string; @@ -176,7 +181,7 @@ export default function CreateWorkspaceLocalPanel(props: { }> - Sandbox setup + {progress().backendLabel || "Sandbox"} setup
{progress().stage}
{props.elapsedSeconds}s
@@ -283,6 +288,22 @@ export default function CreateWorkspaceLocalPanel(props: { + + +