Skip to content

fix: detect and drop error-boilerplate text in fit_markdown pruning#110

Open
LuminDev wants to merge 4 commits into
mainfrom
fix/fit-markdown-error-boilerplate
Open

fix: detect and drop error-boilerplate text in fit_markdown pruning#110
LuminDev wants to merge 4 commits into
mainfrom
fix/fit-markdown-error-boilerplate

Conversation

@LuminDev

@LuminDev LuminDev commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes #108: navigate(..., format="fit_markdown") returned error boilerplate text (e.g. "You can't perform that action at this time") instead of actual page content on GitHub and some other SPAs.

Root Cause

The prune_html_with_profile() scoring algorithm uses text density, link density, tag weights, and class-name negative patterns. GitHub's error banners (with classes like flash-error, js-flash-alert) have moderate text density and low link density — they outscore real content despite containing only boilerplate, and their class names didn't match the existing NEGATIVE_PATTERNS array (nav, footer, sidebar, etc.).

Fix (three-layer defense)

  1. Error boilerplate text detection (prune.rs): New has_error_boilerplate_text() function checks for known error strings ("You can't perform that action at this time", "Uh oh! There was an error while loading", etc.) and drops matching elements outright during prune_node().

  2. Warning class patterns (prune.rs): New WARNING_CLASS_PATTERNS (blankslate, flash, toast, alert-banner) contribute a -0.3 scoring penalty per match via class_id_score(), deprioritizing error banners without full elimination.

  3. Navigate fallback (navigate.rs): resolve_content() now checks pruned fit_markdown output for error boilerplate. If detected and output is short (<500 chars), falls back to plain text content as a safety net.

Changes

  • crates/browser/src/prune.rs: +4 constants, +1 helper function, updated scoring
  • crates/agent/src/tools/navigate.rs: +1 constant, +1 helper, +2 modified branches, +3 tests

Test Plan

  • 174 browser unit tests pass
  • 32 navigate unit tests pass
  • 3 new prune tests: error_boilerplate_text_pruned, error_class_patterns_penalized, real_content_survives_with_error_banner
  • 3 new navigate tests: fit_markdown_falls_back_on_error_text, looks_like_error_boilerplate_detects_known_strings, looks_like_error_boilerplate_ignores_long_content
  • E2E: GitHub PR list page returns clean content via fit_markdown

Closes #108

Error banners (e.g. GitHub's "You can't perform that action at this time",
"Uh oh! There was an error while loading") could survive fit_markdown pruning
because their moderate text density and low link density outscored real content
despite having no negative class-name patterns.

Three-layer defense:
1. Error boilerplate text detection — elements whose visible text matches known
   error strings are dropped outright during prune_node().
2. Warning class patterns — "blankslate", "flash", "toast", "alert-banner"
   receive a -0.3 scoring penalty per match via class_id_score().
3. Navigate fallback — resolve_content() now checks for error boilerplate in
   the pruned output and falls back to plain text content when detected.

Includes unit tests for all three layers.

Closes #108
@Mingye-Lu

