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

  1. Verify the selector matches exactly one element on the current page (test in DevTools with document.querySelector).
  2. Re-run tab.observe() to refresh aria refs, then use a fresh ref id.
  3. If the element is inside an iframe, target the frame or use a full-page/viewport screenshot instead.
  4. 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

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


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/b21392699a60b11c. Report an issue: GitHub.