Skip to content

fix: keep markdown tables block-level - #2375

Merged
1 commit merged into
nesquena:masterfrom
Michaelyklam:fix/issue-2374-markdown-table-block
May 16, 2026
Merged

1 commit merged into
nesquena:masterfrom
Michaelyklam:fix/issue-2374-markdown-table-block

Conversation

@Michaelyklam

Copy link
Copy Markdown
Contributor

Thinking Path

  • Hermes WebUI chat rendering should handle common Markdown output shapes without requiring users to inspect raw syntax.
  • Pipe tables were already converted to <table> markup, but the final paragraph pass did not treat generated tables as block-level output.
  • The fix is to keep generated tables isolated and add table to the paragraph-wrap skip list so valid CommonMark tables stay as tables.

What Changed

  • Keeps renderer-generated tables separated with blank lines before paragraph splitting.
  • Treats <table> like other block elements in the paragraph-wrap skip list.
  • Adds Node-backed regression coverage using the actual renderMd() implementation for a standalone table and a table between paragraphs.
  • Adds a changelog entry for bug: Markdown tables not rendering — table HTML wrapped in <p> tags #2374.

Why It Matters

  • Agent responses that include Markdown tables now render as actual HTML tables instead of raw pipe-delimited text or invalid paragraph-wrapped table markup.
  • The regression test exercises the real browser renderer path rather than a Python mirror.

Verification

  • /home/michael/.hermes/hermes-agent/venv/bin/python -m pytest tests/test_renderer_js_behaviour.py -q — 45 passed
  • node --check static/ui.js
  • git diff --check

Risks / Follow-ups

  • Low risk: the change is limited to the existing Markdown table rendering path and final paragraph-wrap block allowlist.
  • No follow-up expected unless additional CommonMark table variants need broader parser support.

Closes #2374

Model Used

AI-assisted change with repository inspection, targeted editing, and shell-based test verification.

@Michaelyklam
Michaelyklam force-pushed the fix/issue-2374-markdown-table-block branch from 675ee35 to 3cbe206 Compare May 16, 2026 09:12
@nesquena-hermes

Copy link
Copy Markdown
Collaborator

Reading the diff against origin/master and the surrounding renderMd() flow in static/ui.js — the fix addresses two compounding issues that together produce the <p><table>...</table></p> symptom from #2374.

Issue 1: table emission missing blank-line separation

On master, static/ui.js:2608-2618 emits the table as a bare HTML string:

return `<table><thead>${header}</thead><tbody>${body}</tbody></table>`;

If the table sits between text paragraphs, the final paragraph splitter (/\n{2,}/) treats the table and adjacent text as one "paragraph" because there's no guaranteed blank line on either side of the substituted HTML. The PR's two \n\n bookends on the return value are the same pattern already used elsewhere in this renderer (see the comment at static/ui.js:2355-2357 for the bold/emphasis splitter case). That pattern is consistent with the file.

Issue 2: paragraph-wrap skip list missing table

static/ui.js:2760 on master:

s=parts.map(p=>{p=p.trim();if(!p)return '';if(/^<(h[1-6]|ul|ol|pre|hr|blockquote)|^\x00[EQ]/.test(p))return p;return `<p>${p.replace(/\n/g,'<br>')}</p>`;}).join('\n');

The skip list (h[1-6]|ul|ol|pre|hr|blockquote) lacks table. So even when blank lines isolate the table block, the splitter still wraps it in <p>...</p>. Adding table to that alternation, which is what the PR does, is the right surgical fix.

Both fixes are needed: skip-list alone wouldn't help if the table got glued to surrounding text by the lack of blank-line bookends; bookends alone wouldn't help if the isolated block still hit the paragraph wrap.

Tests

tests/test_renderer_js_behaviour.py already drives the actual renderMd() via Node (_render(driver_path, src)), so the two new tests exercise the real renderer:

def test_commonmark_table_is_not_wrapped_in_paragraph(self, driver_path):
    ...
    assert "<p><table" not in out, (...)

def test_table_between_paragraphs_stays_block_level(self, driver_path):
    ...
    assert "<p>Before the table.</p>" in out
    assert "<table><thead>" in out
    assert "<p>After the table.</p>" in out
    assert "<p><table" not in out
    assert "</table></p>" not in out

The CJK fixture in the first test (升级时段, ~30 人) is a nice touch — it covers the original report's content shape exactly.

Risk notes

  • <table> is now in both the SAFE_TAGS allowlist (static/ui.js:2640) and the paragraph-wrap skip list, which is internally consistent.
  • The skip-list regex anchors on ^<(...) — so any table that ends up not at the start of a parts.map chunk (after trim) would still be wrapped. The blank-line bookends in the table-pass replacement guarantee a fresh \n\n boundary, which is why both halves are needed.
  • No interaction with the CSV-table pass (static/ui.js:2463) because that one emits a <div class="csv-table-wrap"> wrapper, not a bare <table>.

