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 { 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");
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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<typeof computerPanelAutoBoot>,
|
||||
): 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(
|
||||
|
||||
@ -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<string | null>(null);
|
||||
const routineSavePending = useRef(false);
|
||||
const webhookSecretProvisionRef = useRef(new Map<string, Promise<string>>());
|
||||
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;
|
||||
|
||||
@ -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 (
|
||||
<>
|
||||
<div
|
||||
@ -77,7 +83,8 @@ export function ComputerPanelPreview({
|
||||
<button
|
||||
type="button"
|
||||
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`}
|
||||
onClick={onOpen}
|
||||
>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user