can1357/oh-my-pi · error · ToolError
No page target matched ${JSON.stringify(options.matcher)}. A
Error message
No page target matched ${JSON.stringify(options.matcher)}. Available pages:\n${summary} What it means
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.
Source
Thrown at packages/coding-agent/src/tools/browser/attach.ts:228
async function enrichPages(pages: Page[]): Promise<Array<{ page: Page; url: string; title: string }>> {
return await Promise.all(
pages.map(async page => ({
page,
url: page.url(),
title: ((await page.title().catch(() => "")) ?? "").trim(),
})),
);
}
async function pickPageFromList(pages: Page[], options: { matcher?: string; preferVisible?: boolean }): Promise<Page> {
const enriched = await enrichPages(pages);
if (options.matcher) {
const needle = options.matcher.toLowerCase();
const hit = enriched.find(p => p.url.toLowerCase().includes(needle) || p.title.toLowerCase().includes(needle));
if (hit) return hit.page;
const summary = enriched.map(p => `- ${p.title || "(untitled)"} ${p.url}`).join("\n");
throw new ToolError(`No page target matched ${JSON.stringify(options.matcher)}. Available pages:\n${summary}`);
}
const usable = enriched.filter(
p => !ATTACH_TARGET_SKIP_PATTERN.test(p.url) && !ATTACH_TARGET_SKIP_PATTERN.test(p.title),
);
if (options.preferVisible && usable.length > 1) {
// Best-effort foreground probe; a tab that cannot answer counts as hidden.
const visibility = await Promise.all(
usable.map(async p => {
try {
return (await p.page.evaluate(() => document.visibilityState === "visible")) === true;
} catch {
return false;
}
}),
);
const foreground = visibility.indexOf(true);
if (foreground >= 0) return usable[foreground]!.page;
}View on GitHub (pinned to 9690622007)
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
Example fix
// before
await pickPage({ matcher: "MyApp — Dashboard" }); // em-dash/case mismatch
// after
await pickPage({ matcher: "dashboard" }); // loose substring from the listed URLs Defensive patterns
Strategy: validation
Validate before calling
const pages = await fetch(`${cdpUrl}/json/list`).then(r => r.json());
const match = pages.find(p =>
(p.title + p.url).toLowerCase().includes(matcher.toLowerCase()));
if (!match) throw new Error(`matcher ${matcher} not found in: ${pages.map(p => p.url).join(", ")}`); Try / catch
try {
const tab = await pickPage({ matcher });
} catch (e) {
if (e instanceof ToolError && e.message.startsWith("No page target matched")) {
// message lists available pages — pick a substring from it and retry
} else throw e;
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Browser selector must be a string; got ${kind}. tab.click/ty
- tab.ariaSnapshot: selector ${__sel} matched no element
- No page targets available on the attached browser
- Element handle selector no longer resolves
- No element matched ${spec.raw}
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/6d7b522fdd168b41.
Report an issue: GitHub.