fix: open the screen beside the chat even before the computer status catches up
A bot that boots its computer inside a run emits no computer.status event, so the client still saw a stopped computer and never opened the screen for screen work or a takeover. Pointer activity and takeover requests now count as proof of a running screen; desktop hosts and bots without a computer still never get one. A presented file is now the newest one since the user last wrote, so another member's run in a group no longer hides it. Only panes the work opened get the wide width, so switching from the computer to routines or settings no longer animates the width. E2E specs follow the new behaviour: the takeover screen opens on its own and group Markdown previews open beside the chat. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C7VSRHHzyt2KwoCkUkZ2Gc
This commit is contained in:
parent
065b1322bb
commit
cb3d65be2a
@ -69,9 +69,10 @@ test("takeover, routine, plugins, and export are reachable", async ({ page }, te
|
||||
})
|
||||
.toBe("waiting_takeover");
|
||||
await captureScreenshot(page, testInfo, "08-protected-input-request");
|
||||
await page.getByTitle("Agent computer").click();
|
||||
// A takeover request brings the bot's screen beside the chat on its own.
|
||||
const sidePanel = page.getByTestId("side-panel");
|
||||
await expect(sidePanel).toHaveCSS("width", "384px");
|
||||
await expect(sidePanel).toHaveAttribute("data-panel", "computer");
|
||||
await expect(sidePanel).toHaveAttribute("data-auto", "");
|
||||
const [mainBox, panelBox] = await Promise.all([
|
||||
page.locator("main").boundingBox(),
|
||||
sidePanel.boundingBox(),
|
||||
|
||||
@ -303,9 +303,11 @@ test("create group from + and see two bots in one transcript", async ({ page },
|
||||
const groupMarkdown = page.getByRole("button", { name: "Preview group-preview.md" });
|
||||
await expect(groupMarkdown).toBeVisible({ timeout: 30_000 });
|
||||
await groupMarkdown.click();
|
||||
const markdownDialog = page.getByRole("dialog", { name: "group-preview.md" });
|
||||
await expect(markdownDialog.getByRole("heading", { name: "Group artifact" })).toBeVisible();
|
||||
await markdownDialog.getByRole("button", { name: "Close preview" }).click();
|
||||
// Wide windows open the file beside the group conversation.
|
||||
const markdownPane = page.getByTestId("artifact-pane");
|
||||
await expect(markdownPane.getByRole("heading", { name: "Group artifact" })).toBeVisible();
|
||||
await markdownPane.getByRole("button", { name: "Close panel" }).click();
|
||||
await expect(markdownPane).toHaveCount(0);
|
||||
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await expect(page.getByRole("button", { name: "Open navigation" })).toBeVisible();
|
||||
|
||||
@ -3370,7 +3370,9 @@ export function ShellPage() {
|
||||
}
|
||||
|
||||
const embeddedScreenUrl = embeddableScreenUrl(screenUrl);
|
||||
const workspacePaneOpen = panel === "computer" || panel === "artifact";
|
||||
// Only what the work opened gets the wide pane; settings, routines and a hand-opened screen keep
|
||||
// one width so switching between them never jumps.
|
||||
const workspacePaneOpen = panel === "artifact" || (panel === "computer" && autoPanel === panel);
|
||||
const hasControl = userHoldsComputerControl(computer, active?.id);
|
||||
const hideScreenLoadError = computerErrorFromScreen && Boolean(embeddedScreenUrl);
|
||||
const computerScreenError =
|
||||
|
||||
@ -28,6 +28,7 @@ function snapshot(overrides: Partial<WorkspaceFocusSnapshot> = {}): WorkspaceFoc
|
||||
|
||||
const report = {
|
||||
id: "msg-report",
|
||||
role: "bot",
|
||||
runId: "run-1",
|
||||
blocks: [
|
||||
{
|
||||
@ -73,7 +74,7 @@ describe("deriveWorkspaceFocus", () => {
|
||||
for (const computer of [
|
||||
{ ...runningComputer, kind: "desktop" },
|
||||
{ ...runningComputer, kind: "none" },
|
||||
{ ...runningComputer, screenAvailable: false },
|
||||
null,
|
||||
]) {
|
||||
expect(
|
||||
deriveWorkspaceFocus(snapshot({ computer }), { screenRunId: "run-1" }).pane,
|
||||
@ -81,6 +82,25 @@ describe("deriveWorkspaceFocus", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("trusts screen work over a computer status that has not caught up with the boot", () => {
|
||||
const stale = { ...runningComputer, state: "stopped", screenAvailable: false };
|
||||
expect(deriveWorkspaceFocus(snapshot({ computer: stale }), { screenRunId: "run-1" }).pane).toBe(
|
||||
"screen",
|
||||
);
|
||||
expect(
|
||||
deriveWorkspaceFocus(
|
||||
snapshot({
|
||||
run: { id: "run-1", status: "waiting_takeover" },
|
||||
computer: { ...stale, takeoverRequested: true },
|
||||
}),
|
||||
).pane,
|
||||
).toBe("screen");
|
||||
// Without evidence of screen work, a learned default never boots a stopped computer.
|
||||
expect(
|
||||
deriveWorkspaceFocus(snapshot({ computer: stale }), { screenMode: "always" }).pane,
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("shows the screen for a takeover request even when the user turned auto off", () => {
|
||||
const focus = deriveWorkspaceFocus(
|
||||
snapshot({
|
||||
@ -104,7 +124,7 @@ describe("deriveWorkspaceFocus", () => {
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("shows a file the latest run presented", () => {
|
||||
it("shows the file the bot presented as its latest result", () => {
|
||||
const focus = deriveWorkspaceFocus(
|
||||
snapshot({ run: { id: "run-1", status: "completed" }, messages: [report] }),
|
||||
);
|
||||
@ -118,17 +138,20 @@ describe("deriveWorkspaceFocus", () => {
|
||||
expect(focus.key).toBe("artifact:art-1|ask:none");
|
||||
});
|
||||
|
||||
it("ignores files that were only attached, presented by older runs, or not previewable", () => {
|
||||
it("ignores files that were only attached, presented before the user wrote again, or not previewable", () => {
|
||||
const attached = {
|
||||
...report,
|
||||
blocks: [{ ...report.blocks[0]!, present: undefined }],
|
||||
};
|
||||
expect(deriveWorkspaceFocus(snapshot({ messages: [attached] })).pane).toBeNull();
|
||||
const followUp = { id: "msg-user", role: "user", runId: "run-2", blocks: [{ kind: "text" }] };
|
||||
expect(deriveWorkspaceFocus(snapshot({ messages: [report, followUp] })).pane).toBeNull();
|
||||
// Another member's newer run in a group does not hide the result.
|
||||
expect(
|
||||
deriveWorkspaceFocus(
|
||||
snapshot({ run: { id: "run-2", status: "running" }, messages: [report] }),
|
||||
snapshot({ run: { id: "run-9", status: "running" }, messages: [report] }),
|
||||
).pane,
|
||||
).toBeNull();
|
||||
).toBe("artifact");
|
||||
const pdf = {
|
||||
...report,
|
||||
blocks: [{ ...report.blocks[0]!, mimeType: "application/pdf", name: "report.pdf" }],
|
||||
@ -181,7 +204,10 @@ describe("applyWorkspaceFocus", () => {
|
||||
|
||||
it("keeps a presented file open while the user follows up", () => {
|
||||
const followUp = deriveWorkspaceFocus(
|
||||
snapshot({ run: { id: "run-2", status: "running" }, messages: [report] }),
|
||||
snapshot({
|
||||
run: { id: "run-2", status: "running" },
|
||||
messages: [report, { id: "msg-user", role: "user", blocks: [{ kind: "text" }] }],
|
||||
}),
|
||||
);
|
||||
expect(followUp.pane).toBeNull();
|
||||
expect(applyWorkspaceFocus(artifact, followUp, { open: "artifact", auto: true })).toBeNull();
|
||||
|
||||
@ -37,7 +37,12 @@ type FocusBlock = {
|
||||
type FocusRun = { id: string; status: string };
|
||||
|
||||
export type WorkspaceFocusSnapshot = {
|
||||
messages: readonly { id: string; runId?: string | null; blocks: readonly FocusBlock[] }[];
|
||||
messages: readonly {
|
||||
id: string;
|
||||
role?: string;
|
||||
runId?: string | null;
|
||||
blocks: readonly FocusBlock[];
|
||||
}[];
|
||||
run?: FocusRun | null;
|
||||
activeRuns?: readonly FocusRun[];
|
||||
computer?: {
|
||||
@ -89,12 +94,12 @@ function workingRunIds(snapshot: WorkspaceFocusSnapshot): Set<string> {
|
||||
return new Set(runs.filter((run) => WORKING_RUN_STATUSES.has(run.status)).map((run) => run.id));
|
||||
}
|
||||
|
||||
/** The newest file a bot presented since the user last wrote; a new request starts fresh. */
|
||||
function presentedArtifact(snapshot: WorkspaceFocusSnapshot): WorkspaceArtifact | null {
|
||||
const latestRunId = snapshot.run?.id;
|
||||
if (!latestRunId) return null;
|
||||
for (let index = snapshot.messages.length - 1; index >= 0; index -= 1) {
|
||||
const message = snapshot.messages[index];
|
||||
if (!message || message.runId !== latestRunId) continue;
|
||||
if (!message) continue;
|
||||
if (message.role === "user") return null;
|
||||
for (let blockIndex = message.blocks.length - 1; blockIndex >= 0; blockIndex -= 1) {
|
||||
const block = message.blocks[blockIndex];
|
||||
if (
|
||||
@ -119,7 +124,7 @@ function presentedArtifact(snapshot: WorkspaceFocusSnapshot): WorkspaceArtifact
|
||||
/**
|
||||
* Derives the pane a conversation should show from what the bot is actually doing: a
|
||||
* takeover request or live work on its screen shows the screen, a file the bot presented
|
||||
* as its result shows that file, and everything else stays a plain conversation.
|
||||
* as its latest result shows that file, and everything else stays a plain conversation.
|
||||
*/
|
||||
export function deriveWorkspaceFocus(
|
||||
snapshot: WorkspaceFocusSnapshot | null,
|
||||
@ -134,16 +139,19 @@ export function deriveWorkspaceFocus(
|
||||
activeRuns: snapshot.activeRuns,
|
||||
});
|
||||
const computer = snapshot.computer;
|
||||
const screenViewable =
|
||||
Boolean(computer?.screenAvailable) && computer?.kind !== "desktop" && computer?.kind !== "none";
|
||||
const takeover = Boolean(computer?.takeoverRequested) && screenViewable;
|
||||
const working = workingRunIds(snapshot);
|
||||
const mode = live.screenMode ?? "auto";
|
||||
const pointerWork = live.screenRunId != null && working.has(live.screenRunId);
|
||||
const pointerWork =
|
||||
Boolean(live.screenActive) || (live.screenRunId != null && working.has(live.screenRunId));
|
||||
// A desktop host or a bot without a computer has no screen to embed.
|
||||
const embeddable = Boolean(computer) && computer?.kind !== "desktop" && computer?.kind !== "none";
|
||||
// A bot that points at its screen or asks for a takeover has a running screen, even when the
|
||||
// client's computer status has not caught up with a boot that happened inside the run.
|
||||
const takeover = embeddable && Boolean(computer?.takeoverRequested);
|
||||
const screenWork =
|
||||
screenViewable &&
|
||||
embeddable &&
|
||||
mode !== "never" &&
|
||||
((working.size > 0 && (mode === "always" || pointerWork)) || Boolean(live.screenActive));
|
||||
(pointerWork || (mode === "always" && working.size > 0 && Boolean(computer?.screenAvailable)));
|
||||
const artifact = presentedArtifact(snapshot);
|
||||
const pane: WorkspacePane | null =
|
||||
takeover || screenWork ? "screen" : artifact ? "artifact" : null;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user