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:
4 files changed
+86
-4
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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, (
|
||||
|
||||
Reference in new issue
Block a user