Merge pull request #4090 from nesquena/stage-3971

Release MO (v0.51.376): Hide Thinking also hides Worklog reasoning (#3903)
This commit is contained in:
nesquena-hermes authored and GitHub committed 2026-06-12 20:22:48 -07:00
commit b2c41658d6
4 files changed
+86 -4

No files matched your search

+6
View File
@@ -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
+1
View File
@@ -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
+17 -4
View File
@@ -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();
}
+62
View File
@@ -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, (