Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions apps/desktop/src/preview/DesktopBrowserHost.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -964,6 +964,40 @@ describe("DesktopBrowserHost", () => {
}).pipe(Effect.scoped),
);

it.effect("paints a hidden guest the agent drives until its turn ends", () =>
Effect.gen(function* () {
const host = yield* DesktopBrowserHost.make;
const debuggee = makeDebuggee();
host.attach(key, debuggee.tab);
const painting = yield* watchPainting(host);
const turn = (active: boolean) =>
host.handleCommandLine(encodeJson({ type: "turn", threadId: key.threadId, active }));

yield* turn(true);
// Playwright's actionability check awaits the page's next frame.
yield* command(host, 1, "Runtime.callFunctionOn");
yield* settle;
expect(painting).toEqual([true]);

debuggee.release();
yield* settle;
yield* TestClock.adjust("1 minute");
expect(painting).toEqual([true]);

yield* turn(false);
expect(painting).toEqual([true, false]);

// Outside a turn it paints until the agent goes idle.
yield* command(host, 2, "Input.dispatchMouseEvent");
yield* settle;
debuggee.release();
yield* settle;
expect(painting).toEqual([true, false, true]);
yield* TestClock.adjust("1 minute");
expect(painting).toEqual([true, false, true, false]);
}).pipe(Effect.scoped),
);

it.effect("stops painting for a screenshot Chromium never answers once its document goes", () =>
Effect.gen(function* () {
const host = yield* DesktopBrowserHost.make;
Expand Down
11 changes: 7 additions & 4 deletions apps/desktop/src/preview/DesktopBrowserHost.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,20 +225,22 @@ export const make = Effect.gen(function* () {
// The agent pauses to think between steps; its tab stays focused until the turn ends.
if (tab.agentUsed && agentTurns.has(tab.key.threadId)) return;
tab.agentUsed = false;
syncPainting(tab);
if (tabs.get(keyOf(tab.key)) === tab) setFocusEmulation(tab, false);
}),
);
};

/**
* Electron composites no offscreen guest, so its screenshot or screencast waits for a frame
* that never comes. The client paints the guest behind the app while one is pending; the
* capture then finishes on its own, and the guest goes back offscreen once none is.
* Electron composites no offscreen guest, so a capture, or agent input that waits on the
* next frame, waits forever. The client paints the guest behind the app while one is pending
* or the agent drives it, and the guest goes back offscreen once neither holds.
*/
const syncPainting = (tab: AttachedTab) => {
const active =
tabs.get(keyOf(tab.key)) === tab &&
(tab.screencasts.size > 0 ||
(tab.agentUsed ||
tab.screencasts.size > 0 ||
tab.capturing.size > 0 ||
tab.waitingScreenshots.some((waiting) => waiting.live()));
if (tab.painting === active) return;
Expand Down Expand Up @@ -344,6 +346,7 @@ export const make = Effect.gen(function* () {
(owner === undefined || !streamingOwners.has(owner))
) {
tab.agentUsed = true;
syncPainting(tab);
}
// Waits for focus emulation; insertText and other non-CDP input would not queue behind it.
const forward = (current?: () => boolean) => {
Expand Down
Loading