diff --git a/apps/server/src/preview/ServerBrowser.test.ts b/apps/server/src/preview/ServerBrowser.test.ts index 6ab35a9613674..e7cf944942548 100644 --- a/apps/server/src/preview/ServerBrowser.test.ts +++ b/apps/server/src/preview/ServerBrowser.test.ts @@ -799,6 +799,40 @@ it.effect("an agent's tab the reloaded desktop dropped goes back to the desktop" ).pipe(Effect.provide(layer)), ); +it.effect("a viewer still that never answers does not block the agent's snapshot", () => + Effect.scoped( + Effect.gen(function* () { + const { browser, broker, tabId } = yield* ready; + const context = contexts[0]!; + const stalled = Promise.withResolvers(); + const newCDPSession = context.newCDPSession; + context.newCDPSession = async (target) => { + const session = await newCDPSession(target); + const send = session.send.getMockImplementation()!; + session.send.mockImplementation((method, input) => { + if (method !== "Page.captureScreenshot") return send(method, input); + stalled.resolve(); + return new Promise(() => {}); + }); + return session; + }; + yield* browser.attachViewer(viewerInput(tabId, false)).pipe(Effect.forkScoped); + yield* Effect.promise(() => stalled.promise); + + const snapshot = yield* broker + .invoke({ scope, tabId, operation: "snapshot", input: {} }) + .pipe(Effect.result); + const evaluated = yield* broker + .invoke({ scope, tabId, operation: "evaluate", input: { expression: "document.title" } }) + .pipe(Effect.result); + expect({ snapshot: snapshot._tag, evaluated: evaluated._tag }).toEqual({ + snapshot: "Success", + evaluated: "Success", + }); + }), + ).pipe(Effect.provide(layer)), +); + it.effect("a desktop page that loaded before the server attached reports its loaded title", () => Effect.scoped( Effect.gen(function* () { diff --git a/apps/server/src/preview/ServerBrowser.ts b/apps/server/src/preview/ServerBrowser.ts index f06c6b34d41b5..cdd57cf3ada87 100644 --- a/apps/server/src/preview/ServerBrowser.ts +++ b/apps/server/src/preview/ServerBrowser.ts @@ -2776,9 +2776,15 @@ const make = Effect.gen(function* () { broadcastControl(tab); pushFileChooser(tab); const pushStill = async () => { - const data = await withCaptureLock(tab, () => - ServerBrowserPage.captureViewport(session, { format: "jpeg", quality }), - ).catch(() => null); + // Waits out queued captures without joining the queue: a webview that cannot + // paint never answers, and the agent's next capture must not wait on it. + const data = await tab.captureLock + .then(() => + tab.closing + ? null + : ServerBrowserPage.captureViewport(session, { format: "jpeg", quality }), + ) + .catch(() => null); if (data) viewer.push({ _tag: "frame", data: Buffer.from(data, "base64"), ack: Effect.void }); }; let settleTimer: ReturnType | null = null;