fix(browser): park webviews permanently paintable

Delete the renderer capture prep handshake and keep screenshots on the serialized invalidate/retry path. The capture harness now defaults to the production P1 attach-off parking check.
This commit is contained in:
Mohamed Boudra
2026-07-03 11:34:48 +02:00
parent cc21262130
commit ab62a023fd
15 changed files with 358 additions and 1652 deletions
@@ -1,47 +1,67 @@
import { afterEach, describe, expect, it } from "vitest";
import {
cancelResidentBrowserWebviewPixelCapture,
clearResidentBrowserWebviewsForTests,
ensureResidentBrowserWebview,
prepareBrowserWebview,
prepareResidentBrowserWebviewForPixelCapture,
releaseResidentBrowserWebview,
removeResidentBrowserWebview,
restoreResidentBrowserWebviewAfterPixelCapture,
takeResidentBrowserWebview,
} from "./browser-webview-resident";
const RESIDENT_HOST_ID = "paseo-browser-resident-webviews";
function residentHost(): HTMLElement {
const host = document.getElementById(RESIDENT_HOST_ID);
if (!host) {
throw new Error("Expected resident browser host");
}
return host;
}
function expectPermanentHostParking(host: HTMLElement): void {
expect(host.style.position).toBe("fixed");
expect(host.style.left).toBe("0px");
expect(host.style.top).toBe("0px");
expect(host.style.width).toBe("1px");
expect(host.style.height).toBe("1px");
expect(host.style.overflow).toBe("hidden");
expect(host.style.opacity).toBe("1");
expect(host.style.pointerEvents).toBe("none");
expect(host.style.display).toBe("block");
expect(host.style.visibility).toBe("visible");
expect(host.style.transform).toBe("");
}
function expectResidentWebviewParking(webview: HTMLElement): void {
expect(webview.style.display).toBe("inline-flex");
expect(webview.style.flex).toBe("0 0 auto");
expect(webview.style.width).toBe("1280px");
expect(webview.style.height).toBe("800px");
expect(webview.style.position).toBe("absolute");
expect(webview.style.left).toBe("0px");
expect(webview.style.top).toBe("0px");
expect(webview.style.zIndex).toBe("0");
}
describe("resident browser webviews", () => {
afterEach(() => {
clearResidentBrowserWebviewsForTests();
});
it("keeps a browser webview mounted offscreen and reuses the same node", () => {
const host = document.createElement("div");
it("parks a browser webview in the permanent paintable 1x1 host", () => {
const visibleHost = document.createElement("div");
const webview = document.createElement("webview");
host.appendChild(webview);
document.body.appendChild(host);
visibleHost.appendChild(webview);
document.body.appendChild(visibleHost);
releaseResidentBrowserWebview("browser-a", webview);
expect(host.children).toHaveLength(0);
const host = residentHost();
expect(visibleHost.children).toHaveLength(0);
expect(Array.from(host.children)).toEqual([webview]);
expect(webview.isConnected).toBe(true);
expect(webview.style.display).toBe("inline-flex");
expect(webview.style.width).toBe("1280px");
expect(webview.style.height).toBe("800px");
expect(webview.style.position).toBe("absolute");
expect(webview.style.left).toBe("0px");
expect(webview.style.top).toBe("0px");
expect(webview.style.zIndex).toBe("0");
const reused = takeResidentBrowserWebview("browser-a");
expect(reused).toBe(webview);
expect(webview.style.position).toBe("");
expect(webview.style.left).toBe("");
expect(webview.style.top).toBe("");
expect(webview.style.zIndex).toBe("");
expect(takeResidentBrowserWebview("browser-a")).toBeNull();
expectPermanentHostParking(host);
expectResidentWebviewParking(webview);
});
it("creates a resident webview for an agent-created unfocused tab", () => {
@@ -57,6 +77,80 @@ describe("resident browser webviews", () => {
expect((webview as HTMLUnknownElement & { src?: string })?.src).toContain(
"https://example.com",
);
expectPermanentHostParking(residentHost());
expectResidentWebviewParking(webview as HTMLElement);
});
it("normalizes an existing resident host back to permanent parking", () => {
const staleHost = document.createElement("div");
staleHost.id = RESIDENT_HOST_ID;
staleHost.style.left = "-20000px";
staleHost.style.width = "1280px";
staleHost.style.height = "800px";
staleHost.style.opacity = "0";
staleHost.style.display = "none";
document.body.appendChild(staleHost);
const webview = ensureResidentBrowserWebview({
browserId: "browser-stale-host",
url: "https://example.com",
});
expect(webview).not.toBeNull();
expectPermanentHostParking(staleHost);
expectResidentWebviewParking(webview as HTMLElement);
});
it("parks resident webviews as an overlapping stack", () => {
const firstWebview = ensureResidentBrowserWebview({
browserId: "browser-first",
url: "https://example.com/first",
});
const secondWebview = ensureResidentBrowserWebview({
browserId: "browser-second",
url: "https://example.com/second",
});
const host = residentHost();
expect(firstWebview?.parentElement).toBe(host);
expect(secondWebview?.parentElement).toBe(host);
expectResidentWebviewParking(firstWebview as HTMLElement);
expectResidentWebviewParking(secondWebview as HTMLElement);
});
it("moves a resident webview into a visible pane without recreating the node", () => {
const webview = ensureResidentBrowserWebview({
browserId: "browser-visible",
url: "https://example.com",
});
const visibleWebview = takeResidentBrowserWebview("browser-visible");
expect(visibleWebview).toBe(webview);
expect(webview?.style.position).toBe("");
expect(webview?.style.left).toBe("");
expect(webview?.style.top).toBe("");
expect(webview?.style.zIndex).toBe("");
expect(takeResidentBrowserWebview("browser-visible")).toBeNull();
});
it("returns an existing visible pane webview instead of creating a resident duplicate", () => {
const visibleHost = document.createElement("div");
const visibleWebview = document.createElement("webview");
prepareBrowserWebview(visibleWebview, {
browserId: "browser-visible-pane",
initialUrl: "https://example.com",
});
visibleHost.appendChild(visibleWebview);
document.body.appendChild(visibleHost);
const webview = ensureResidentBrowserWebview({
browserId: "browser-visible-pane",
url: "https://example.com/agent",
});
expect(webview).toBe(visibleWebview);
expect(document.getElementById(RESIDENT_HOST_ID)).toBeNull();
});
it("removes a resident webview when its browser tab closes", () => {
@@ -70,231 +164,4 @@ describe("resident browser webviews", () => {
expect(webview?.isConnected).toBe(false);
expect(takeResidentBrowserWebview("browser-closed")).toBeNull();
});
it("temporarily makes resident webviews paintable for pixel capture", async () => {
const webview = ensureResidentBrowserWebview({
browserId: "browser-capture",
url: "https://example.com",
});
if (!webview) {
throw new Error("Expected resident browser webview");
}
const preparation = await prepareResidentBrowserWebviewForPixelCapture({
browserId: "browser-capture",
});
const host = document.getElementById("paseo-browser-resident-webviews");
expect(preparation.token).toBe("capture-1");
expect(host?.style.left).toBe("0px");
expect(host?.style.top).toBe("0px");
expect(host?.style.width).toBe("1px");
expect(host?.style.height).toBe("1px");
expect(host?.style.overflow).toBe("hidden");
expect(host?.style.opacity).toBe("1");
expect(host?.style.pointerEvents).toBe("none");
expect(webview.style.display).toBe("inline-flex");
expect(webview.style.width).toBe("1280px");
expect(webview.style.height).toBe("800px");
expect(webview.style.position).toBe("absolute");
expect(webview.style.left).toBe("0px");
expect(webview.style.top).toBe("0px");
expect(webview.style.zIndex).toBe("2");
await restoreResidentBrowserWebviewAfterPixelCapture(preparation);
expect(host?.style.left).toBe("-20000px");
expect(host?.style.width).toBe("1280px");
expect(host?.style.height).toBe("800px");
expect(host?.style.opacity).toBe("0");
expect(webview.style.zIndex).toBe("0");
});
it("parks resident webviews as an overlapping stack and raises the capture target", async () => {
const firstWebview = ensureResidentBrowserWebview({
browserId: "browser-first",
url: "https://example.com/first",
});
const secondWebview = ensureResidentBrowserWebview({
browserId: "browser-second",
url: "https://example.com/second",
});
if (!firstWebview || !secondWebview) {
throw new Error("Expected resident browser webviews");
}
expect(firstWebview.style.position).toBe("absolute");
expect(firstWebview.style.left).toBe("0px");
expect(firstWebview.style.top).toBe("0px");
expect(firstWebview.style.zIndex).toBe("0");
expect(secondWebview.style.position).toBe("absolute");
expect(secondWebview.style.left).toBe("0px");
expect(secondWebview.style.top).toBe("0px");
expect(secondWebview.style.zIndex).toBe("0");
const preparation = await prepareResidentBrowserWebviewForPixelCapture({
browserId: "browser-second",
});
expect(firstWebview.style.zIndex).toBe("0");
expect(secondWebview.style.zIndex).toBe("2");
await restoreResidentBrowserWebviewAfterPixelCapture(preparation);
expect(firstWebview.style.zIndex).toBe("0");
expect(secondWebview.style.zIndex).toBe("0");
});
it("prepares the physically resident webview when a focused duplicate exists outside the resident host", async () => {
const residentWebview = ensureResidentBrowserWebview({
browserId: "browser-focused-undisplayed",
url: "https://example.com/resident",
});
if (!residentWebview) {
throw new Error("Expected resident browser webview");
}
const visibleHost = document.createElement("div");
const duplicateWebview = document.createElement("webview");
prepareBrowserWebview(duplicateWebview, {
browserId: "browser-focused-undisplayed",
initialUrl: "https://example.com/duplicate",
});
visibleHost.appendChild(duplicateWebview);
document.body.prepend(visibleHost);
const preparation = await prepareResidentBrowserWebviewForPixelCapture({
browserId: "browser-focused-undisplayed",
});
const residentHost = document.getElementById("paseo-browser-resident-webviews");
expect(residentHost?.style.left).toBe("0px");
expect(residentHost?.style.width).toBe("1px");
expect(residentWebview.style.zIndex).toBe("2");
expect(duplicateWebview.style.zIndex).toBe("");
await restoreResidentBrowserWebviewAfterPixelCapture(preparation);
});
it("does not apply resident prep styles for a genuinely visible pane webview", async () => {
const visibleHost = document.createElement("div");
const visibleWebview = document.createElement("webview");
prepareBrowserWebview(visibleWebview, {
browserId: "browser-visible-pane",
initialUrl: "https://example.com",
});
visibleWebview.style.display = "flex";
visibleWebview.style.width = "100%";
visibleWebview.style.height = "100%";
visibleHost.appendChild(visibleWebview);
document.body.appendChild(visibleHost);
const preparation = await prepareResidentBrowserWebviewForPixelCapture({
browserId: "browser-visible-pane",
});
const residentHost = document.getElementById("paseo-browser-resident-webviews");
expect(residentHost?.style.left).toBe("-20000px");
expect(residentHost?.style.width).toBe("1280px");
expect(visibleWebview.style.position).toBe("");
expect(visibleWebview.style.zIndex).toBe("");
await restoreResidentBrowserWebviewAfterPixelCapture(preparation);
});
it("clears capture preparation when a resident webview is taken visible", async () => {
const webview = ensureResidentBrowserWebview({
browserId: "browser-visible",
url: "https://example.com",
});
if (!webview) {
throw new Error("Expected resident browser webview");
}
const preparation = await prepareResidentBrowserWebviewForPixelCapture({
browserId: "browser-visible",
});
const visibleWebview = takeResidentBrowserWebview("browser-visible");
expect(visibleWebview).toBe(webview);
expect(webview.style.position).toBe("");
expect(webview.style.left).toBe("");
expect(webview.style.top).toBe("");
expect(webview.style.zIndex).toBe("");
await restoreResidentBrowserWebviewAfterPixelCapture(preparation);
expect(webview.style.position).toBe("");
expect(webview.style.left).toBe("");
expect(webview.style.top).toBe("");
expect(webview.style.zIndex).toBe("");
});
it("keeps the resident host paintable until every capture token is restored", async () => {
ensureResidentBrowserWebview({
browserId: "browser-overlap",
url: "https://example.com",
});
const first = await prepareResidentBrowserWebviewForPixelCapture({
browserId: "browser-overlap",
});
const second = await prepareResidentBrowserWebviewForPixelCapture({
browserId: "browser-overlap",
});
const host = document.getElementById("paseo-browser-resident-webviews");
await restoreResidentBrowserWebviewAfterPixelCapture(first);
expect(host?.style.left).toBe("0px");
expect(host?.style.width).toBe("1px");
expect(host?.style.opacity).toBe("1");
await restoreResidentBrowserWebviewAfterPixelCapture(second);
expect(host?.style.left).toBe("-20000px");
expect(host?.style.width).toBe("1280px");
expect(host?.style.opacity).toBe("0");
});
it("cancels an in-flight pixel capture preparation by request id", async () => {
ensureResidentBrowserWebview({
browserId: "browser-cancel",
url: "https://example.com",
});
const preparation = prepareResidentBrowserWebviewForPixelCapture({
requestId: "prepare-1",
browserId: "browser-cancel",
});
const host = document.getElementById("paseo-browser-resident-webviews");
expect(host?.style.left).toBe("0px");
expect(host?.style.opacity).toBe("1");
await cancelResidentBrowserWebviewPixelCapture({ requestId: "prepare-1" });
await expect(preparation).rejects.toThrow("Browser pixel capture preparation was canceled.");
expect(host?.style.left).toBe("-20000px");
expect(host?.style.width).toBe("1280px");
expect(host?.style.opacity).toBe("0");
});
it("parks the resident host when a prepared browser tab is removed", async () => {
const webview = ensureResidentBrowserWebview({
browserId: "browser-detached",
url: "https://example.com",
});
const preparation = await prepareResidentBrowserWebviewForPixelCapture({
browserId: "browser-detached",
});
const host = document.getElementById("paseo-browser-resident-webviews");
removeResidentBrowserWebview("browser-detached");
expect(webview?.isConnected).toBe(false);
expect(host?.style.left).toBe("-20000px");
expect(host?.style.width).toBe("1280px");
expect(host?.style.opacity).toBe("0");
await restoreResidentBrowserWebviewAfterPixelCapture(preparation);
expect(host?.style.left).toBe("-20000px");
});
});
@@ -1,28 +1,14 @@
import { getDesktopHost } from "@/desktop/host";
const RESIDENT_BROWSER_HOST_ID = "paseo-browser-resident-webviews";
const BROWSER_ID_ATTRIBUTE = "data-paseo-browser-id";
const RESIDENT_VIEWPORT_WIDTH = 1280;
const RESIDENT_VIEWPORT_HEIGHT = 800;
const residentWebviewsByBrowserId = new Map<string, HTMLElement>();
const activeCapturePreparations = new Map<string, ActiveCapturePreparation>();
let captureBridgeInstallCount = 0;
let captureBridgeDisposer: (() => void) | null = null;
let nextCapturePreparationId = 0;
interface BrowserWebviewElement extends HTMLElement {
src: string;
}
interface ActiveCapturePreparation {
browserId: string;
requestId?: string;
preparesResidentHost: boolean;
webview: HTMLElement;
}
function trimNonEmpty(value: string | null | undefined): string | null {
if (typeof value !== "string") {
return null;
@@ -36,22 +22,8 @@ function readDocument(): Document | null {
}
function applyResidentHostParkingStyle(host: HTMLElement): void {
host.setAttribute("aria-hidden", "true");
host.style.position = "fixed";
host.style.left = "-20000px";
host.style.top = "0";
host.style.width = `${RESIDENT_VIEWPORT_WIDTH}px`;
host.style.height = `${RESIDENT_VIEWPORT_HEIGHT}px`;
host.style.overflow = "hidden";
host.style.opacity = "0";
host.style.pointerEvents = "none";
host.style.zIndex = "";
host.style.clipPath = "";
host.style.visibility = "";
host.style.transform = "";
}
function applyResidentHostCaptureStyle(host: HTMLElement): void {
// Parked browser webviews must remain paintable at all times; screenshot
// correctness depends on the proven states in docs/browser-capture-harness.md.
host.setAttribute("aria-hidden", "true");
host.style.position = "fixed";
host.style.left = "0";
@@ -61,15 +33,17 @@ function applyResidentHostCaptureStyle(host: HTMLElement): void {
host.style.overflow = "hidden";
host.style.opacity = "1";
host.style.pointerEvents = "none";
host.style.zIndex = "1";
host.style.display = "block";
host.style.zIndex = "";
host.style.clipPath = "";
host.style.visibility = "";
host.style.visibility = "visible";
host.style.transform = "";
}
function getResidentBrowserHost(ownerDocument: Document): HTMLElement {
const existing = ownerDocument.getElementById(RESIDENT_BROWSER_HOST_ID);
if (existing) {
applyResidentHostParkingStyle(existing);
return existing;
}
@@ -92,17 +66,6 @@ function findBrowserWebview(browserId: string, ownerDocument: Document): HTMLEle
return null;
}
function findBrowserWebviewForPixelCapture(
browserId: string,
ownerDocument: Document,
): HTMLElement | null {
const resident = residentWebviewsByBrowserId.get(browserId) ?? null;
if (resident?.isConnected) {
return resident;
}
return findBrowserWebview(browserId, ownerDocument);
}
function applyResidentWebviewStyle(webview: HTMLElement): void {
webview.style.display = "inline-flex";
webview.style.flex = "0 0 auto";
@@ -125,116 +88,6 @@ function clearResidentWebviewParkingStyle(webview: HTMLElement): void {
webview.style.zIndex = "";
}
function residentWebviewChildren(host: HTMLElement): HTMLElement[] {
const webviews: HTMLElement[] = [];
for (const element of host.querySelectorAll(`[${BROWSER_ID_ATTRIBUTE}]`)) {
if (element instanceof HTMLElement) {
webviews.push(element);
}
}
return webviews;
}
function raiseResidentWebviewForCapture(host: HTMLElement, target: HTMLElement): void {
for (const webview of residentWebviewChildren(host)) {
applyResidentWebviewStyle(webview);
}
applyResidentWebviewStyle(target);
target.style.zIndex = "2";
}
function nextAnimationFrame(): Promise<void> {
if (typeof requestAnimationFrame !== "function") {
return Promise.resolve();
}
return new Promise((resolve) => {
requestAnimationFrame(() => {
resolve();
});
});
}
async function waitForCapturePaint(webview: HTMLElement): Promise<void> {
await nextAnimationFrame();
await nextAnimationFrame();
webview.getBoundingClientRect();
}
function activeCaptureTokenFor(input: { token?: string; requestId?: string }): string | null {
const token = trimNonEmpty(input.token);
if (token && activeCapturePreparations.has(token)) {
return token;
}
const requestId = trimNonEmpty(input.requestId);
if (!requestId) {
return null;
}
for (const [candidateToken, preparation] of activeCapturePreparations.entries()) {
if (preparation.requestId === requestId) {
return candidateToken;
}
}
return null;
}
function latestActiveResidentHostPreparation(): ActiveCapturePreparation | null {
let latest: ActiveCapturePreparation | null = null;
for (const preparation of activeCapturePreparations.values()) {
if (preparation.preparesResidentHost) {
latest = preparation;
}
}
return latest;
}
function syncResidentHostCaptureState(): void {
const host = readDocument()?.getElementById(RESIDENT_BROWSER_HOST_ID);
if (!(host instanceof HTMLElement)) {
return;
}
const latest = latestActiveResidentHostPreparation();
if (latest) {
applyResidentHostCaptureStyle(host);
raiseResidentWebviewForCapture(host, latest.webview);
return;
}
applyResidentHostParkingStyle(host);
for (const webview of residentWebviewChildren(host)) {
applyResidentWebviewStyle(webview);
}
}
function releaseCapturePreparationToken(token: string): void {
const preparation = activeCapturePreparations.get(token);
if (!preparation) {
return;
}
activeCapturePreparations.delete(token);
if (preparation.preparesResidentHost) {
const host = readDocument()?.getElementById(RESIDENT_BROWSER_HOST_ID);
if (host instanceof HTMLElement && preparation.webview.parentElement === host) {
applyResidentWebviewStyle(preparation.webview);
}
syncResidentHostCaptureState();
}
}
function releaseCapturePreparationsForBrowser(browserId: string): void {
for (const [token, preparation] of activeCapturePreparations.entries()) {
if (preparation.browserId === browserId) {
activeCapturePreparations.delete(token);
}
}
syncResidentHostCaptureState();
}
function releaseAllCapturePreparations(): void {
activeCapturePreparations.clear();
syncResidentHostCaptureState();
}
export function prepareBrowserWebview(
webview: HTMLElement,
input: { browserId: string; initialUrl?: string | null },
@@ -293,7 +146,6 @@ export function takeResidentBrowserWebview(browserId: string): HTMLElement | nul
}
residentWebviewsByBrowserId.delete(normalizedBrowserId);
releaseCapturePreparationsForBrowser(normalizedBrowserId);
clearResidentWebviewParkingStyle(webview);
return webview;
}
@@ -314,98 +166,6 @@ export function releaseResidentBrowserWebview(browserId: string, webview: HTMLEl
getResidentBrowserHost(ownerDocument).appendChild(webview);
}
export async function prepareResidentBrowserWebviewForPixelCapture(input: {
browserId: string;
requestId?: string;
}): Promise<{ token: string }> {
const browserId = trimNonEmpty(input.browserId);
if (!browserId) {
throw new Error("Browser id is required for pixel capture preparation.");
}
const ownerDocument = readDocument();
if (!ownerDocument) {
throw new Error("Browser pixel capture preparation requires a document.");
}
const host = getResidentBrowserHost(ownerDocument);
const webview = findBrowserWebviewForPixelCapture(browserId, ownerDocument);
if (!webview) {
throw new Error(`Browser webview ${browserId} is not mounted.`);
}
const token = `capture-${++nextCapturePreparationId}`;
const preparesResidentHost = webview.parentElement === host;
const requestId = trimNonEmpty(input.requestId);
activeCapturePreparations.set(token, {
browserId,
...(requestId ? { requestId } : {}),
preparesResidentHost,
webview,
});
try {
if (preparesResidentHost) {
applyResidentHostCaptureStyle(host);
raiseResidentWebviewForCapture(host, webview);
}
await waitForCapturePaint(webview);
if (!activeCapturePreparations.has(token)) {
throw new Error("Browser pixel capture preparation was canceled.");
}
return { token };
} catch (error) {
releaseCapturePreparationToken(token);
throw error;
}
}
export async function restoreResidentBrowserWebviewAfterPixelCapture(input: {
token: string;
}): Promise<void> {
releaseCapturePreparationToken(input.token);
}
export async function cancelResidentBrowserWebviewPixelCapture(input: {
requestId?: string;
token?: string;
}): Promise<void> {
const token = activeCaptureTokenFor(input);
if (!token) {
return;
}
releaseCapturePreparationToken(token);
}
export function installResidentBrowserCaptureBridge(): () => void {
captureBridgeInstallCount += 1;
if (!captureBridgeDisposer) {
const browserBridge = getDesktopHost()?.browser;
const disposePrepare = browserBridge?.onPrepareForPixelCapture?.(
prepareResidentBrowserWebviewForPixelCapture,
);
const disposeRestore = browserBridge?.onRestorePixelCapture?.(
restoreResidentBrowserWebviewAfterPixelCapture,
);
const disposeCancel = browserBridge?.onCancelPixelCapture?.(
cancelResidentBrowserWebviewPixelCapture,
);
captureBridgeDisposer = () => {
disposePrepare?.();
disposeRestore?.();
disposeCancel?.();
};
}
return () => {
captureBridgeInstallCount = Math.max(0, captureBridgeInstallCount - 1);
if (captureBridgeInstallCount > 0) {
return;
}
captureBridgeDisposer?.();
captureBridgeDisposer = null;
releaseAllCapturePreparations();
};
}
export function removeResidentBrowserWebview(browserId: string): void {
const normalizedBrowserId = trimNonEmpty(browserId);
if (!normalizedBrowserId) {
@@ -414,7 +174,6 @@ export function removeResidentBrowserWebview(browserId: string): void {
const resident = residentWebviewsByBrowserId.get(normalizedBrowserId) ?? null;
residentWebviewsByBrowserId.delete(normalizedBrowserId);
releaseCapturePreparationsForBrowser(normalizedBrowserId);
resident?.remove();
}
@@ -423,7 +182,5 @@ export function clearResidentBrowserWebviewsForTests(): void {
webview.remove();
}
residentWebviewsByBrowserId.clear();
releaseAllCapturePreparations();
nextCapturePreparationId = 0;
readDocument()?.getElementById(RESIDENT_BROWSER_HOST_ID)?.remove();
}