{"record":{"id":"b21392699a60b11c","repo":"can1357/oh-my-pi","slug":"screenshot-selector-did-not-resolve-to-an-element","errorCode":null,"errorMessage":"Screenshot selector did not resolve to an element","messagePattern":"Screenshot selector did not resolve to an element","errorType":"exception","errorClass":"ToolError","httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/tools/browser/tab-worker.ts","lineNumber":1907,"sourceCode":"\t\t// compositor surface, which follows the *active* target: a backgrounded\n\t\t// page can stall waiting for a fresh frame (the 20s screenshot timeouts)\n\t\t// or hand back a sibling tab's pixels. Activate first; best-effort so an\n\t\t// already-active or freshly-closed target never fails the capture.\n\t\t//\n\t\t// For a user-driven browser, redundant activation would steal window focus.\n\t\t// The supervisor disables it only after adopting the visible tab; if the user\n\t\t// later switches away, reject capture rather than risk sibling-tab pixels.\n\t\tawait preparePageForScreenshot(page, signal, this.#activateForScreenshot);\n\t\tconst fullPage = opts.selector ? false : (opts.fullPage ?? false);\n\t\tconst captureType = \"png\";\n\t\tconst captureMime = \"image/png\" as const;\n\t\tlet buffer: Buffer;\n\t\tif (opts.selector) {\n\t\t\tconst handle =\n\t\t\t\tparseAriaRefSelector(opts.selector) !== null\n\t\t\t\t\t? await this.#resolveAriaRef(opts.selector)\n\t\t\t\t\t: asElementHandle(await untilAborted(signal, () => page.$(normalizeSelector(opts.selector!))));\n\t\t\tif (!handle) throw new ToolError(\"Screenshot selector did not resolve to an element\");\n\t\t\ttry {\n\t\t\t\t// Bring the element into view with a single instant scroll instead of puppeteer's\n\t\t\t\t// scrollIntoViewIfNeeded(), whose IntersectionObserver promise can stall indefinitely\n\t\t\t\t// on continuously-animating pages (WebGL / backdrop-filter \"glass\" effects). Best-effort.\n\t\t\t\tawait untilAborted(signal, () =>\n\t\t\t\t\thandle.evaluate(el => {\n\t\t\t\t\t\tconst target = el as unknown as {\n\t\t\t\t\t\t\tscrollIntoView: (opts: { behavior: string; block: string; inline: string }) => void;\n\t\t\t\t\t\t};\n\t\t\t\t\t\ttarget.scrollIntoView({ behavior: \"instant\", block: \"center\", inline: \"center\" });\n\t\t\t\t\t}),\n\t\t\t\t).catch(() => undefined);\n\t\t\t\t// scrollIntoView:false skips the same IntersectionObserver check inside screenshot();\n\t\t\t\t// captureBeyondViewport (puppeteer's default) still renders the clipped region.\n\t\t\t\tconst shotOpts: ElementScreenshotOptions = { type: captureType, scrollIntoView: false };\n\t\t\t\tbuffer = (await untilAborted(signal, () => handle.screenshot(shotOpts))) as Buffer;\n\t\t\t} finally {\n\t\t\t\tawait handle.dispose().catch(() => undefined);","sourceCodeStart":1889,"sourceCodeEnd":1925,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/tools/browser/tab-worker.ts#L1889-L1925","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nawait tab.screenshot({ selector: \".submit-btn\" }); // class renamed\n// after\nawait tab.observe();\nconst el = tab.elements.find(e => e.role === \"button\" && e.name === \"Submit\");\nawait tab.screenshot({ selector: el?.ariaRef ?? \"button[type=submit]\" });","handlingStrategy":"validation","validationCode":"const found = await tab.evaluate((sel) => !!document.querySelector(sel), selector);\nif (!found) throw new Error(`selector ${selector} matches nothing; refresh refs via observe()`);","typeGuard":"function isSelector(s: string): boolean {\n  return typeof s === \"string\" && s.trim().length > 0 && !s.includes(\",\");\n}","tryCatchPattern":"try {\n  await tab.screenshot({ selector });\n} catch (err) {\n  if (err.message.includes(\"did not resolve to an element\")) {\n    await tab.observe();\n    return tab.screenshot({ selector: refreshedSelector });\n  }\n  throw err;\n}","preventionTips":["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"],"tags":["browser","selector","screenshot","element-not-found"],"backgroundTag":"selector-did-not-resolve","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}