From 1075d6e3a1c9c0977002a57ea8a207aeffd1a654 Mon Sep 17 00:00:00 2001 From: Rod Boev Date: Sun, 5 Jul 2026 16:25:59 -0400 Subject: [PATCH 1/6] fix(#5641): tolerate edge whitespace in markdown table parsing --- static/ui.js | 2 +- tests/test_issue5641_table_whitespace.py | 43 ++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 tests/test_issue5641_table_whitespace.py diff --git a/static/ui.js b/static/ui.js index 0eab8871..f2e54b58 100644 --- a/static/ui.js +++ b/static/ui.js @@ -6148,7 +6148,7 @@ function renderMd(raw){ // Tables: | col | col | header row followed by | --- | --- | separator then data rows // NOTE: table pass runs BEFORE outer link pass so [label](url) in table cells // is handled by inlineMd() only — prevents double-linking. - s=s.replace(/((?:^\|.+\|\n?)+)/gm,block=>{ + s=s.replace(/((?:^[ \t]{0,3}\|.+\|[ \t]*\n?)+)/gm,block=>{ const rows=block.trim().split('\n').filter(r=>r.trim()); if(rows.length<2)return block; const isSep=r=>/^\|[\s|:-]+\|$/.test(r.trim()); diff --git a/tests/test_issue5641_table_whitespace.py b/tests/test_issue5641_table_whitespace.py new file mode 100644 index 00000000..adb17a85 --- /dev/null +++ b/tests/test_issue5641_table_whitespace.py @@ -0,0 +1,43 @@ +import importlib.util +from pathlib import Path + +import pytest + + +_HELPERS_PATH = Path(__file__).with_name("test_renderer_js_behaviour.py") +_SPEC = importlib.util.spec_from_file_location("issue5641_renderer_helpers", _HELPERS_PATH) +_HELPERS = importlib.util.module_from_spec(_SPEC) +assert _SPEC.loader is not None +_SPEC.loader.exec_module(_HELPERS) + +NODE = _HELPERS.NODE +_render = _HELPERS._render +driver_path = _HELPERS.driver_path + + +pytestmark = pytest.mark.skipif(NODE is None, reason="node not on PATH") + + +@pytest.mark.parametrize( + "src", + [ + "| a | b | \n|---|---|\n| 1 | 2 |", + "| a | b |\n|---|---| \n| 1 | 2 |", + " | a | b |\n |---|---|\n | 1 | 2 |", + " | a | b |\n |---|---|\n | 1 | 2 |", + ], +) +def test_table_rows_with_edge_whitespace_still_render_as_table(driver_path, src): + out = _render(driver_path, src) + assert "" in out + assert "" in out + + +def test_four_space_indented_table_like_block_stays_outside_table(driver_path): + src = ( + " | a | b |\n" + " |---|---|\n" + " | 1 | 2 |" + ) + out = _render(driver_path, src) + assert " Date: Sun, 5 Jul 2026 16:39:54 -0400 Subject: [PATCH 2/6] fix(#5641): keep table boundary scoped to spaces --- static/ui.js | 2 +- tests/test_issue5641_table_whitespace.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/static/ui.js b/static/ui.js index f2e54b58..0c7c7fa6 100644 --- a/static/ui.js +++ b/static/ui.js @@ -6148,7 +6148,7 @@ function renderMd(raw){ // Tables: | col | col | header row followed by | --- | --- | separator then data rows // NOTE: table pass runs BEFORE outer link pass so [label](url) in table cells // is handled by inlineMd() only — prevents double-linking. - s=s.replace(/((?:^[ \t]{0,3}\|.+\|[ \t]*\n?)+)/gm,block=>{ + s=s.replace(/((?:^ {0,3}\|.+\|[ \t]*\n?)+)/gm,block=>{ const rows=block.trim().split('\n').filter(r=>r.trim()); if(rows.length<2)return block; const isSep=r=>/^\|[\s|:-]+\|$/.test(r.trim()); diff --git a/tests/test_issue5641_table_whitespace.py b/tests/test_issue5641_table_whitespace.py index adb17a85..2d5edd15 100644 --- a/tests/test_issue5641_table_whitespace.py +++ b/tests/test_issue5641_table_whitespace.py @@ -31,6 +31,7 @@ def test_table_rows_with_edge_whitespace_still_render_as_table(driver_path, src) out = _render(driver_path, src) assert "
1
" in out assert "" in out + assert "

" not in out def test_four_space_indented_table_like_block_stays_outside_table(driver_path): From f09e5d0b33a18283868f01d93a1f29ade8dc1370 Mon Sep 17 00:00:00 2001 From: Rod Boev Date: Sun, 5 Jul 2026 22:54:44 -0400 Subject: [PATCH 3/6] ci(#5658): install Playwright in pytest CI so browser tests run --- .github/workflows/tests.yml | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index aa2acc79..b826d67c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -103,7 +103,7 @@ jobs: run: | python -m pip install --upgrade pip # Office parsers stay optional at runtime; CI installs them explicitly for the workspace preview tests. - pip install "pyyaml>=6.0" pytest pytest-timeout pytest-asyncio pytest-shard python-docx openpyxl python-pptx + pip install "pyyaml>=6.0" pytest pytest-timeout pytest-asyncio pytest-shard python-docx openpyxl python-pptx playwright # ruff is installed so tests/test_ruff_forward_lint.py runs its E9/F821 # tree-clean assertions in-suite (mirrors how eslint is available for # tests/test_static_js_runtime_lint.py). If install fails the test @@ -117,5 +117,18 @@ jobs: # importorskip and the matrix stays green. pip install mcp || echo "mcp install failed — test_mcp_server.py will importorskip" + - name: Get Playwright version + id: pw-version + run: echo "version=$(pip show playwright | grep '^Version:' | cut -d' ' -f2)" >> "$GITHUB_OUTPUT" + + - name: Cache Playwright browsers + uses: actions/cache@v4 + with: + path: ~/.cache/ms-playwright + key: ${{ runner.os }}-playwright-${{ steps.pw-version.outputs.version }} + + - name: Install Playwright browsers + run: python -m playwright install --with-deps chromium + - name: Run tests (shard ${{ matrix.shard }} of 5) run: pytest tests/ -v --timeout=60 --shard-id=${{ matrix.shard }} --num-shards=5 From e19eefbe59dd267a949a73206f17b7e3f5025c7a Mon Sep 17 00:00:00 2001 From: Rod Boev Date: Sun, 5 Jul 2026 22:59:37 -0400 Subject: [PATCH 4/6] ci(#5658): add restore-keys for graceful cache fallback on version bumps --- .github/workflows/tests.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b826d67c..78b9b01b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -126,6 +126,8 @@ jobs: with: path: ~/.cache/ms-playwright key: ${{ runner.os }}-playwright-${{ steps.pw-version.outputs.version }} + restore-keys: | + ${{ runner.os }}-playwright- - name: Install Playwright browsers run: python -m playwright install --with-deps chromium From 18a2078ebbff9512f2762b51e895b421380df329 Mon Sep 17 00:00:00 2001 From: Frank Song Date: Mon, 6 Jul 2026 08:38:08 +0800 Subject: [PATCH 5/6] fix(streaming): guard empty terminal error hints --- api/streaming.py | 6 ++- ..._issue5121_provider_auth_terminal_error.py | 38 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/api/streaming.py b/api/streaming.py index 4829a5c3..6eeb3892 100644 --- a/api/streaming.py +++ b/api/streaming.py @@ -8675,9 +8675,13 @@ def _run_agent_streaming( _snapshot_and_append_partial_on_error(s, stream_id) except Exception: logger.debug("Failed to snapshot partials on error for %s", stream_id, exc_info=True) + _error_content = ( + f'**{_err_label}:** {_error_payload.get("message") or _err_label}' + + (f'\n\n*{_err_hint}*' if _err_hint else '') + ) _error_message = { 'role': 'assistant', - 'content': f'**{_err_label}:** {_error_payload.get("message") or _err_label}\n\n*{_err_hint}*', + 'content': _error_content, 'timestamp': int(time.time()), '_error': True, } diff --git a/tests/test_issue5121_provider_auth_terminal_error.py b/tests/test_issue5121_provider_auth_terminal_error.py index b3332dbc..877c62bd 100644 --- a/tests/test_issue5121_provider_auth_terminal_error.py +++ b/tests/test_issue5121_provider_auth_terminal_error.py @@ -455,6 +455,44 @@ def test_non_auth_silent_failure_still_uses_no_response(tmp_path, monkeypatch): assert saved.messages[-1]["_error"] is True +def test_live_settlement_empty_hint_does_not_append_empty_emphasis(tmp_path, monkeypatch): + session = _prepare_session( + "empty_hint_failure", + "stream_empty_hint_failure", + pending_user_message="Please fail plainly", + ) + + class EmptyHintFailureAgent(MockAgent): + def run_conversation(self, **kwargs): + return { + "status": "failed", + "messages": list(kwargs.get("conversation_history") or []), + "error": "synthetic hard failure", + } + + fake_queue = _run_stream( + monkeypatch, + session, + "stream_empty_hint_failure", + EmptyHintFailureAgent, + workspace=str(tmp_path), + ) + saved = Session.load("empty_hint_failure") + assert saved is not None + + events = _queue_events(fake_queue) + apperrors = [data for event, data in events if event == "apperror"] + assert apperrors, "expected apperror for generic terminal failure" + assert apperrors[-1]["type"] == "error" + assert apperrors[-1].get("hint") in (None, "") + + error_content = saved.messages[-1]["content"] + assert saved.messages[-1]["_error"] is True + assert error_content == "**Error:** synthetic hard failure" + assert "\n\n**" not in error_content + assert not error_content.endswith("**") + + def test_completed_assistant_answer_with_stale_partial_flag_settles_done(tmp_path, monkeypatch): session = _prepare_session( "completed_answer_stale_partial", From e3dfc2995068a0020aa102043dfb5e510cc8cb02 Mon Sep 17 00:00:00 2001 From: nesquena-hermes Date: Mon, 6 Jul 2026 03:53:18 +0000 Subject: [PATCH 6/6] docs(changelog): stamp phase1 batch (#5644, #5653, #5661) --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b7cf007..fb9c4627 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,15 @@ - **Completed the remaining UI translations across all 8 non-English locales.** 664 strings that still fell back to English (marked `TODO` in the locale table) are now translated, so the German / Spanish / French / Japanese / Korean / Portuguese / Chinese / Russian interfaces are fully localized. No new selectable language was added — this fills in the languages already offered. Interpolation tokens are preserved per key. Thanks @mo7al876any. (#5535, #5530) - **Recover from "context compression exhausted" with one click.** When a long conversation grows too large for automatic compression to make room, the terminal error now shows a **Start focused continuation** action. It opens a fresh linked session that keeps your workspace, model, profile, and toolset but starts with an empty model-facing transcript — so you can describe the next narrow task without replaying the oversized context. Bare "continue"/"go on" on an exhausted conversation is intercepted and pointed at the action (substantive prompts still go through), and repeated clicks or extra tabs converge on the same continuation instead of spawning duplicates. Thanks @franksong2702. (#5538, #4685) +### Fixed + +- **Markdown tables render even when a row has trailing whitespace or a small leading indent.** A table whose header or separator row ended in a stray space (something LLMs emit routinely) previously fell through to raw literal pipes instead of a real `

1
`; the renderer's table matcher now tolerates optional trailing whitespace and up to a 3-space leading indent, matching CommonMark. Thanks @rodboev. (#5644, #5641) +- **No more empty italic hint under settlement error cards.** When a live turn ended in an error with no additional detail to show, the error card could render a dangling empty italic line; the hint block is now only emitted when there's actual hint text. Thanks @franksong2702. (#5653) + +### Internal + +- **Browser (Playwright) tests actually run in CI.** The pytest CI job now installs Playwright + Chromium (with a version-pinned cache), so the browser-backed test files that were silently skipping now execute — closing a real coverage gap. Thanks @rodboev. (#5661, #5658) + ### Documentation - **Documented the `HERMES_WEBUI_*` environment variables and refreshed stale version/test/locale references.** README, ARCHITECTURE, TESTING, ROADMAP, SPRINTS, and the `.env` example files now describe the supported runtime env vars (host/port/state-dir/agent-dir/home) and no longer carry stale hardcoded version/test-count/locale figures. Thanks @mo7al876any. (#5536)