Open the side panel without booting the computer

Showing the right side panel (computer preview and routines) used to boot a
stopped computer right away. A boot is slow and heavy on the host, and most
of the time the panel is opened for routines, not the screen. The panel now
only reconnects a computer that already runs; a resting computer shows its
Open action without hover, and Open still boots it. A stopped computer is no
longer treated as needing maintenance.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TafdPTJdWxTYt9SYbDuKQ8
This commit is contained in:
Claude 2026-10-05 10:57:11 +00:00
parent 738811dbe7
commit f59e130059
No known key found for this signature in database
5 changed files with 40 additions and 55 deletions

View File

@ -1,5 +1,5 @@
import { expect, test } from "@playwright/test"; 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) => { test("teach a task records interaction and saves a draft", async ({ page }, testInfo) => {
const stamp = Date.now(); 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.getByRole("button", { name: "Take control" })).toHaveCount(0);
await expect(sidePanel.getByTestId("computer-more-button")).toHaveCount(0); await expect(sidePanel.getByTestId("computer-more-button")).toHaveCount(0);
await expect(sidePanel.getByTestId("computer-preview")).toBeVisible(); 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"); await captureScreenshot(page, testInfo, "teach-sidepanel-simple");
const preview = sidePanel.getByTestId("computer-preview"); const preview = sidePanel.getByTestId("computer-preview");
await preview.hover(); await preview.hover();
const openButton = sidePanel.getByTestId("computer-preview-open");
await expect(openButton).toBeVisible(); await expect(openButton).toBeVisible();
await expect(openButton.getByText("Open", { exact: true })).toBeVisible(); await expect(openButton.getByText("Open", { exact: true })).toBeVisible();
await captureScreenshot(page, testInfo, "teach-sidepanel-open-hover"); await captureScreenshot(page, testInfo, "teach-sidepanel-open-hover");

View File

@ -9,9 +9,8 @@ import {
activeThreadRuns, activeThreadRuns,
applyThreadSendReceipt, applyThreadSendReceipt,
clearActiveThreadRuns, clearActiveThreadRuns,
computerPanelAutoBoot,
computerPanelAutoUsesBoot,
computerPanelNeedsMaintenance, computerPanelNeedsMaintenance,
computerPanelReconnects,
computerTakeoverBlocked, computerTakeoverBlocked,
isThreadSnapshotEvent, isThreadSnapshotEvent,
mergeThreadSnapshot, mergeThreadSnapshot,
@ -1513,25 +1512,19 @@ describe("computer event reduction", () => {
}); });
}); });
it("auto-boots stopped computers and recovers a running screen that has no URL", () => { it("reconnects only a running screen that has no URL and never boots on open", () => {
expect(computerPanelAutoBoot("stopped")).toBe("boot"); expect(computerPanelReconnects("stopped")).toBe(false);
expect(computerPanelAutoBoot("error")).toBe("boot"); expect(computerPanelReconnects("error")).toBe(false);
expect(computerPanelAutoBoot(undefined)).toBe("boot"); expect(computerPanelReconnects(undefined)).toBe(false);
expect(computerPanelAutoBoot("running", "https://screen.example")).toBe("wait"); expect(computerPanelReconnects("booting")).toBe(false);
expect(computerPanelAutoBoot("running", null)).toBe("recover-screen"); expect(computerPanelReconnects("suspended")).toBe(false);
expect(computerPanelAutoBoot("booting")).toBe("wait"); expect(computerPanelReconnects("running", "https://screen.example")).toBe(false);
expect(computerPanelAutoBoot("suspended")).toBe("wait"); expect(computerPanelReconnects("running", null)).toBe(true);
}); });
it("maps recover-screen to computer.boot, not computer.recover", () => { it("shows maintenance only for an errored computer that is not booting", () => {
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", () => {
expect(computerPanelNeedsMaintenance("error", false)).toBe(true); 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("error", true)).toBe(false);
expect(computerPanelNeedsMaintenance("running", false)).toBe(false); expect(computerPanelNeedsMaintenance("running", false)).toBe(false);
expect(computerPanelNeedsMaintenance(undefined, false)).toBe(false); expect(computerPanelNeedsMaintenance(undefined, false)).toBe(false);
@ -1541,7 +1534,7 @@ describe("computer event reduction", () => {
const panel = "computer"; const panel = "computer";
const booting = false; const booting = false;
const showInSidePanel = (computerOpen: boolean) => const showInSidePanel = (computerOpen: boolean) =>
panel === "computer" && !computerOpen && computerPanelNeedsMaintenance("stopped", booting); panel === "computer" && !computerOpen && computerPanelNeedsMaintenance("error", booting);
expect(showInSidePanel(false)).toBe(true); expect(showInSidePanel(false)).toBe(true);
expect(showInSidePanel(true)).toBe(false); expect(showInSidePanel(true)).toBe(false);

View File

@ -539,27 +539,23 @@ export function computerTakeoverBlocked(
return isActive(runStatus as RunStatus); 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, state: ComputerStatus["state"] | undefined,
screenUrl?: string | null, 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<typeof computerPanelAutoBoot>,
): boolean { ): boolean {
return action === "boot" || action === "recover-screen"; return state === "running" && !screenUrl;
} }
export function computerPanelNeedsMaintenance( export function computerPanelNeedsMaintenance(
state: ComputerStatus["state"] | undefined, state: ComputerStatus["state"] | undefined,
booting: boolean, booting: boolean,
): boolean { ): boolean {
return !booting && (state === "error" || state === "stopped"); return !booting && state === "error";
} }
export function reduceComputerStatus( export function reduceComputerStatus(

View File

@ -124,8 +124,7 @@ import {
activeThreadRuns, activeThreadRuns,
applyThreadSendReceipt, applyThreadSendReceipt,
clearActiveThreadRuns, clearActiveThreadRuns,
computerPanelAutoBoot, computerPanelReconnects,
computerPanelAutoUsesBoot,
computerTakeoverBlocked, computerTakeoverBlocked,
isComputerStatusEvent, isComputerStatusEvent,
prependThreadMessagePage, prependThreadMessagePage,
@ -551,7 +550,6 @@ export function ShellPage() {
outputTokens: number; outputTokens: number;
runs: number; runs: number;
} | null>(null); } | null>(null);
const autoBooted = useRef<string | null>(null);
const routineSavePending = useRef(false); const routineSavePending = useRef(false);
const webhookSecretProvisionRef = useRef(new Map<string, Promise<string>>()); const webhookSecretProvisionRef = useRef(new Map<string, Promise<string>>());
const ensureWebhookSecret = (botId: string) => const ensureWebhookSecret = (botId: string) =>
@ -2503,11 +2501,7 @@ export function ShellPage() {
} }
useEffect(() => { useEffect(() => {
if (panel !== "computer") { if (panel !== "computer" || !active) return;
autoBooted.current = null;
return;
}
if (!active) return;
const botId = active.id; const botId = active.id;
let cancelled = false; let cancelled = false;
void (async () => { void (async () => {
@ -2518,19 +2512,10 @@ export function ShellPage() {
const state = snap?.computer?.state; const state = snap?.computer?.state;
const screen = state === "running" ? await refreshComputerScreen(botId) : null; const screen = state === "running" ? await refreshComputerScreen(botId) : null;
if (cancelled || activeBotId.current !== botId) return; if (cancelled || activeBotId.current !== botId) return;
const action = computerPanelAutoBoot(state, screen); if (!computerPanelReconnects(state, screen)) return;
if (action === "wait") { await bootComputer({ takeControl: false, overlay: false, force: true }).catch(
if (state === "running") autoBooted.current = botId; () => undefined,
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);
})(); })();
return () => { return () => {
cancelled = true; cancelled = true;

View File

@ -35,6 +35,12 @@ export function ComputerPanelPreview({
onOpen: () => void; onOpen: () => void;
}) { }) {
const { t } = useLingui(); 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 ( return (
<> <>
<div <div
@ -77,7 +83,8 @@ export function ComputerPanelPreview({
<button <button
type="button" type="button"
data-testid="computer-preview-open" data-testid="computer-preview-open"
className="absolute inset-0 flex cursor-pointer items-center justify-center bg-overlay/40 opacity-0 transition-opacity group-hover:opacity-100 focus-visible:opacity-100" data-resting={resting ? "" : undefined}
className="absolute inset-0 flex cursor-pointer items-center justify-center opacity-0 transition-opacity group-hover:bg-overlay/40 group-hover:opacity-100 focus-visible:bg-overlay/40 focus-visible:opacity-100 data-resting:items-end data-resting:pb-4 data-resting:opacity-100"
aria-label={t`Open`} aria-label={t`Open`}
onClick={onOpen} onClick={onOpen}
> >