can1357/oh-my-pi · error · ToolError
tab.extract( ) found no readable content on
Error message
tab.extract(${JSON.stringify(format)}) found no readable content on ${url} What it means
CmuxTab.extract() snapshots the current page HTML and runs it through a readability extractor (extractReadableFromHtml). This ToolError is thrown when the extractor returns null — i.e. the readability algorithm could not identify any article/main content in the page's HTML. The cmux browser snapshot returned HTML that either was empty or looked like boilerplate (scripts, iframes, empty shell) with no extractable body.
Solutions
- Wait for the page to finish loading first (tab.waitForNavigation() or tab.waitForSelector() on a known content element) before calling tab.extract().
- Call tab.waitFor('<content selector>') to ensure the app has rendered, then retry extract().
- Fall back to tab.evaluate() with a custom extraction function (e.g. document.body.innerText) when the readability algorithm fails on a valid page.
- Check the URL the tab actually ended up on (tab.waitForUrl or a snapshot) — you may be extracting from a login/error interstitial instead of the target page.
Example fix
// before: extract immediately after goto on a slow SPA
await tab.goto(url);
const md = await tab.extract();
// after: wait for rendered content first
await tab.goto(url);
await tab.waitForSelector("main article");
const md = await tab.extract(); Defensive patterns
Strategy: fallback
Validate before calling
// ensure there is HTML to extract and the page has rendered
await tab.waitForSelector("body");
const hasHtml = await tab.evaluate(() => document.body.innerText.trim().length > 0);
if (!hasHtml) throw new Error("page has no text content to extract"); Try / catch
let content: string;
try {
content = await tab.extract("markdown");
} catch (err) {
if (err instanceof ToolError && err.message.includes("found no readable content")) {
content = await tab.evaluate(() => document.body.innerText);
} else throw err;
} Prevention
- Always wait for a known content selector before extracting on SPAs
- Prefer tab.evaluate-based extraction for non-article layouts (dashboards, apps)
- Check the final URL after navigation to rule out interstitial/error pages
- Remember readability ignores iframe content — hoist or bypass iframes
When it happens
Trigger: Calling tab.extract() (default 'markdown' or 'text' format) when the snapshot's page.html is empty, when the page is a JS-rendered SPA whose DOM the daemon snapshotted before content rendered, when the page is mostly iframes/embeds (readability skips them), or when the document is boilerplate-only (login walls, consent interstitials, error pages).
Common situations: Extracting from single-page apps that hydrate slowly; pages behind Cloudflare/bot-check interstitials; pages whose content lives in an <iframe>; navigating to a URL that failed and landed on a blank or error page; PDFs or non-HTML documents where the snapshot yields no HTML body.
Related errors
- tab.extract( ) produced empty content for
- Accessibility snapshot unavailable
- app.path must be absolute
- autoStarted ? `omp browser relay is serving at
- browser app.cdp_url must be the HTTP CDP discovery endpoint…
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/5cc27673e202d8a6.
Report an issue: GitHub.
Appendix: source
Thrown at packages/coding-agent/src/tools/browser/cmux/cmux-tab.ts:543
async scrollIntoView(selector: string): Promise<void> {
await this.#selectorAction(selector, "scrollIntoView");
}
async select(selector: string, ...values: string[]): Promise<string[]> {
return await this.#selectorAction<string[]>(selector, "select", { values });
}
async extract(format: ReadableFormat = "markdown"): Promise<string> {
const result = (await this.#request("browser.snapshot", { interactive: false })) as CmuxSnapshotResult;
const html = typeof result.page?.html === "string" ? result.page.html : "";
const url =
(typeof result.url === "string" && result.url.length > 0 ? result.url : undefined) ??
(typeof result.page?.url === "string" && result.page.url.length > 0 ? result.page.url : undefined) ??
this.#lastUrl;
const readable = await extractReadableFromHtml(html, url, format);
if (!readable) {
throw new ToolError(`tab.extract(${JSON.stringify(format)}) found no readable content on ${url}`);
}
const content = format === "markdown" ? readable.markdown : readable.text;
if (!content) {
throw new ToolError(`tab.extract(${JSON.stringify(format)}) produced empty ${format} content for ${url}`);
}
return content;
}
async screenshot(opts: ScreenshotOptions = {}): Promise<string> {
const context = this.#requireRunContext("tab.screenshot()");
// The cmux daemon's `browser.screenshot` captures the surface viewport
// only — it has no element-clip or full-page mode, and Bun.Image cannot
// crop locally. Degrade transparently instead of silently mislabeling
// the capture: scroll the element into view, then TELL the model the
// image is the full viewport (reports showed selector captures being
// consumed as element crops).
const captureNotes: string[] = [];
if (opts.selector) {View on GitHub (pinned to 9690622007)