evOo-local/packages/core/src/workspace-focus.test.ts
cynarAI e28c3acf02
feat: let the pane beside a conversation follow the bot's work (#123)
The live screen opens wide beside the chat while the bot works on it or asks
for a takeover, a file the bot presents as its result opens next to the
conversation, and a question that scrolled away gets a "Needs you" pill back
to it. Shared core rules derive the focus from signals clients already have
(pointer events, takeover requests, a new `present` flag on attach_file,
pending asks), move the pane only on focus edges, never replace a pane the
user opened, and learn per bot whether the screen opens on its own. Pane
widths persist per device; mobile shows a pill that opens the computer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C7VSRHHzyt2KwoCkUkZ2Gc
2026-10-03 22:23:46 +02:00

306 lines
10 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
applyWorkspaceFocus,
deriveWorkspaceFocus,
isPaneArtifactMimeType,
liveScreenRunId,
parseScreenPaneHabit,
recordScreenPaneChoice,
screenPaneMode,
type WorkspaceFocusSnapshot,
} from "./workspace-focus.js";
const runningComputer = {
kind: "docker",
state: "running",
screenAvailable: true,
takeoverRequested: false,
};
function snapshot(overrides: Partial<WorkspaceFocusSnapshot> = {}): WorkspaceFocusSnapshot {
return {
messages: [],
run: { id: "run-1", status: "running" },
computer: runningComputer,
...overrides,
};
}
const report = {
id: "msg-report",
role: "bot",
runId: "run-1",
blocks: [
{
kind: "file",
artifactId: "art-1",
name: "report.md",
mimeType: "text/markdown",
present: true,
},
],
};
describe("deriveWorkspaceFocus", () => {
it("keeps a plain conversation when the bot only chats", () => {
const focus = deriveWorkspaceFocus(
snapshot({ messages: [{ id: "m", runId: "run-1", blocks: [{ kind: "text" }] }] }),
);
expect(focus.pane).toBeNull();
expect(focus.key).toBe("none|ask:none");
});
it("shows the screen once the working run acts on it", () => {
expect(deriveWorkspaceFocus(snapshot(), { screenRunId: "run-1" }).pane).toBe("screen");
expect(deriveWorkspaceFocus(snapshot(), { screenRunId: "run-old" }).pane).toBeNull();
});
it("keeps showing the screen while the bot pointed at it moments ago", () => {
const finished = snapshot({ run: { id: "run-1", status: "completed" } });
expect(deriveWorkspaceFocus(finished, { screenActive: true }).pane).toBe("screen");
expect(
deriveWorkspaceFocus(finished, { screenActive: true, screenMode: "never" }).pane,
).toBeNull();
});
it("returns to the conversation when the run finishes", () => {
const focus = deriveWorkspaceFocus(snapshot({ run: { id: "run-1", status: "completed" } }), {
screenRunId: "run-1",
});
expect(focus.pane).toBeNull();
});
it("never offers a screen it cannot embed", () => {
for (const computer of [
{ ...runningComputer, kind: "desktop" },
{ ...runningComputer, kind: "none" },
null,
]) {
expect(
deriveWorkspaceFocus(snapshot({ computer }), { screenRunId: "run-1" }).pane,
).toBeNull();
}
});
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({
run: { id: "run-1", status: "waiting_takeover" },
computer: { ...runningComputer, takeoverRequested: true },
}),
{ screenMode: "never" },
);
expect(focus).toMatchObject({ pane: "screen", takeover: true });
});
it("follows the learned screen mode", () => {
expect(deriveWorkspaceFocus(snapshot(), { screenMode: "always" }).pane).toBe("screen");
expect(
deriveWorkspaceFocus(snapshot(), { screenMode: "never", screenRunId: "run-1" }).pane,
).toBeNull();
expect(
deriveWorkspaceFocus(snapshot({ run: { id: "run-1", status: "completed" } }), {
screenMode: "always",
}).pane,
).toBeNull();
});
it("shows the file the bot presented as its latest result", () => {
const focus = deriveWorkspaceFocus(
snapshot({ run: { id: "run-1", status: "completed" }, messages: [report] }),
);
expect(focus.pane).toBe("artifact");
expect(focus.artifact).toEqual({
messageId: "msg-report",
artifactId: "art-1",
name: "report.md",
mimeType: "text/markdown",
});
expect(focus.key).toBe("artifact:art-1|ask:none");
});
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-9", status: "running" }, messages: [report] }),
).pane,
).toBe("artifact");
const pdf = {
...report,
blocks: [{ ...report.blocks[0]!, mimeType: "application/pdf", name: "report.pdf" }],
};
expect(deriveWorkspaceFocus(snapshot({ messages: [pdf] })).pane).toBeNull();
});
it("prefers live screen work over a presented file", () => {
expect(
deriveWorkspaceFocus(snapshot({ messages: [report] }), { screenRunId: "run-1" }).pane,
).toBe("screen");
});
it("names the ask the bot is waiting on", () => {
const focus = deriveWorkspaceFocus(
snapshot({
run: { id: "run-1", status: "waiting_input" },
messages: [{ id: "ask-1", runId: "run-1", blocks: [{ kind: "ask", status: "pending" }] }],
}),
);
expect(focus.needsYouMessageId).toBe("ask-1");
expect(focus.key).toBe("none|ask:ask-1");
});
});
describe("applyWorkspaceFocus", () => {
const plain = deriveWorkspaceFocus(snapshot());
const screen = deriveWorkspaceFocus(snapshot(), { screenRunId: "run-1" });
const artifact = deriveWorkspaceFocus(
snapshot({ run: { id: "run-1", status: "completed" }, messages: [report] }),
);
const closed = { open: null, auto: false } as const;
it("treats the first focus of a conversation as a baseline", () => {
expect(applyWorkspaceFocus(null, screen, closed)).toBeNull();
});
it("opens the pane on an edge and closes it again when the work ends", () => {
const opened = applyWorkspaceFocus(plain, screen, closed);
expect(opened).toEqual({ open: "screen", auto: true });
expect(applyWorkspaceFocus(screen, plain, opened!)).toEqual(closed);
});
it("moves from the screen to the presented result", () => {
expect(applyWorkspaceFocus(screen, artifact, { open: "screen", auto: true })).toEqual({
open: "artifact",
auto: true,
});
});
it("keeps a presented file open while the user follows up", () => {
const followUp = deriveWorkspaceFocus(
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();
});
it("never replaces or closes what the user opened by hand", () => {
expect(applyWorkspaceFocus(plain, screen, { open: "other", auto: false })).toBeNull();
expect(applyWorkspaceFocus(plain, artifact, { open: "screen", auto: false })).toBeNull();
expect(applyWorkspaceFocus(screen, plain, { open: "screen", auto: false })).toBeNull();
expect(applyWorkspaceFocus(plain, screen, { open: "screen", auto: false })).toBeNull();
});
it("swaps in a newer presented file", () => {
const newer = deriveWorkspaceFocus(
snapshot({
run: { id: "run-1", status: "completed" },
messages: [
report,
{
...report,
id: "msg-2",
blocks: [{ ...report.blocks[0]!, artifactId: "art-2", name: "v2.md" }],
},
],
}),
);
expect(applyWorkspaceFocus(artifact, newer, { open: "artifact", auto: false })).toEqual({
open: "artifact",
auto: false,
});
});
it("ignores focus changes that only concern a new ask", () => {
const asking = { ...plain, needsYouMessageId: "ask-1", key: "none|ask:ask-1" };
expect(applyWorkspaceFocus(plain, asking, closed)).toBeNull();
});
});
describe("screen pane habit", () => {
it("learns to always show the screen after repeated manual opens", () => {
let habit = 0;
for (let index = 0; index < 3; index += 1) habit = recordScreenPaneChoice(habit, "opened");
expect(screenPaneMode(habit)).toBe("always");
expect(recordScreenPaneChoice(habit, "opened")).toBe(3);
});
it("learns to stop opening it after repeated dismissals, and recovers on a manual open", () => {
let habit = recordScreenPaneChoice(2, "dismissed");
expect(screenPaneMode(habit)).toBe("auto");
habit = recordScreenPaneChoice(habit, "dismissed");
expect(screenPaneMode(habit)).toBe("never");
expect(screenPaneMode(recordScreenPaneChoice(habit, "opened"))).toBe("auto");
});
it("parses stored habits defensively", () => {
expect(parseScreenPaneHabit(null)).toBe(0);
expect(parseScreenPaneHabit("2")).toBe(2);
expect(parseScreenPaneHabit("99")).toBe(3);
expect(parseScreenPaneHabit("-9")).toBe(-2);
expect(parseScreenPaneHabit("x")).toBe(0);
});
});
describe("isPaneArtifactMimeType", () => {
it("accepts text and images, not PDFs", () => {
expect(isPaneArtifactMimeType("text/csv")).toBe(true);
expect(isPaneArtifactMimeType("image/png")).toBe(true);
expect(isPaneArtifactMimeType("application/pdf")).toBe(false);
});
});
describe("liveScreenRunId", () => {
const pointer = {
type: "thread.computer",
runId: "run-1",
createdAt: "2026-01-01T00:00:10.000Z",
payload: {
kind: "pointer",
screen: { width: 1280, height: 800 },
steps: [{ action: "click", x: 640, y: 400 }],
},
};
const now = Date.parse("2026-01-01T00:00:20.000Z");
it("names the run that is pointing at the screen right now", () => {
expect(liveScreenRunId(pointer, now)).toBe("run-1");
});
it("ignores stale, malformed and unrelated events", () => {
expect(liveScreenRunId(pointer, now + 60_000)).toBeNull();
expect(liveScreenRunId({ ...pointer, payload: { kind: "pointer" } }, now)).toBeNull();
expect(liveScreenRunId({ ...pointer, runId: undefined }, now)).toBeNull();
expect(liveScreenRunId({ ...pointer, type: "thread.progress" }, now)).toBeNull();
});
});