From 558b1730a629007caa26ac1e894a5aea404edc84 Mon Sep 17 00:00:00 2001 From: nesquena-hermes Date: Wed, 22 Apr 2026 13:21:42 -0700 Subject: [PATCH] =?UTF-8?q?fix:=20thinking=20card=20no=20longer=20mirrors?= =?UTF-8?q?=20main=20response=20=E2=80=94=20v0.50.154=20(closes=20#852)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove early return in _streamDisplay() bypassing think-block stripping when reasoningText populated. --- CHANGELOG.md | 5 ++ static/messages.js | 5 +- tests/test_issue852_thinking_card_mirror.py | 58 +++++++++++++++++++++ 3 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 tests/test_issue852_thinking_card_mirror.py diff --git a/CHANGELOG.md b/CHANGELOG.md index e8597e3..34f8099 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Hermes Web UI -- Changelog +## [v0.50.154] — 2026-04-22 + +### Fixed +- **Thinking card no longer mirrors main response** — removed early return in `_streamDisplay()` that bypassed think-block stripping when `reasoningText` was populated. (`static/messages.js`) (closes #852) + ## [v0.50.153] — 2026-04-22 ### Fixed diff --git a/static/messages.js b/static/messages.js index 4d8fa74..ca7e9dd 100644 --- a/static/messages.js +++ b/static/messages.js @@ -279,7 +279,10 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){ } function _streamDisplay(){ const raw=_stripXmlToolCalls(assistantText); - if(reasoningText) return raw; + // Always run think-block stripping even when reasoningText is populated. + // Some providers emit reasoning content via on_reasoning AND wrap it in + // tags in the token stream — the early-return caused the thinking + // card and main response to show identical content (closes #852). for(const {open,close} of _thinkPairs){ // Trim leading whitespace before checking for the open tag — some models // (e.g. MiniMax) emit newlines before . diff --git a/tests/test_issue852_thinking_card_mirror.py b/tests/test_issue852_thinking_card_mirror.py new file mode 100644 index 0000000..a39694b --- /dev/null +++ b/tests/test_issue852_thinking_card_mirror.py @@ -0,0 +1,58 @@ +"""Regression tests for #852 — thinking card must not mirror the main response. + +The `_streamDisplay()` function in messages.js had an early return +`if(reasoningText) return raw` that bypassed think-block stripping when +the reasoning SSE event had populated `reasoningText`. Providers that emit +reasoning via BOTH `on_reasoning` AND `` tags in the token stream +then showed identical content in the thinking card and the main response. +""" +import os +import re + + +_SRC = os.path.join(os.path.dirname(__file__), "..") + + +def _read(name): + return open(os.path.join(_SRC, name), encoding="utf-8").read() + + +class TestStreamDisplayStripsThinkBlocksAlways: + + def test_early_return_on_reasoning_text_is_gone(self): + """Regression guard: the bypass that caused the thinking card to + mirror the main response must stay removed.""" + js = _read("static/messages.js") + m = re.search(r'function _streamDisplay\(\)\{.*?\n \}', js, re.DOTALL) + assert m, "_streamDisplay not found" + fn = m.group(0) + assert "if(reasoningText) return raw" not in fn, ( + "The early-return `if(reasoningText) return raw;` must remain " + "removed (#852) — it caused the thinking card to mirror the main " + "response when providers emit tags AND reasoning SSE events." + ) + + def test_think_pair_stripping_still_runs(self): + """The `_thinkPairs` stripping loop must still be present so the + fix actually strips think blocks.""" + js = _read("static/messages.js") + m = re.search(r'function _streamDisplay\(\)\{.*?\n \}', js, re.DOTALL) + assert m + fn = m.group(0) + assert "_thinkPairs" in fn, ( + "_streamDisplay must iterate _thinkPairs to strip think blocks" + ) + assert "trimmed.startsWith(open)" in fn, ( + "the think-block stripping must check for the open tag" + ) + + def test_still_handles_incomplete_think_tag_partial_prefix(self): + """Existing behaviour preserved: partial `