Skip to content

Commit 7e23f5a

Browse files
committed
Reject blank full-page background captures
1 parent 98fd4d7 commit 7e23f5a

2 files changed

Lines changed: 106 additions & 12 deletions

File tree

src/chrome/src/agent/agent.js

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -777,19 +777,42 @@ export class Agent extends LoopDetector {
777777
}
778778
}
779779

780+
async _captureFullPageWithBlankRetry(tabId, capturePolicy) {
781+
const probe = await this._captureViewportProbe(tabId);
782+
const captureOnce = async () => {
783+
const capture = await this._withIndicatorsHidden(tabId, () =>
784+
cdpClient.captureFullPageScreenshot(tabId, capturePolicy)
785+
);
786+
const imageData = typeof capture === 'string' ? capture : capture?.data;
787+
if (!imageData) return null;
788+
return {
789+
dataUrl: `data:image/png;base64,${imageData}`,
790+
capture,
791+
};
792+
};
793+
return this._retryBlankScreenshotCapture(
794+
await captureOnce(),
795+
captureOnce,
796+
{ probe },
797+
);
798+
}
799+
780800
async captureFullPageScreenshotForUser(tabId) {
781801
if (!tabId) return { ok: false, error: 'No tab ID' };
782802
try {
783803
const capturePolicy = await this._getFullPageCapturePolicy(tabId);
784804
await cdpClient.attach(tabId);
785805
await this._preparePageForCapture(tabId);
786-
const capture = await this._withIndicatorsHidden(tabId, () =>
787-
cdpClient.captureFullPageScreenshot(tabId, capturePolicy)
788-
);
789-
const imageData = typeof capture === 'string' ? capture : capture?.data;
806+
const captured = await this._captureFullPageWithBlankRetry(tabId, capturePolicy);
807+
const capture = captured?.capture;
790808
const warning = typeof capture === 'object' ? capture?.warning || null : null;
791-
if (!imageData) return { ok: false, error: 'Full-page screenshot returned no image data' };
792-
return { ok: true, dataUrl: `data:image/png;base64,${imageData}`, warning };
809+
if (!captured?.dataUrl) {
810+
return { ok: false, error: 'Full-page screenshot returned no image data' };
811+
}
812+
if (captured?.blankFrameRetry?.finalBlank) {
813+
return { ok: false, error: 'Background full-page screenshot remained blank after retries' };
814+
}
815+
return { ok: true, dataUrl: captured.dataUrl, warning };
793816
} catch (e) {
794817
return { ok: false, error: e?.message || String(e) };
795818
}
@@ -15051,14 +15074,15 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
1505115074
const capturePolicy = await this._getFullPageCapturePolicy(tabId);
1505215075
await cdpClient.attach(tabId);
1505315076
await this._preparePageForCapture(tabId);
15054-
const capture = await this._withIndicatorsHidden(tabId, () =>
15055-
cdpClient.captureFullPageScreenshot(tabId, capturePolicy)
15056-
);
15057-
const imageData = typeof capture === 'string' ? capture : capture?.data;
15077+
const captured = await this._captureFullPageWithBlankRetry(tabId, capturePolicy);
15078+
const capture = captured?.capture;
1505815079
const captureWarning = typeof capture === 'object' ? capture?.warning || null : null;
1505915080
const captureBounds = typeof capture === 'object' ? capture?.captureBounds || null : null;
15060-
if (!imageData) throw new Error('Full-page screenshot returned no image data');
15061-
const rawUrl = `data:image/png;base64,${imageData}`;
15081+
if (!captured?.dataUrl) throw new Error('Full-page screenshot returned no image data');
15082+
if (captured?.blankFrameRetry?.finalBlank) {
15083+
throw new Error('Background full-page screenshot remained blank after retries');
15084+
}
15085+
const rawUrl = captured.dataUrl;
1506215086
const warningNote = captureWarning ? `\nWarning: ${captureWarning}` : '';
1506315087

1506415088
// If the caller asked to save, do it with the RAW (uncompressed,

test/run.js

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25502,6 +25502,76 @@ test('user full-page screenshot responses preserve compositor fallback warnings'
2550225502
}
2550325503
});
2550425504

25505+
test('Chrome full-page screenshot paths reject blank background captures after retries', async () => {
25506+
const originalChrome = globalThis.chrome;
25507+
const originalAttach = cdpClientCh.attach;
25508+
const originalCapture = cdpClientCh.captureFullPageScreenshot;
25509+
const originalDelays = AgentCh.BLANK_SCREENSHOT_RETRY_DELAYS_MS;
25510+
let captureCalls = 0;
25511+
try {
25512+
globalThis.chrome = {
25513+
...(originalChrome || {}),
25514+
tabs: {
25515+
...(originalChrome?.tabs || {}),
25516+
get: async (tabId) => ({
25517+
id: tabId,
25518+
active: false,
25519+
url: 'https://example.test/background',
25520+
}),
25521+
},
25522+
};
25523+
cdpClientCh.attach = async () => ({ attached: true });
25524+
cdpClientCh.captureFullPageScreenshot = async () => {
25525+
captureCalls += 1;
25526+
return { data: 'Ymxhbms=' };
25527+
};
25528+
AgentCh.BLANK_SCREENSHOT_RETRY_DELAYS_MS = [0];
25529+
25530+
const agent = new AgentCh({
25531+
getActive: () => ({ supportsVision: true }),
25532+
getVisionProvider: async () => null,
25533+
});
25534+
agent._preparePageForCapture = async () => {};
25535+
agent._withIndicatorsHidden = async (_tabId, capture) => capture();
25536+
agent._captureViewportProbe = async () => ({
25537+
readyState: 'complete',
25538+
documentTextChars: 200,
25539+
visibleTextChars: 100,
25540+
domNodes: 50,
25541+
imageCount: 0,
25542+
scrollHeight: 1200,
25543+
innerHeight: 800,
25544+
});
25545+
agent._analyzeScreenshotBlankness = async () => ({
25546+
blank: true,
25547+
reason: 'near-all-white frame',
25548+
meanLuma: 255,
25549+
lumaStdDev: 0,
25550+
whiteRatio: 1,
25551+
blackRatio: 0,
25552+
});
25553+
25554+
const userResult = await agent.captureFullPageScreenshotForUser(42);
25555+
assert.deepEqual(userResult, {
25556+
ok: false,
25557+
error: 'Background full-page screenshot remained blank after retries',
25558+
});
25559+
25560+
const toolResult = await agent.executeTool(42, 'full_page_screenshot', {});
25561+
assert.deepEqual(toolResult, {
25562+
success: false,
25563+
error: 'Full page screenshot failed: Background full-page screenshot remained blank after retries',
25564+
});
25565+
assert.equal(captureCalls, 4, 'each full-page path should retry once before rejecting the blank frame');
25566+
} finally {
25567+
AgentCh.BLANK_SCREENSHOT_RETRY_DELAYS_MS = originalDelays;
25568+
cdpClientCh.attach = originalAttach;
25569+
cdpClientCh.captureFullPageScreenshot = originalCapture;
25570+
if (originalChrome === undefined) delete globalThis.chrome;
25571+
else globalThis.chrome = originalChrome;
25572+
}
25573+
});
25574+
2550525575
test('user full-page screenshots apply adapter capture policy without LLM adapter injection', async () => {
2550625576
const originalChrome = globalThis.chrome;
2550725577
const originalAttach = cdpClientCh.attach;

0 commit comments

Comments
 (0)