can1357/oh-my-pi · error · ToolError
Screenshot selector did not resolve to an element
Error message
Screenshot selector did not resolve to an element
What it means
When a screenshot is requested with a selector option, the worker resolves it via ARIA-ref (#resolveAriaRef) or a CSS query (page.$ after normalizeSelector). If neither yields an ElementHandle, it throws this ToolError instead of taking a viewport screenshot. The selector matched zero elements at call time.
Source
Thrown at packages/coding-agent/src/tools/browser/tab-worker.ts:1907
// compositor surface, which follows the *active* target: a backgrounded
// page can stall waiting for a fresh frame (the 20s screenshot timeouts)
// or hand back a sibling tab's pixels. Activate first; best-effort so an
// already-active or freshly-closed target never fails the capture.
//
// For a user-driven browser, redundant activation would steal window focus.
// The supervisor disables it only after adopting the visible tab; if the user
// later switches away, reject capture rather than risk sibling-tab pixels.
await preparePageForScreenshot(page, signal, this.#activateForScreenshot);
const fullPage = opts.selector ? false : (opts.fullPage ?? false);
const captureType = "png";
const captureMime = "image/png" as const;
let buffer: Buffer;
if (opts.selector) {
const handle =
parseAriaRefSelector(opts.selector) !== null
? await this.#resolveAriaRef(opts.selector)
: asElementHandle(await untilAborted(signal, () => page.$(normalizeSelector(opts.selector!))));
if (!handle) throw new ToolError("Screenshot selector did not resolve to an element");
try {
// Bring the element into view with a single instant scroll instead of puppeteer's
// scrollIntoViewIfNeeded(), whose IntersectionObserver promise can stall indefinitely
// on continuously-animating pages (WebGL / backdrop-filter "glass" effects). Best-effort.
await untilAborted(signal, () =>
handle.evaluate(el => {
const target = el as unknown as {
scrollIntoView: (opts: { behavior: string; block: string; inline: string }) => void;
};
target.scrollIntoView({ behavior: "instant", block: "center", inline: "center" });
}),
).catch(() => undefined);
// scrollIntoView:false skips the same IntersectionObserver check inside screenshot();
// captureBeyondViewport (puppeteer's default) still renders the clipped region.
const shotOpts: ElementScreenshotOptions = { type: captureType, scrollIntoView: false };
buffer = (await untilAborted(signal, () => handle.screenshot(shotOpts))) as Buffer;
} finally {
await handle.dispose().catch(() => undefined);View on GitHub (pinned to 9690622007)
Solutions
- Verify the selector matches exactly one element on the current page (test in DevTools with document.querySelector).
- Re-run tab.observe() to refresh aria refs, then use a fresh ref id.
- If the element is inside an iframe, target the frame or use a full-page/viewport screenshot instead.
- Fall back to a coordinates-based clip or a viewport screenshot when a selector cannot be stabilized.
Example fix
// before
await tab.screenshot({ selector: ".submit-btn" }); // class renamed
// after
await tab.observe();
const el = tab.elements.find(e => e.role === "button" && e.name === "Submit");
await tab.screenshot({ selector: el?.ariaRef ?? "button[type=submit]" }); Defensive patterns
Strategy: validation
Validate before calling
const found = await tab.evaluate((sel) => !!document.querySelector(sel), selector);
if (!found) throw new Error(`selector ${selector} matches nothing; refresh refs via observe()`); Type guard
function isSelector(s: string): boolean {
return typeof s === "string" && s.trim().length > 0 && !s.includes(",");
} Try / catch
try {
await tab.screenshot({ selector });
} catch (err) {
if (err.message.includes("did not resolve to an element")) {
await tab.observe();
return tab.screenshot({ selector: refreshedSelector });
}
throw err;
} Prevention
- Validate selectors against the live DOM before use
- Re-run tab.observe() to refresh aria refs after any page change
- Scope selectors to the correct frame/shadow root
- Fall back to viewport screenshots when selectors are unstable
When it happens
Trigger: tab.screenshot({ selector }) where the CSS selector matches nothing, or an aria-ref id was passed but the referenced element no longer exists in the cached observation list.
Common situations: Typo in a CSS selector; page content changed since observe() so the aria ref is gone; element lives inside an iframe the top-level page.$ cannot reach; a framework re-rendered and removed the node before the call.
Related errors
- No element matched ${spec.raw}
- Drag ${role} selector did not resolve: ${target}
- Browser selector must be a string; got ${kind}. tab.click/ty
- tab.ariaSnapshot: selector ${__sel} matched no element
- No page target matched ${JSON.stringify(options.matcher)}. A
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/b21392699a60b11c.
Report an issue: GitHub.