Skip to content

Fix raw </pre> handling in response renderer#1150

Closed
bsgdigital wants to merge 2 commits intonesquena:masterfrom
bsgdigital:fix/pre-tag-response-parsing
Closed

Fix raw </pre> handling in response renderer#1150
bsgdigital wants to merge 2 commits intonesquena:masterfrom
bsgdigital:fix/pre-tag-response-parsing

Conversation

@bsgdigital
Copy link
Copy Markdown
Contributor

@bsgdigital bsgdigital commented Apr 27, 2026

Bug Description

Fixes response rendering where raw HTML code blocks could consume following content.

When model output included raw <pre><code>...</code></pre>, later message content could be rendered as if still inside a code box. This was especially visible when multiple code boxes appeared in one response.

Root Cause

renderMd() in static/ui.js performs a safe inline-HTML normalization pass that rewrites <code>...</code> to markdown backticks.

That rewrite was applied globally, including inside raw <pre> blocks coming from model output. For multiline code inside <pre><code>...</code></pre>, this could degrade block structure (introducing stray backticks and malformed code-box boundaries), which then cascaded into subsequent content rendering incorrectly.

Fix

Stash raw <pre>...</pre> blocks before the inline <code> normalization pass, then restore them immediately after.

// Before
s = s.replace(/<code>([^<]*?)<\/code>/gi, (_, t) => '`' + t + '`');

// After
const rawPreStash = [];
s = s.replace(/(<pre\b[^>]*>[\s\S]*?<\/pre>)/gi, m => {
  rawPreStash.push(m);
  return `\x00R${rawPreStash.length - 1}\x00`;
});
s = s.replace(/<code>([^<]*?)<\/code>/gi, (_, t) => '`' + t + '`');
s = s.replace(/\x00R(\d+)\x00/g, (_, i) => rawPreStash[+i]);

This is a minimal, targeted change: only raw <pre> regions are protected from the inline <code> rewrite; the rest of the markdown pipeline remains unchanged.

Files Changed

  • static/ui.js — protect raw <pre> blocks during inline HTML normalization
  • tests/test_renderer_js_behaviour.py — add regression test for multiple multiline raw <pre><code> blocks

How to Verify

Send output that contains multiple raw HTML code blocks, e.g.:

"Return two raw HTML code blocks using <pre><code>...</code></pre>, each multiline, with plain text between and after them."

Before fix: code-box boundaries could degrade and later content could appear inside a code box.
After fix: each code block stays balanced and plain text between/after blocks renders normally.

Test Plan

  • Added regression test in tests/test_renderer_js_behaviour.py (TestRawPreCodePreservation)
  • Ran direct Node renderMd smoke verification for multiline raw <pre><code> blocks
  • Confirmed touched files have no lints
  • Full local pytest run

Risk Assessment

Low — change scope is limited to stashing/restoring raw <pre> segments around one inline rewrite pass. No CSS/layout changes and no behavior changes for fenced markdown code blocks.

@nesquena-hermes
Copy link
Copy Markdown
Collaborator

Thanks for this PR! The stash-and-restore pattern for protecting raw <pre> blocks is the correct approach for this class of rendering bug.

The fix is well-targeted:

  • Stashing <pre>...</pre> blocks before the inline <code> normalization pass, then restoring them after, is minimal and precise
  • The null-byte sentinel (\x00R{i}\x00) is a good choice — won't appear in normal content
  • The regression test in test_renderer_js_behaviour.py covering multiple multiline <pre><code> blocks is exactly what's needed to lock this in

One note on scope: The PR also includes commits touching api/updates.py and server.py related to auto-reapplying local patches after upstream sync (feat(update): auto-reapply local patches after upstream sync). That's a significant and separate feature bundled with this renderer fix. The maintainer may want to consider separating these concerns — the renderer fix is a clean low-risk change, while the update/patch management changes are higher-risk and warrant independent review.

Files changed:

  • static/ui.js (+6/0) — the core renderer fix
  • tests/test_renderer_js_behaviour.py (+27/0) — regression test
  • api/updates.py (+124/-27) — local patches update flow (unrelated to renderer fix)
  • server.py (+76/0) — startup branch enforcement (unrelated to renderer fix)
  • tests/test_local_patches_update_flow.py (+77/0) — tests for the update flow

