diff --git a/frontend/src/shared/browserCommandHandler.ts b/frontend/src/shared/browserCommandHandler.ts index b6f31d55..f9a580d1 100644 --- a/frontend/src/shared/browserCommandHandler.ts +++ b/frontend/src/shared/browserCommandHandler.ts @@ -57,10 +57,26 @@ export function getActionLabel(action: string): string { } async function handleScreenshot(wv: BrowserWebview): Promise> { - const nativeImage = await wv.capturePage(); - const dataUrl = nativeImage.toDataURL(); - const base64 = dataUrl.replace(/^data:image\/\w+;base64,/, ''); - return { image: base64, url: wv.getURL(), title: wv.getTitle() }; + // capturePage throws UnknownVizError if the webview hasn't composited a frame + // yet (the Viz compositor races the first paint, reliably bit turn-0 captures). + // Retry a few times with a short backoff so a cold first screenshot succeeds + // instead of burning a whole agent turn on a transient error. + let lastErr: any; + for (let attempt = 0; attempt < 4; attempt++) { + try { + const nativeImage = await wv.capturePage(); + if (!nativeImage.isEmpty()) { + const dataUrl = nativeImage.toDataURL(); + const base64 = dataUrl.replace(/^data:image\/\w+;base64,/, ''); + return { image: base64, url: wv.getURL(), title: wv.getTitle() }; + } + lastErr = new Error('capturePage returned an empty image (frame not painted yet)'); + } catch (err: any) { + lastErr = err; + } + await new Promise((r) => setTimeout(r, 250 * (attempt + 1))); + } + return { error: `Screenshot failed after retries: ${lastErr?.message || String(lastErr)}` }; } async function handleGetText(wv: BrowserWebview): Promise> { diff --git a/frontend/src/shared/browserRegistry.ts b/frontend/src/shared/browserRegistry.ts index 604508e5..a60a8f70 100644 --- a/frontend/src/shared/browserRegistry.ts +++ b/frontend/src/shared/browserRegistry.ts @@ -11,6 +11,7 @@ export interface BrowserWebview extends HTMLElement { capturePage: (rect?: { x: number; y: number; width: number; height: number }) => Promise<{ toDataURL: () => string; toPNG: () => Buffer; + isEmpty: () => boolean; }>; executeJavaScript: (code: string) => Promise; sendInputEvent: (event: any) => void;