diff --git a/CHANGELOG.md b/CHANGELOG.md index a9508e5c..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 diff --git a/static/commands.js b/static/commands.js index 8ddd011d..08ddd861 100644 --- a/static/commands.js +++ b/static/commands.js @@ -1402,6 +1402,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 4c23865c..b1951f4a 100644 --- a/static/ui.js +++ b/static/ui.js @@ -7241,6 +7241,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'); @@ -7274,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(); @@ -7343,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'); @@ -9287,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)=>{ @@ -11477,9 +11489,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-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')){ + 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..87660586 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,48 @@ class TestUiJsThinkingGate: ) break + def test_worklog_reasoning_rows_are_gated_by_show_thinking(self): + src = read('static/ui.js') + # 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): + 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") + # 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 " + "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 +217,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, (