From c4284dc77c9db90661ab53043205a03d49ba37d4 Mon Sep 17 00:00:00 2001 From: Frank Song Date: Thu, 11 Jun 2026 12:25:17 +0800 Subject: [PATCH 1/3] fix: hide Worklog reasoning with Thinking --- CHANGELOG.md | 4 +++ static/commands.js | 1 + static/ui.js | 6 ++-- tests/test_reasoning_show_hide.py | 50 +++++++++++++++++++++++++++++++ 4 files changed, 59 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ee6d415..4ca288de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ - **New RFC: Stable Assistant Turn Anchors for Live-to-Final rendering.** Defines a frontend presentation/reconciliation model for anchoring one assistant turn across live streaming, settlement, replay/reload/recovery, Compact Worklog, Transparent Stream, terminal states, artifacts, and side effects. (#3926) +### Fixed + +- **Hide Thinking now also hides Worklog reasoning rows without hiding tool activity.** The `show_thinking=false` path now suppresses `.wl-reason` entries that come from reasoning text and prunes already-rendered rows when Thinking is toggled off, while preserving tool cards and ordinary Worklog anchor/progress rows. (#3903) + ## [v0.51.358] — 2026-06-11 — Release LV (first-run password bootstrap hardening) ### Security diff --git a/static/commands.js b/static/commands.js index 5fb97d30..1872d1bc 100644 --- a/static/commands.js +++ b/static/commands.js @@ -1362,6 +1362,7 @@ function cmdReasoning(args){ const on=(arg==='show'||arg==='on'); // Update the UI render gate immediately for responsiveness. window._showThinking=on; + if(!on&&typeof removeThinking==='function') removeThinking(); if(typeof renderMessages==='function') renderMessages(); // Persist via /api/reasoning → config.yaml display.show_reasoning // (CLI reads the same key). Also mirror into WebUI settings.json diff --git a/static/ui.js b/static/ui.js index 7c9d0132..5270bde9 100644 --- a/static/ui.js +++ b/static/ui.js @@ -6649,6 +6649,7 @@ function _renderWorklogReasonInto(row, text){ row.innerHTML=html; } function _worklogReasonNodeFromText(text, attrs){ + if(window._showThinking===false) return null; const html=_worklogReasonHtmlFromText(text); if(!html) return null; const row=document.createElement('div'); @@ -10633,9 +10634,10 @@ function removeThinking(){ const turn=$('liveAssistantTurn'); const blocks=_assistantTurnBlocks(turn); if(blocks) blocks.querySelectorAll('.agent-activity-thinking').forEach(el=>el.remove()); - if(blocks) blocks.querySelectorAll('.tool-call-group[data-agent-activity-group="1"]').forEach(group=>{ + if(blocks) blocks.querySelectorAll('.wl-reason[data-worklog-reason-source="reasoning"]').forEach(el=>el.remove()); + if(blocks) blocks.querySelectorAll('.live-worklog[data-live-worklog-shell="1"],.tool-worklog-group[data-live-tool-call-group="1"],.tool-call-group[data-live-tool-call-group="1"],.tool-call-group[data-agent-activity-group="1"]').forEach(group=>{ _syncToolCallGroupSummary(group); - if(!group.querySelector('.tool-card-row,.agent-activity-thinking')){ + if(!group.querySelector('.tool-card-row,.agent-activity-thinking,.wl-reason')){ if(typeof _clearActivityElapsedTimer==='function') _clearActivityElapsedTimer(); group.remove(); } diff --git a/tests/test_reasoning_show_hide.py b/tests/test_reasoning_show_hide.py index 454916c7..69fa1f3d 100644 --- a/tests/test_reasoning_show_hide.py +++ b/tests/test_reasoning_show_hide.py @@ -20,6 +20,23 @@ def read(rel): return (REPO / rel).read_text(encoding='utf-8') +def function_body(src, name): + start = src.find(f"function {name}") + assert start != -1, f"{name} not found" + brace = src.find("{", start) + assert brace != -1, f"{name} body not found" + depth = 0 + for idx in range(brace, len(src)): + ch = src[idx] + if ch == "{": + depth += 1 + elif ch == "}": + depth -= 1 + if depth == 0: + return src[brace + 1:idx] + raise AssertionError(f"{name} body did not close") + + # ── api/config.py ───────────────────────────────────────────────────────────── class TestShowThinkingConfig: @@ -80,6 +97,36 @@ class TestUiJsThinkingGate: ) break + def test_worklog_reasoning_rows_are_gated_by_show_thinking(self): + src = read('static/ui.js') + node_fn = function_body(src, "_worklogReasonNodeFromText") + assert 'window._showThinking===false' in node_fn and 'return null' in node_fn, ( + "reasoning-source Worklog rows must not be created when thinking is hidden" + ) + + def test_show_thinking_gate_does_not_hide_worklog_anchor_text(self): + src = read('static/ui.js') + html_fn = function_body(src, "_worklogReasonHtmlFromText") + assert 'window._showThinking' not in html_fn, ( + "the low-level Worklog text renderer is also used for anchor/progress text; " + "only reasoning-source rows should be gated" + ) + + def test_remove_thinking_prunes_reasoning_rows_but_preserves_tool_or_anchor_rows(self): + src = read('static/ui.js') + fn = function_body(src, "removeThinking") + assert '.wl-reason[data-worklog-reason-source="reasoning"]' in fn, ( + "removeThinking must sweep already-rendered reasoning Worklog rows" + ) + assert '.tool-card-row,.agent-activity-thinking,.wl-reason' in fn, ( + "empty-group cleanup must preserve groups that still contain tool cards " + "or non-reasoning Worklog anchor rows" + ) + assert '.live-worklog[data-live-worklog-shell="1"]' in fn, ( + "live Worklog shells must be considered for cleanup after their reasoning " + "rows are removed" + ) + # ── static/messages.js ──────────────────────────────────────────────────────── @@ -158,6 +205,9 @@ class TestReasoningCommand: assert 'renderMessages' in fn, ( "show/hide branch must call renderMessages()" ) + assert "typeof removeThinking==='function'" in fn and "removeThinking()" in fn, ( + "hide/off must prune already-rendered live Thinking and Worklog reasoning rows" + ) # Persistence: POST to /api/reasoning (CLI-shared config.yaml) AND # /api/settings (boot.js mirror). assert "api('/api/reasoning'" in fn, ( From 35877dab1b0ec8aead8a0a8010415e98b69e76db Mon Sep 17 00:00:00 2001 From: nesquena-hermes Date: Sat, 13 Jun 2026 03:09:50 +0000 Subject: [PATCH 2/3] fix(#3971): move show-thinking gate to the real Worklog render paths (Opus catch) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Opus final gate caught that the staged re-implementation placed the window._showThinking===false gate in _worklogReasonNodeFromText, which has ZERO callers — so live/settled reasoning rows (built by _syncWorklogReasonFromAnchor and _appendWorklogReason) were never actually hidden, and removeThinking swept data-worklog-reason-source=reasoning which the real rows (data-worklog-anchor-reason=1) don't carry. The string-match tests passed against the dead function = green-suite blind spot. Fix: - Gate _syncWorklogReasonFromAnchor (live + settled): when _showThinking===false, remove any existing reasoning row and bail before building. - Gate _appendWorklogReason (settled rebuild): return null when _showThinking===false. - removeThinking + the settled-rebuild cleanup now sweep BOTH data-worklog-anchor-reason=1 (the real attribute) and the legacy data-worklog-reason-source=reasoning. - Re-anchor the tests to assert against the real render functions + the correct attribute. --- static/ui.js | 17 ++++++++++++++--- tests/test_reasoning_show_hide.py | 22 +++++++++++++++++----- 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/static/ui.js b/static/ui.js index 97c8980d..b1951f4a 100644 --- a/static/ui.js +++ b/static/ui.js @@ -7275,10 +7275,19 @@ function _syncWorklogReasonFromAnchor(group, anchor, displayTextOverride){ const list=_toolWorklogListEl(group); if(!group||!list) return; const anchorKey=_worklogReasonAnchorKey(anchor); + const selector=anchorKey?`:scope > .wl-reason[data-worklog-anchor-key="${CSS.escape(anchorKey)}"]`:':scope > .wl-reason[data-worklog-anchor-reason="1"]'; + // When reasoning/thinking display is turned off (#3903), do not render Worklog + // reasoning rows on the live OR settled path — remove any existing one and bail + // before building. (The gate must live here, in the actual render path, not in + // the unused _worklogReasonNodeFromText helper.) + if(window._showThinking===false){ + const existing=list.querySelector(selector); + if(existing) existing.remove(); + return; + } const html=arguments.length>2 ? _worklogReasonHtmlFromAnchor(anchor, displayTextOverride) : _worklogReasonHtmlFromAnchor(anchor); - const selector=anchorKey?`:scope > .wl-reason[data-worklog-anchor-key="${CSS.escape(anchorKey)}"]`:':scope > .wl-reason[data-worklog-anchor-reason="1"]'; let reason=list.querySelector(selector); if(!html){ if(reason) reason.remove(); @@ -7344,6 +7353,8 @@ function _migrateLegacyLiveActivityGroupsToWorklog(blocks, worklog){ } function _appendWorklogReason(list, anchor){ if(!list) return null; + // Reasoning display off (#3903): never append a Worklog reasoning row. + if(window._showThinking===false) return null; const html=_worklogReasonHtmlFromAnchor(anchor); if(!html) return null; const reason=document.createElement('div'); @@ -9288,7 +9299,7 @@ function renderMessages(options){ // regression vs master; same content-loss-on-switch class as #3668). The // `:not([data-live-thinking="1"])` / live-card guards below keep the active // turn's own live nodes from being double-built. - inner.querySelectorAll('.tool-worklog-group:not([data-compression-card]),.tool-call-group:not([data-compression-card]),.tool-card-row:not([data-compression-card]),.agent-activity-thinking:not([data-live-thinking="1"]):not([data-event-type="thinking"]),.wl-reason[data-worklog-reason-source="reasoning"]').forEach(el=>el.remove()); + inner.querySelectorAll('.tool-worklog-group:not([data-compression-card]),.tool-call-group:not([data-compression-card]),.tool-card-row:not([data-compression-card]),.agent-activity-thinking:not([data-live-thinking="1"]):not([data-event-type="thinking"]),.wl-reason[data-worklog-anchor-reason="1"],.wl-reason[data-worklog-reason-source="reasoning"]').forEach(el=>el.remove()); const byActivity = new Map(); const assistantIdxs=[...assistantSegments.keys()].sort((a,b)=>a-b); const _assistantAnchorForActivity=(aIdx,segmentSeq,burstId)=>{ @@ -11478,7 +11489,7 @@ function removeThinking(){ const turn=$('liveAssistantTurn'); const blocks=_assistantTurnBlocks(turn); if(blocks) blocks.querySelectorAll('.agent-activity-thinking').forEach(el=>el.remove()); - if(blocks) blocks.querySelectorAll('.wl-reason[data-worklog-reason-source="reasoning"]').forEach(el=>el.remove()); + if(blocks) blocks.querySelectorAll('.wl-reason[data-worklog-anchor-reason="1"],.wl-reason[data-worklog-reason-source="reasoning"]').forEach(el=>el.remove()); if(blocks) blocks.querySelectorAll('.live-worklog[data-live-worklog-shell="1"],.tool-worklog-group[data-live-tool-call-group="1"],.tool-call-group[data-live-tool-call-group="1"],.tool-call-group[data-agent-activity-group="1"]').forEach(group=>{ _syncToolCallGroupSummary(group); if(!group.querySelector('.tool-card-row,.agent-activity-thinking,.wl-reason')){ diff --git a/tests/test_reasoning_show_hide.py b/tests/test_reasoning_show_hide.py index 69fa1f3d..87660586 100644 --- a/tests/test_reasoning_show_hide.py +++ b/tests/test_reasoning_show_hide.py @@ -99,9 +99,17 @@ class TestUiJsThinkingGate: def test_worklog_reasoning_rows_are_gated_by_show_thinking(self): src = read('static/ui.js') - node_fn = function_body(src, "_worklogReasonNodeFromText") - assert 'window._showThinking===false' in node_fn and 'return null' in node_fn, ( - "reasoning-source Worklog rows must not be created when thinking is hidden" + # The gate must sit in the ACTUAL render paths that build Worklog reasoning + # rows — _syncWorklogReasonFromAnchor (live + settled) and _appendWorklogReason + # (settled rebuild) — not in the unused _worklogReasonNodeFromText helper. + sync_fn = function_body(src, "_syncWorklogReasonFromAnchor") + assert 'window._showThinking===false' in sync_fn and 'return' in sync_fn, ( + "_syncWorklogReasonFromAnchor must bail (and remove any existing row) " + "when thinking display is off" + ) + append_fn = function_body(src, "_appendWorklogReason") + assert 'window._showThinking===false' in append_fn and 'return null' in append_fn, ( + "_appendWorklogReason must not build a reasoning row when thinking is hidden" ) def test_show_thinking_gate_does_not_hide_worklog_anchor_text(self): @@ -115,8 +123,12 @@ class TestUiJsThinkingGate: def test_remove_thinking_prunes_reasoning_rows_but_preserves_tool_or_anchor_rows(self): src = read('static/ui.js') fn = function_body(src, "removeThinking") - assert '.wl-reason[data-worklog-reason-source="reasoning"]' in fn, ( - "removeThinking must sweep already-rendered reasoning Worklog rows" + # The live/settled reasoning rows are tagged data-worklog-anchor-reason="1" + # (by _syncWorklogReasonFromAnchor / _appendWorklogReason); the sweep MUST + # target that attribute, not only the legacy data-worklog-reason-source. + assert '.wl-reason[data-worklog-anchor-reason="1"]' in fn, ( + "removeThinking must sweep the actually-rendered reasoning Worklog rows " + '(data-worklog-anchor-reason="1")' ) assert '.tool-card-row,.agent-activity-thinking,.wl-reason' in fn, ( "empty-group cleanup must preserve groups that still contain tool cards " From 02e201fcf1540bef73c1a2d23f522c59a3ecafb3 Mon Sep 17 00:00:00 2001 From: nesquena-hermes Date: Sat, 13 Jun 2026 03:19:44 +0000 Subject: [PATCH 3/3] docs(changelog): stamp #3903 Worklog-reasoning-hide as v0.51.376 (Release MO) --- CHANGELOG.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 68d04763..32bdc2b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ ## [Unreleased] +## [v0.51.376] — 2026-06-13 — Release MO (Hide Thinking also hides Worklog reasoning, #3903) + +### Fixed + +- **Hiding Thinking now also hides Worklog reasoning rows, without hiding tool activity (#3903).** When reasoning/thinking display is turned off, the Worklog reasoning rows (`.wl-reason`) are now suppressed on both the live streaming path and the settled/reloaded render path, and any already-rendered reasoning rows are pruned when Thinking is toggled off — while tool cards and ordinary Worklog anchor/progress rows are preserved. (Previously the gate sat on an unused helper, so reasoning rows kept rendering even with Thinking off.) (#3903) + ## [v0.51.375] — 2026-06-13 — Release MN (Transparent Stream activity display, #3820) ### Added @@ -121,10 +127,6 @@ - **Internal Stable Assistant Turn Anchors Phase 0 scaffold.** The browser now ships an inert `HermesAssistantTurnAnchors` helper surface plus a documented state-layer inventory for #3926, pinning current live/replay/settled source classifications and event dedupe precedence without changing visible chat rendering. - **New RFC: Stable Assistant Turn Anchors for Live-to-Final rendering.** Defines a frontend presentation/reconciliation model for anchoring one assistant turn across live streaming, settlement, replay/reload/recovery, Compact Worklog, Transparent Stream, terminal states, artifacts, and side effects. (#3926) -### Fixed - -- **Hide Thinking now also hides Worklog reasoning rows without hiding tool activity.** The `show_thinking=false` path now suppresses `.wl-reason` entries that come from reasoning text and prunes already-rendered rows when Thinking is toggled off, while preserving tool cards and ordinary Worklog anchor/progress rows. (#3903) - ## [v0.51.358] — 2026-06-11 — Release LV (first-run password bootstrap hardening) ### Security