LGTM. Two-line fix with two real-renderer regression tests.

@nesquena-hermes nesquena-hermes closed this pull request by merging all changes into nesquena:master in 3de4338 May 16, 2026
eleboucher pushed a commit to eleboucher/homelab that referenced this pull request May 16, 2026
… 0.51.75) (#527)

This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [ghcr.io/nesquena/hermes-webui](https://github.com/nesquena/hermes-webui) | patch | `0.51.74` → `0.51.75` |

---

### Release Notes

<details>
<summary>nesquena/hermes-webui (ghcr.io/nesquena/hermes-webui)</summary>

### [`v0.51.75`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v05175--2026-05-16--Release-AY-stage-368--11-PR-safe-lane-batch--storage--i18n--run-journal-parity--attachments--compression-sidebar--restart-recovery--text-mode-images--tables--settings-i18n--German-labels)

[Compare Source](nesquena/hermes-webui@v0.51.74...v0.51.75)

##### Test infrastructure

- Stage-368 maintainer fix — pytest no longer self-loops on the `_schedule_restart` daemon thread. Several existing tests in `tests/test_update_banner_fixes.py` call `api.updates._schedule_restart()`, which spawns a daemon thread that eventually calls `os.execv()`. Those tests monkeypatch `os.execv` for the test scope, but monkeypatch teardown can win the race against the daemon thread, restoring the real `os.execv` before the thread fires it — at which point the daemon re-execs the entire pytest process with the original argv, looking from the outside like pytest hangs at 99 % then restarts the suite from 0 % in an infinite loop. `tests/conftest.py` now installs a permanent no-op wrapper on `os.execv` at module-import time so late-firing daemon threads cannot re-exec pytest. New `tests/test_pytest_execv_guard.py` pins the guard against future regressions.

##### Added

- **PR [#&#8203;2377](nesquena/hermes-webui#2377 by [@&#8203;franksong2702](https://github.com/franksong2702) (refs [#&#8203;2283](nesquena/hermes-webui#2283), refs [#&#8203;2363](nesquena/hermes-webui#2363), refs [#&#8203;1925](nesquena/hermes-webui#1925)) — Run-journal replay timeline parity checks. After [#&#8203;2283](nesquena/hermes-webui#2283) shipped the first run-journal replay slice and [#&#8203;2363](nesquena/hermes-webui#2363) documented the cross-layer state consistency contract, this PR adds explicit parity assertions over the replayed timeline so divergences between the journal and the visible transcript (Thinking → tool calls → assistant text) surface as test failures instead of silent drift.

##### Fixed

- **PR [#&#8203;2391](nesquena/hermes-webui#2391 by [@&#8203;Michaelyklam](https://github.com/Michaelyklam) (fixes [#&#8203;2389](nesquena/hermes-webui#2389)) — Reduce browser storage pressure during service-worker updates and over long-running sessions. `static/sw.js` now calls `deleteOldShellCaches()` BEFORE `caches.open(CACHE_NAME)` in the install handler so the new \~2.2 MB shell cache no longer overlaps the old one during a version bump (especially painful on shared-origin quota accounting). A new `_clearSessionViewedCount()` helper plus extended `_clearHandoffStorageForSession()` prune `hermes-session-viewed-counts`, `hermes-session-completion-unread`, and `hermes-session-observed-streaming` on every single-session delete and batch-delete so per-session tracking maps no longer grow unbounded.

- **PR [#&#8203;2387](nesquena/hermes-webui#2387 by [@&#8203;Michaelyklam](https://github.com/Michaelyklam) (fixes [#&#8203;2386](nesquena/hermes-webui#2386)) — Guard `localStorage.setItem('hermes-webui-session', ...)` and workspace-panel runtime-state writes with `try { … } catch (_) {}` across `static/boot.js`, `static/sessions.js`, `static/commands.js`, and `static/messages.js`. These convenience writes were previously fatal UI operations on quota-exhausted browsers (especially Firefox public-domain setups where shared quota fills up after a service-worker shell rotation).

- **PR [#&#8203;2368](nesquena/hermes-webui#2368 by [@&#8203;Michaelyklam](https://github.com/Michaelyklam) — Hybridize background profile env routing so background title generation, manual compression, and update-summary workers honor a session's non-default profile. The pure thread-local refactor for [#&#8203;2321](nesquena/hermes-webui#2321) was reverted because `hermes_cli.config.load_config()` still reads `HERMES_HOME` from process env. This PR keeps the thread-local layer for WebUI helpers and adds an `os.environ.update(runtime_env)` mirror under a narrow `_ENV_LOCK` for the worker body, with proper restore of prior values. New test asserts `OPENROUTER_API_KEY` is visible from the worker against a non-default profile.

- **PR [#&#8203;2382](nesquena/hermes-webui#2382 by [@&#8203;Michaelyklam](https://github.com/Michaelyklam) (fixes [#&#8203;2380](nesquena/hermes-webui#2380)) — Serve raw chat attachments from the per-session inbox in addition to the session workspace. Chat uploads were intentionally moved out of workspaces into a per-session attachment inbox in an earlier release; the transcript renderer still emits stable `api/file/raw?session_id=...&path=<filename>` URLs, but `_handle_file_raw` only checked `session.workspace` so inbox-backed uploads rendered as broken images. The URL surface is preserved and a session-attachment fallback is added with path-traversal guards intact.

- **PR [#&#8203;2385](nesquena/hermes-webui#2385 by [@&#8203;franksong2702](https://github.com/franksong2702) — Keep fuller compression snapshots reachable in the sidebar. The default behavior hides `pre_compression_snapshot: true` rows so archived compression segments do not duplicate the active continuation. A real long Kanban session exposed a narrower failure: the fuller transcript was still present on disk but remained marked as `pre_compression_snapshot`, so the sidebar surfaced a shorter row and the fuller transcript became unreachable. The fix preserves discoverability without re-introducing duplication in normal cases.

- **PR [#&#8203;2371](nesquena/hermes-webui#2371 by [@&#8203;franksong2702](https://github.com/franksong2702) — Clarify interrupted turn recovery after a WebUI restart. WebUI executes browser-originated agent turns inside the WebUI process; if that process restarts mid-turn, the worker dies with it. Run journal replay can only replay events that were already emitted, so the stale-pending repair path is now annotated and refined to make the post-restart state explicit (interrupted, recoverable, or terminal) instead of leaving the user with a half-rendered turn and no signal.

- **PR [#&#8203;2378](nesquena/hermes-webui#2378 by [@&#8203;Michaelyklam](https://github.com/Michaelyklam) — Strip historical images in text-only mode. Current-turn uploads already respect `agent.image_input_mode: text`, but saved conversation history still passed native `image_url` content parts back into later provider calls, breaking text-only providers on replayed turns. `_sanitize_messages_for_api()` gains a `cfg=` keyword argument so the API-history sanitizer can strip historical native image parts when the mode is text. Default `cfg=None` preserves prior behavior for callers that don't pass the new argument.

- **PR [#&#8203;2375](nesquena/hermes-webui#2375 by [@&#8203;Michaelyklam](https://github.com/Michaelyklam) — Keep Markdown tables block-level. Pipe tables were already converted to `<table>` markup, but the final paragraph pass did not treat generated tables as block-level output, occasionally wrapping them in `<p>` and breaking the surrounding layout. The fix isolates generated tables and adds `table` to the paragraph-wrap skip list so valid CommonMark tables render predictably.

- **PR [#&#8203;2372](nesquena/hermes-webui#2372 by [@&#8203;mccxj](https://github.com/mccxj) — Settings → Conversation page action buttons now respect locale selection. Pre-fix, the JSON export, MD export, and Copy buttons had hardcoded English labels/titles. Adds `data-i18n` / `data-i18n-title` attributes plus the missing translation keys so non-English locales no longer see English labels stuck in the middle of a translated screen.

- **PR [#&#8203;2381](nesquena/hermes-webui#2381 by [@&#8203;Michaelyklam](https://github.com/Michaelyklam) (fixes [#&#8203;2379](nesquena/hermes-webui#2379)) — German relative session-time labels now interpolate the elapsed value instead of rendering the literal `{n}` placeholder in the sidebar/header. The German locale now uses function-valued translations for minutes, hours, and days, matching the other locale bundles.

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about these updates again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xMDEuMSIsInVwZGF0ZWRJblZlciI6IjQzLjEwMS4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJyZW5vdmF0ZS9jb250YWluZXIiLCJ0eXBlL3BhdGNoIl19-->

Reviewed-on: https://git.erwanleboucher.dev/eleboucher/homelab/pulls/527
pull Bot pushed a commit to soitun/hermes-webui that referenced this pull request May 16, 2026
SysAdminDoc pushed a commit to SysAdminDoc/hermes-webui that referenced this pull request Jun 26, 2026
bernyforce pushed a commit to bernyforce/hermes-webui that referenced this pull request Jul 29, 2026
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.

bug: Markdown tables not rendering — table HTML wrapped in <p> tags

2 participants