{"record":{"id":"45bafc29058bfcec","repo":"can1357/oh-my-pi","slug":"tab-extract-json-stringify-format-produced-em","errorCode":null,"errorMessage":"tab.extract(${JSON.stringify(format)}) produced empty ${format} content for ${url}","messagePattern":"tab\\.extract\\((.+?)\\) produced empty (.+?) content for (.+?)","errorType":"exception","errorClass":"ToolError","httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/tools/browser/cmux/cmux-tab.ts","lineNumber":547,"sourceCode":"\n\tasync select(selector: string, ...values: string[]): Promise<string[]> {\n\t\treturn await this.#selectorAction<string[]>(selector, \"select\", { values });\n\t}\n\n\tasync extract(format: ReadableFormat = \"markdown\"): Promise<string> {\n\t\tconst result = (await this.#request(\"browser.snapshot\", { interactive: false })) as CmuxSnapshotResult;\n\t\tconst html = typeof result.page?.html === \"string\" ? result.page.html : \"\";\n\t\tconst url =\n\t\t\t(typeof result.url === \"string\" && result.url.length > 0 ? result.url : undefined) ??\n\t\t\t(typeof result.page?.url === \"string\" && result.page.url.length > 0 ? result.page.url : undefined) ??\n\t\t\tthis.#lastUrl;\n\t\tconst readable = await extractReadableFromHtml(html, url, format);\n\t\tif (!readable) {\n\t\t\tthrow new ToolError(`tab.extract(${JSON.stringify(format)}) found no readable content on ${url}`);\n\t\t}\n\t\tconst content = format === \"markdown\" ? readable.markdown : readable.text;\n\t\tif (!content) {\n\t\t\tthrow new ToolError(`tab.extract(${JSON.stringify(format)}) produced empty ${format} content for ${url}`);\n\t\t}\n\t\treturn content;\n\t}\n\n\tasync screenshot(opts: ScreenshotOptions = {}): Promise<string> {\n\t\tconst context = this.#requireRunContext(\"tab.screenshot()\");\n\t\t// The cmux daemon's `browser.screenshot` captures the surface viewport\n\t\t// only — it has no element-clip or full-page mode, and Bun.Image cannot\n\t\t// crop locally. Degrade transparently instead of silently mislabeling\n\t\t// the capture: scroll the element into view, then TELL the model the\n\t\t// image is the full viewport (reports showed selector captures being\n\t\t// consumed as element crops).\n\t\tconst captureNotes: string[] = [];\n\t\tif (opts.selector) {\n\t\t\tawait this.scrollIntoView(opts.selector);\n\t\t\tcaptureNotes.push(\n\t\t\t\t`selector ${JSON.stringify(opts.selector)} was scrolled into view, but this surface cannot clip to an element — the image is the full viewport`,\n\t\t\t);","sourceCodeStart":529,"sourceCodeEnd":565,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/tools/browser/cmux/cmux-tab.ts#L529-L565","documentation":"CmuxTab.extract() threw this ToolError because the readability extractor produced a result object but the requested format's field (readable.markdown or readable.text) was empty/undefined. The page had extractable structure, yet the converter emitted zero characters for the requested output format.","triggerScenarios":"Calling tab.extract('text') (or 'markdown') on a page whose readable content converts to an empty string — e.g. content that is only images/embeds with no text nodes, or an extractor bug where markdown conversion drops all nodes while text extraction would succeed (or vice versa).","commonSituations":"Image-only pages (galleries, canvas-heavy apps) where there is genuinely no text; pages whose entire body is one <img> or <video>; extraction formats where the readability library's markdown serializer silently drops certain node types.","solutions":["Try the other format (tab.extract('text') vs tab.extract('markdown')) — one serializer may produce content where the other returns empty.","Verify the page actually has text by running tab.evaluate(() => document.body.innerText.length) before extracting.","Fall back to tab.evaluate() with document.body.innerText or a targeted selector's textContent for image-only or oddly structured pages.","Ensure the page is fully loaded and scrolled (lazy-loaded content may not be in the snapshot HTML)."],"exampleFix":"// before: single format, fails on image-heavy page\nconst md = await tab.extract(\"markdown\");\n// after: fall back across formats and to raw text\nlet content: string;\ntry {\n  content = await tab.extract(\"markdown\");\n} catch {\n  content = await tab.extract(\"text\").catch(() => tab.evaluate(() => document.body.innerText));\n}","handlingStrategy":"fallback","validationCode":"const textLen = await tab.evaluate(() => document.body.innerText.length);\nif (textLen === 0) throw new Error(\"page contains no text; extraction will be empty\");","typeGuard":null,"tryCatchPattern":"let content: string;\ntry {\n  content = await tab.extract(\"markdown\");\n} catch (err) {\n  if (err instanceof ToolError && err.message.includes(\"produced empty\")) {\n    content = await tab.extract(\"text\");\n  } else throw err;\n}","preventionTips":["Verify the page has text nodes via document.body.innerText before choosing extraction","Fall back across formats (markdown → text → raw innerText) instead of failing","Expect empty output on image/canvas-only pages and handle that case explicitly","Ensure lazy-loaded content is scrolled into the DOM before snapshotting"],"tags":["browser","content-extraction","empty-content","readability"],"backgroundTag":"empty-extraction-output","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}