From f10442e284b385ef00ec1c7edf9bfcd0f5cd4c86 Mon Sep 17 00:00:00 2001 From: alex-SOLTIA Date: Fri, 6 Mar 2026 22:47:43 +0100 Subject: [PATCH] fix: prevent screenshot --annotate from hanging on complex pages On pages with many interactive elements, `screenshot --annotate` would fire hundreds of `boundingBox()` calls simultaneously via `Promise.all`, saturating Chrome's CDP connection and causing timeouts/hangs. Fix: process elements in batches of 20 with a 1-second timeout per element. Elements that time out are skipped (treated as null) rather than blocking the entire operation. Closes #509 Co-Authored-By: Claude Opus 4.6 --- src/actions.ts | 62 ++++++++++++++++++++++++++++++-------------------- 1 file changed, 37 insertions(+), 25 deletions(-) diff --git a/src/actions.ts b/src/actions.ts index 681a33e9..27619e60 100644 --- a/src/actions.ts +++ b/src/actions.ts @@ -758,31 +758,43 @@ async function handleScreenshot( const { refs } = await browser.getSnapshot({ interactive: true }); const entries = Object.entries(refs); - const results = await Promise.all( - entries.map(async ([ref, data]): Promise => { - try { - const locator = browser.getLocatorFromRef(ref); - if (!locator) return null; - const box = await locator.boundingBox(); - if (!box || box.width === 0 || box.height === 0) return null; - const num = parseInt(ref.replace('e', ''), 10); - return { - ref, - number: num, - role: data.role, - name: data.name || undefined, - box: { - x: Math.round(box.x), - y: Math.round(box.y), - width: Math.round(box.width), - height: Math.round(box.height), - }, - }; - } catch { - return null; - } - }) - ); + + // Process in batches to avoid saturating Chrome with CDP calls (#509) + const BATCH_SIZE = 20; + const ELEMENT_TIMEOUT = 1000; + const results: (Annotation | null)[] = []; + for (let i = 0; i < entries.length; i += BATCH_SIZE) { + const batch = entries.slice(i, i + BATCH_SIZE); + const batchResults = await Promise.all( + batch.map(async ([ref, data]): Promise => { + try { + const locator = browser.getLocatorFromRef(ref); + if (!locator) return null; + const box = await Promise.race([ + locator.boundingBox(), + new Promise((resolve) => setTimeout(() => resolve(null), ELEMENT_TIMEOUT)), + ]); + if (!box || box.width === 0 || box.height === 0) return null; + const num = parseInt(ref.replace('e', ''), 10); + return { + ref, + number: num, + role: data.role, + name: data.name || undefined, + box: { + x: Math.round(box.x), + y: Math.round(box.y), + width: Math.round(box.width), + height: Math.round(box.height), + }, + }; + } catch { + return null; + } + }) + ); + results.push(...batchResults); + } // When a selector is provided the screenshot is cropped to that element, // so filter to annotations that overlap the target and shift coordinates.