Copy link
Copy Markdown
Owner

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 44682f0646

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/browser/src/prune.rs Outdated
Comment on lines +147 to +148
if has_error_boilerplate_text(element) {
return None;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid dropping container subtrees for nested error banners

When a page wraps both a transient error banner and valid content in one short root element (common for SPA <div id="app">... pages, especially sparse issue/login/status pages), this check returns None for the whole container before recursion can prune only the banner. Because the 500-byte guard still allows these small containers, prune_html_with_profile can discard all real content; navigate(..., format="fit_markdown") then either falls back to raw unpruned text or returns empty instead of structured markdown.

Useful? React with 👍 / 👎.

Comment thread crates/browser/src/prune.rs Outdated
"an error occurred",
"this page is taking too long",
"there was an error loading",
"please try again",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not treat generic retry text as boilerplate

The standalone "please try again" matcher is too generic for an unconditional drop: any short legitimate article, form, or help text such as “If checkout fails, please try again” matches has_error_boilerplate_text and is removed whenever its containing element is under 500 bytes. This causes valid page content to disappear simply because it includes a common retry instruction, not because it is an error banner.

Useful? React with 👍 / 👎.

Comment on lines +255 to +257
ERROR_BOILERPLATE_PATTERNS
.iter()
.any(|pattern| lower.contains(pattern))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Normalize boilerplate text before matching

If the same boilerplate is split by normal template whitespace, such as You can't perform that action at this\n time, this exact contains check misses it because element.text().collect() is not whitespace-normalized before comparing with the single-space patterns. In that case the new pruning and navigate fallback both fail to recognize the short error banner, so the GitHub-style boilerplate this change is meant to remove can still be returned as fit_markdown.

Useful? React with 👍 / 👎.

Comment on lines +316 to 319
if (md.trim().is_empty() || looks_like_error_boilerplate(&md))
&& !text.trim().is_empty()
{
cap_content(text, max_chars)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not fall back to the same error-only text

When pruning successfully removes an error-only main section, md is empty, but this fallback returns text, which is derived from the same fetched HTML. For a page or extracted main area that only contains Something went wrong/GitHub error boilerplate, navigate(..., format="fit_markdown") still surfaces the boilerplate instead of the empty pruned result, so the safety net reintroduces the exact content the new pruning removed.

Useful? React with 👍 / 👎.

LuminDev added a commit that referenced this pull request Jul 7, 2026
…ers, generic retry text, whitespace normalization

- Use direct text nodes (not descendant text) in has_error_boilerplate_text
  to prevent dropping containers that wrap both an error banner and valid
  content. Only drop elements without element children.

- Remove 'please try again' from ERROR_BOILERPLATE_PATTERNS (too generic,
  caused false positives on legitimate content containing retry text).

- Normalize whitespace before boilerplate pattern matching so multi-line
  text like 'you can't perform that action at this
  time' matches single-space patterns.
LuminDev added a commit that referenced this pull request Jul 7, 2026
…ning already removed it

When fit_markdown pruning strips error banners from the main HTML
section and md is empty (or looks_like_error_boilerplate), the old
fallback returned the raw text — which is derived from the same HTML
and still contains the same error boilerplate. The safety net
reintroduced the exact content the pruning removed.

Now the fallback also checks !looks_like_error_boilerplate(text),
so error pages that produce nothing but boilerplate return the
(empty) pruned md instead of surfacing the raw error text.

Closes: review comment on PR #110 (comment 3530817196)
LuminDev added 2 commits July 11, 2026 14:30
…hitespace normalization

- Use direct text nodes (not descendant text) in has_error_boilerplate_text
  to prevent dropping containers that wrap both an error banner and valid
  content. Only drop elements without element children.

- Remove 'please try again' from ERROR_BOILERPLATE_PATTERNS (too generic,
  caused false positives on legitimate content containing retry text).

- Normalize whitespace before boilerplate pattern matching so multi-line
  text like 'you can't perform that action at this
  time' matches single-space patterns.
…ning already removed it

When fit_markdown pruning strips error banners from the main HTML
section and md is empty (or looks_like_error_boilerplate), the old
fallback returned the raw text — which is derived from the same HTML
and still contains the same error boilerplate. The safety net
reintroduced the exact content the pruning removed.

Now the fallback also checks !looks_like_error_boilerplate(text),
so error pages that produce nothing but boilerplate return the
(empty) pruned md instead of surfacing the raw error text.

Closes: review comment on PR #110 (comment 3530817196)
@Mingye-Lu
Mingye-Lu force-pushed the fix/fit-markdown-error-boilerplate branch from e69f1df to c4ad133 Compare July 11, 2026 06:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

navigate fit_markdown returns error boilerplate instead of page content on SPAs

2 participants