diff --git a/apps/web/e2e/teach-task.spec.ts b/apps/web/e2e/teach-task.spec.ts index c4f456d..835d31c 100644 --- a/apps/web/e2e/teach-task.spec.ts +++ b/apps/web/e2e/teach-task.spec.ts @@ -1,5 +1,5 @@ import { expect, test } from "@playwright/test"; -import { captureScreenshot, completeOnboarding, signup } from "./helpers"; +import { activeBotId, captureScreenshot, completeOnboarding, rpc, signup } from "./helpers"; test("teach a task records interaction and saves a draft", async ({ page }, testInfo) => { const stamp = Date.now(); @@ -17,11 +17,15 @@ test("teach a task records interaction and saves a draft", async ({ page }, test await expect(sidePanel.getByRole("button", { name: "Take control" })).toHaveCount(0); await expect(sidePanel.getByTestId("computer-more-button")).toHaveCount(0); await expect(sidePanel.getByTestId("computer-preview")).toBeVisible(); + // Showing the panel never boots the computer; its Open action is visible without hover. + const openButton = sidePanel.getByTestId("computer-preview-open"); + await expect(openButton).toBeVisible(); + const botId = activeBotId(page); + expect((await rpc<{ state: string }>(page, "computer/status", { botId })).state).toBe("stopped"); await captureScreenshot(page, testInfo, "teach-sidepanel-simple"); const preview = sidePanel.getByTestId("computer-preview"); await preview.hover(); - const openButton = sidePanel.getByTestId("computer-preview-open"); await expect(openButton).toBeVisible(); await expect(openButton.getByText("Open", { exact: true })).toBeVisible(); await captureScreenshot(page, testInfo, "teach-sidepanel-open-hover"); diff --git a/apps/web/src/lib/thread-events.test.ts b/apps/web/src/lib/thread-events.test.ts index ab0da23..22c3b8d 100644 --- a/apps/web/src/lib/thread-events.test.ts +++ b/apps/web/src/lib/thread-events.test.ts @@ -9,9 +9,8 @@ import { activeThreadRuns, applyThreadSendReceipt, clearActiveThreadRuns, - computerPanelAutoBoot, - computerPanelAutoUsesBoot, computerPanelNeedsMaintenance, + computerPanelReconnects, computerTakeoverBlocked, isThreadSnapshotEvent, mergeThreadSnapshot, @@ -1513,25 +1512,19 @@ describe("computer event reduction", () => { }); }); - it("auto-boots stopped computers and recovers a running screen that has no URL", () => { - expect(computerPanelAutoBoot("stopped")).toBe("boot"); - expect(computerPanelAutoBoot("error")).toBe("boot"); - expect(computerPanelAutoBoot(undefined)).toBe("boot"); - expect(computerPanelAutoBoot("running", "https://screen.example")).toBe("wait"); - expect(computerPanelAutoBoot("running", null)).toBe("recover-screen"); - expect(computerPanelAutoBoot("booting")).toBe("wait"); - expect(computerPanelAutoBoot("suspended")).toBe("wait"); + it("reconnects only a running screen that has no URL and never boots on open", () => { + expect(computerPanelReconnects("stopped")).toBe(false); + expect(computerPanelReconnects("error")).toBe(false); + expect(computerPanelReconnects(undefined)).toBe(false); + expect(computerPanelReconnects("booting")).toBe(false); + expect(computerPanelReconnects("suspended")).toBe(false); + expect(computerPanelReconnects("running", "https://screen.example")).toBe(false); + expect(computerPanelReconnects("running", null)).toBe(true); }); - it("maps recover-screen to computer.boot, not computer.recover", () => { - expect(computerPanelAutoUsesBoot("recover-screen")).toBe(true); - expect(computerPanelAutoUsesBoot("boot")).toBe(true); - expect(computerPanelAutoUsesBoot("wait")).toBe(false); - }); - - it("shows maintenance only after a stopped or errored computer finishes booting", () => { + it("shows maintenance only for an errored computer that is not booting", () => { expect(computerPanelNeedsMaintenance("error", false)).toBe(true); - expect(computerPanelNeedsMaintenance("stopped", false)).toBe(true); + expect(computerPanelNeedsMaintenance("stopped", false)).toBe(false); expect(computerPanelNeedsMaintenance("error", true)).toBe(false); expect(computerPanelNeedsMaintenance("running", false)).toBe(false); expect(computerPanelNeedsMaintenance(undefined, false)).toBe(false); @@ -1541,7 +1534,7 @@ describe("computer event reduction", () => { const panel = "computer"; const booting = false; const showInSidePanel = (computerOpen: boolean) => - panel === "computer" && !computerOpen && computerPanelNeedsMaintenance("stopped", booting); + panel === "computer" && !computerOpen && computerPanelNeedsMaintenance("error", booting); expect(showInSidePanel(false)).toBe(true); expect(showInSidePanel(true)).toBe(false); diff --git a/apps/web/src/lib/thread-events.ts b/apps/web/src/lib/thread-events.ts index 705e910..82f1775 100644 --- a/apps/web/src/lib/thread-events.ts +++ b/apps/web/src/lib/thread-events.ts @@ -539,27 +539,23 @@ export function computerTakeoverBlocked( return isActive(runStatus as RunStatus); } -export function computerPanelAutoBoot( +/** + * Opening the side panel only reconnects a computer that already runs. Starting a stopped one + * is the user's choice (Open), because a boot is slow and heavy on the host. + * The reconnect must use computer.boot — never computer.recover (that destroys the sandbox). + */ +export function computerPanelReconnects( state: ComputerStatus["state"] | undefined, screenUrl?: string | null, -): "boot" | "recover-screen" | "wait" { - if (state === "booting" || state === "suspended") return "wait"; - if (state === "running") return screenUrl ? "wait" : "recover-screen"; - return "boot"; -} - -/** Auto panel reconnect must use computer.boot — never computer.recover (that destroys the sandbox). */ -export function computerPanelAutoUsesBoot( - action: ReturnType, ): boolean { - return action === "boot" || action === "recover-screen"; + return state === "running" && !screenUrl; } export function computerPanelNeedsMaintenance( state: ComputerStatus["state"] | undefined, booting: boolean, ): boolean { - return !booting && (state === "error" || state === "stopped"); + return !booting && state === "error"; } export function reduceComputerStatus( diff --git a/apps/web/src/pages/Shell.tsx b/apps/web/src/pages/Shell.tsx index 1fcfc84..765575c 100644 --- a/apps/web/src/pages/Shell.tsx +++ b/apps/web/src/pages/Shell.tsx @@ -124,8 +124,7 @@ import { activeThreadRuns, applyThreadSendReceipt, clearActiveThreadRuns, - computerPanelAutoBoot, - computerPanelAutoUsesBoot, + computerPanelReconnects, computerTakeoverBlocked, isComputerStatusEvent, prependThreadMessagePage, @@ -551,7 +550,6 @@ export function ShellPage() { outputTokens: number; runs: number; } | null>(null); - const autoBooted = useRef(null); const routineSavePending = useRef(false); const webhookSecretProvisionRef = useRef(new Map>()); const ensureWebhookSecret = (botId: string) => @@ -2503,11 +2501,7 @@ export function ShellPage() { } useEffect(() => { - if (panel !== "computer") { - autoBooted.current = null; - return; - } - if (!active) return; + if (panel !== "computer" || !active) return; const botId = active.id; let cancelled = false; void (async () => { @@ -2518,19 +2512,10 @@ export function ShellPage() { const state = snap?.computer?.state; const screen = state === "running" ? await refreshComputerScreen(botId) : null; if (cancelled || activeBotId.current !== botId) return; - const action = computerPanelAutoBoot(state, screen); - if (action === "wait") { - if (state === "running") autoBooted.current = botId; - return; - } - if (action === "boot" && autoBooted.current === botId) return; - autoBooted.current = botId; - if (!computerPanelAutoUsesBoot(action)) return; - await bootComputer({ - takeControl: false, - overlay: action === "boot", - force: true, - }).catch(() => undefined); + if (!computerPanelReconnects(state, screen)) return; + await bootComputer({ takeControl: false, overlay: false, force: true }).catch( + () => undefined, + ); })(); return () => { cancelled = true; diff --git a/apps/web/src/pages/shell/computer-panel-preview.tsx b/apps/web/src/pages/shell/computer-panel-preview.tsx index 93f0da1..c591690 100644 --- a/apps/web/src/pages/shell/computer-panel-preview.tsx +++ b/apps/web/src/pages/shell/computer-panel-preview.tsx @@ -35,6 +35,12 @@ export function ComputerPanelPreview({ onOpen: () => void; }) { const { t } = useLingui(); + // Opening the panel never starts the computer, so a resting one shows its Open action outright. + const resting = + !booting && + computer?.state !== "running" && + computer?.state !== "booting" && + !computersAreUnavailable(sandboxProvider); return ( <>