The renderer fix itself looks correct and low-risk. The bundled update management changes will need separate careful review.

@nesquena-hermes
Copy link
Copy Markdown
Collaborator

🔴 Hold: Please split into two PRs

This PR is being placed on hold because it bundles two unrelated changes that each deserve their own review track.

Change 1 — static/ui.js renderMd() pre-block fix ✅

The stash/restore logic for raw <pre> blocks is clean, self-contained, and valuable. We want this. It's easy to review in isolation and has no side-effects on other subsystems.

Change 2 — server.py + api/updates.py local-patches branch flow 🔍

The _ensure_active_branch() startup logic that auto-switches the repo to a local-patches branch is a complex behavioral change: it alters how the server initialises, affects update flows, and could have meaningful impact on users' git state. This warrants its own focused review, dedicated testing notes, and a separate discussion thread.

Requested action

Please split this into two PRs:

  1. PR A — only static/ui.js renderMd pre-block stash/restore fix
  2. PR Bserver.py + api/updates.py local-patches branch flow (with context on motivation, edge cases handled, and rollback behaviour)

Once split, PR A can move quickly. PR B can get the deeper review it needs without blocking the UI fix.

Thanks for the work here — both changes look like they're solving real problems, just easier to ship safely when reviewed separately.

/hold

bsgdigital added 2 commits April 27, 2026 19:40
Stash raw pre blocks before inline code-tag normalization so multiline code content is not rewritten into stray backticks that can corrupt subsequent code box rendering. Add a JS-renderer behavior test that guards balanced pre/code output across multiple raw code blocks.

Made-with: Cursor
Rename the raw pre stash variable so static substring checks for _pre_stash continue to target the intended code-block stash section.

Made-with: Cursor
@bsgdigital bsgdigital force-pushed the fix/pre-tag-response-parsing branch from f0359fc to 18870cf Compare April 27, 2026 19:40
@bsgdigital
Copy link
Copy Markdown
Contributor Author

@nesquena-hermes apologies, fixed now

@nesquena-hermes
Copy link
Copy Markdown
Collaborator

Integrated via #1172. Thank you @bsgdigital!

nesquena-hermes added a commit that referenced this pull request Apr 27, 2026
…, .env (#1179)

Merged as v0.50.228. 2644 tests passing. Browser QA 21/21 (desktop 1440×900 + mobile iPhone 14). All 5 fix invariants verified live in browser.

**Fix verifications:**
- #1172 (`renderMd` pre-stash): `rawPreStash` present in function, `<pre>` blocks pass through without content rewrite ✅
- #1174 (model race guard): `syncTopbar()` contains `liveStillPending` guard ✅
- #1175 (tool card): `.tool-card-result pre` max-height=360px, `.tool-card.open .tool-card-detail` overflow=auto, cap=600px ✅  
- #1176 (empty session guard): double-click New Conversation on empty session → stays on same session, composer focused ✅
- #1178 (`.env` atomic write): `tempfile.mkstemp + os.replace` in `providers.py`, 9/9 env tests pass ✅

Thanks @bsgdigital (#1150) and @bergeouss (#1178)!
JKJameson pushed a commit to JKJameson/hermes-webui that referenced this pull request Apr 29, 2026
…, .env (nesquena#1179)

Merged as v0.50.228. 2644 tests passing. Browser QA 21/21 (desktop 1440×900 + mobile iPhone 14). All 5 fix invariants verified live in browser.

**Fix verifications:**
- nesquena#1172 (`renderMd` pre-stash): `rawPreStash` present in function, `<pre>` blocks pass through without content rewrite ✅
- nesquena#1174 (model race guard): `syncTopbar()` contains `liveStillPending` guard ✅
- nesquena#1175 (tool card): `.tool-card-result pre` max-height=360px, `.tool-card.open .tool-card-detail` overflow=auto, cap=600px ✅  
- nesquena#1176 (empty session guard): double-click New Conversation on empty session → stays on same session, composer focused ✅
- nesquena#1178 (`.env` atomic write): `tempfile.mkstemp + os.replace` in `providers.py`, 9/9 env tests pass ✅

Thanks @bsgdigital (nesquena#1150) and @bergeouss (nesquena#1178)!
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.

2 participants