Open the side panel without booting the computer (#162)
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. Claude-Session: https://claude.ai/code/session_01TafdPTJdWxTYt9SYbDuKQ8 Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
parent
568d37c48c
commit
e55207f5d2
@ -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");
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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}
|
||||||
>
|
>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user