{"record":{"id":"6d7b522fdd168b41","repo":"can1357/oh-my-pi","slug":"no-page-target-matched-json-stringify-options-ma","errorCode":null,"errorMessage":"No page target matched ${JSON.stringify(options.matcher)}. Available pages:\\n${summary}","messagePattern":"No page target matched (.+?)\\. Available pages:\\\\n(.+?)","errorType":"exception","errorClass":"ToolError","httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/tools/browser/attach.ts","lineNumber":228,"sourceCode":"\nasync function enrichPages(pages: Page[]): Promise<Array<{ page: Page; url: string; title: string }>> {\n\treturn await Promise.all(\n\t\tpages.map(async page => ({\n\t\t\tpage,\n\t\t\turl: page.url(),\n\t\t\ttitle: ((await page.title().catch(() => \"\")) ?? \"\").trim(),\n\t\t})),\n\t);\n}\n\nasync function pickPageFromList(pages: Page[], options: { matcher?: string; preferVisible?: boolean }): Promise<Page> {\n\tconst enriched = await enrichPages(pages);\n\tif (options.matcher) {\n\t\tconst needle = options.matcher.toLowerCase();\n\t\tconst hit = enriched.find(p => p.url.toLowerCase().includes(needle) || p.title.toLowerCase().includes(needle));\n\t\tif (hit) return hit.page;\n\t\tconst summary = enriched.map(p => `- ${p.title || \"(untitled)\"}  ${p.url}`).join(\"\\n\");\n\t\tthrow new ToolError(`No page target matched ${JSON.stringify(options.matcher)}. Available pages:\\n${summary}`);\n\t}\n\tconst usable = enriched.filter(\n\t\tp => !ATTACH_TARGET_SKIP_PATTERN.test(p.url) && !ATTACH_TARGET_SKIP_PATTERN.test(p.title),\n\t);\n\tif (options.preferVisible && usable.length > 1) {\n\t\t// Best-effort foreground probe; a tab that cannot answer counts as hidden.\n\t\tconst visibility = await Promise.all(\n\t\t\tusable.map(async p => {\n\t\t\t\ttry {\n\t\t\t\t\treturn (await p.page.evaluate(() => document.visibilityState === \"visible\")) === true;\n\t\t\t\t} catch {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t}),\n\t\t);\n\t\tconst foreground = visibility.indexOf(true);\n\t\tif (foreground >= 0) return usable[foreground]!.page;\n\t}","sourceCodeStart":210,"sourceCodeEnd":246,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/tools/browser/attach.ts#L210-L246","documentation":"pickPageFromList matches an optional matcher string against each page's URL and title (case-insensitive substring). If a matcher is supplied and no page matches, it throws ToolError listing JSON.stringify(matcher) plus every available page as \"- title  url\" so the caller can pick a correct needle. Without a matcher the function instead skips non-usable targets.","triggerScenarios":"Calling page-attach with options.matcher set to a substring that appears in no page URL or title; matcher casing/encoding mismatch (the compare lowercases both sides); matching against a page that closed between enumeration and match; matcher pointing at devtools/extension pages that were skipped from the usable list but present in the summary.","commonSituations":"Assuming title contains an app name it doesn't; URL uses a different host/port/path than the matcher; SPA route changed after load; typing a regex instead of a substring — matcher is a plain substring, not a pattern.","solutions":["Read the \"Available pages\" list in the error and copy an exact URL/title substring as the matcher","Use a shorter, distinctive substring (e.g. host or route) rather than an exact full URL","Re-enumerate pages — the target list may have changed (tabs opened/closed)","Drop the matcher to let the tool pick the best usable page automatically","Remember matching is substring + lowercase: avoid regex syntax and case-sensitive assumptions"],"exampleFix":"// before\nawait pickPage({ matcher: \"MyApp — Dashboard\" }); // em-dash/case mismatch\n// after\nawait pickPage({ matcher: \"dashboard\" }); // loose substring from the listed URLs","handlingStrategy":"validation","validationCode":"const pages = await fetch(`${cdpUrl}/json/list`).then(r => r.json());\nconst match = pages.find(p =>\n  (p.title + p.url).toLowerCase().includes(matcher.toLowerCase()));\nif (!match) throw new Error(`matcher ${matcher} not found in: ${pages.map(p => p.url).join(\", \")}`);","typeGuard":null,"tryCatchPattern":"try {\n  const tab = await pickPage({ matcher });\n} catch (e) {\n  if (e instanceof ToolError && e.message.startsWith(\"No page target matched\")) {\n    // message lists available pages — pick a substring from it and retry\n  } else throw e;\n}","preventionTips":["Use short distinctive substrings (host, route) not full exact titles","Remember matching is case-insensitive substring — no regex, watch punctuation/em-dashes","Re-enumerate targets before matching; tabs change dynamically","Omit matcher to auto-pick the best usable page when you don't care which"],"tags":["browser","electron","selector","matching"],"backgroundTag":"no-page-matches-filter","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}