diff --git a/static/ui.js b/static/ui.js index 3be3ce3f..30a4c910 100644 --- a/static/ui.js +++ b/static/ui.js @@ -12360,6 +12360,60 @@ function _resetMismatchedLiveAssistantTurnForSession(turn, sessionId){ turn.dataset.sessionId=sid; return true; } +function _liveAnchorReasoningRowForFallback(turn, opts){ + opts=opts||{}; + const blocks=typeof _assistantTurnBlocks==='function' ? _assistantTurnBlocks(turn) : turn; + if(!turn||!blocks||!blocks.querySelectorAll) return null; + const streamId=String(opts.streamId||S.activeStreamId||''); + const sessionId=String(opts.sessionId||(S.session&&S.session.session_id)||''); + const rows=blocks.querySelectorAll( + '[data-anchor-scene-row="1"][data-anchor-source-event-type="reasoning"],'+ + '[data-anchor-scene-row="1"][data-anchor-row-role="thinking"],'+ + '.wl-reason[data-worklog-anchor-reason="1"]' + ); + for(const row of Array.from(rows)){ + const rowStreamId=String(row.getAttribute&&row.getAttribute('data-anchor-stream-id')||''); + if(rowStreamId&&streamId&&rowStreamId!==streamId) continue; + const rowSessionId=String(row.getAttribute&&row.getAttribute('data-session-id')||''); + if(rowSessionId&&sessionId&&rowSessionId!==sessionId) continue; + return row; + } + return null; +} +function _updateLiveAnchorReasoningRowForFallback(turn, text, opts){ + const clean=_sanitizeThinkingDisplayText(text); + if(!clean||window._showThinking===false) return false; + const row=_liveAnchorReasoningRowForFallback(turn, opts); + if(!row) return false; + if(row.classList&&row.classList.contains('wl-reason')){ + if(typeof _renderWorklogReasonInto==='function') _renderWorklogReasonInto(row, clean); + else row.textContent=clean; + const group=row.closest&&row.closest('.tool-worklog-group,.tool-call-group,.live-worklog'); + if(group&&typeof _syncToolCallGroupSummary==='function') _syncToolCallGroupSummary(group); + }else if(row.classList&&row.classList.contains('transparent-event-row')){ + _renderThinkingInto(row, clean); + const eventAt=row.getAttribute&&row.getAttribute('data-event-at'); + const nextTs=typeof _firstValidTimestampSeconds==='function' + ? _firstValidTimestampSeconds(opts&&opts.ts, opts&&opts.timestamp, opts&&opts.created_at, eventAt) + : null; + if(typeof _decorateTransparentEventRow==='function'){ + _decorateTransparentEventRow(row,{ + type:'thinking', + text:clean, + preview:clean, + ts:nextTs||undefined, + live:true, + segmentSeq:opts&&opts.segmentSeq, + burstId:opts&&opts.burstId, + }); + } + }else{ + _renderThinkingInto(row, clean); + } + if(turn&&typeof _syncTransparentEventControls==='function') _syncTransparentEventControls(turn); + if(typeof scrollIfPinned==='function') scrollIfPinned(); + return true; +} function renderLiveAnchorActivityScene(streamId, scene, opts){ opts=opts||{}; const requestedMode=opts.mode; @@ -18468,6 +18522,7 @@ function appendThinking(text='', options){ String(existingLiveTurn.dataset.sessionId)!==String(S.session.session_id||'')){ if(!_resetMismatchedLiveAssistantTurnForSession(existingLiveTurn, S.session.session_id)) return; } + if(anchorRenderFallback&&existingLiveTurn&&_updateLiveAnchorReasoningRowForFallback(existingLiveTurn,text,options)) return; if(!allowPendingPlaceholder&&!anchorRenderFallback&&isLiveAnchorActivitySceneOwner(S.activeStreamId)){ _renderLiveAnchorActivitySceneForStream(S.activeStreamId, S.session.session_id); return; diff --git a/tests/test_issue5720_reasoning_owner.py b/tests/test_issue5720_reasoning_owner.py index f67b993e..f48e368c 100644 --- a/tests/test_issue5720_reasoning_owner.py +++ b/tests/test_issue5720_reasoning_owner.py @@ -17,7 +17,9 @@ NODE = shutil.which("node") def _run_reasoning_scene( *, + activity_mode: str = "transparent_stream", fail_first_anchor_render: bool = False, + fail_anchor_render_on: int | None = None, stale_active_stream: bool = False, stale_live_turn: bool = False, show_thinking: bool = True, @@ -30,8 +32,11 @@ def _run_reasoning_scene( "ISSUE5720_ANCHORS_JS", str(ROOT / "static" / "assistant_turn_anchors.js"), ) + env["ISSUE5720_ACTIVITY_MODE"] = activity_mode if fail_first_anchor_render: env["ISSUE5720_FAIL_FIRST_ANCHOR_RENDER"] = "1" + if fail_anchor_render_on is not None: + env["ISSUE5720_FAIL_ANCHOR_RENDER_ON"] = str(fail_anchor_render_on) if stale_active_stream: env["ISSUE5720_STALE_ACTIVE_STREAM"] = "1" if stale_live_turn: @@ -133,6 +138,43 @@ def test_reasoning_fallback_rebuilds_from_another_sessions_live_turn(): assert result["inflight_reasoning_text"] == "Plan step" +@pytest.mark.skipif(NODE is None, reason="node not on PATH") +@pytest.mark.parametrize("activity_mode", ["transparent_stream", "compact_worklog"]) +def test_later_deferred_anchor_paint_updates_existing_reasoning_owner(activity_mode): + result = _run_reasoning_scene( + activity_mode=activity_mode, + fail_anchor_render_on=2, + ) + + assert result["first_text"] == "Plan" + assert result["second_text"] == "Plan step" + assert result["same_node"] is True + assert result["live_reasoning_rows"] == 1 + assert result["fallback_rows_after_recovery"] == 0 + assert result["visible_reasoning_owners"] == 1 + assert result["anchor_render_attempts"] == 2 + assert result["anchor_reasoning_events"] == 1 + assert result["anchor_reasoning_text"] == "Plan step" + assert result["inflight_reasoning_text"] == "Plan step" + + +@pytest.mark.skipif(NODE is None, reason="node not on PATH") +def test_later_deferred_anchor_paint_remains_hidden_in_final_answer_only_mode(): + result = _run_reasoning_scene( + activity_mode="hide_all_activity", + fail_anchor_render_on=2, + ) + + assert result["first_text"] is None + assert result["second_text"] is None + assert result["live_reasoning_rows"] == 0 + assert result["fallback_rows_after_recovery"] == 0 + assert result["visible_reasoning_owners"] == 0 + assert result["anchor_render_attempts"] == 2 + assert result["anchor_reasoning_events"] == 1 + assert result["anchor_reasoning_text"] == "Plan step" + + _NODE_SCENE = r""" const fs = require('fs'); const uiSrc = fs.readFileSync(process.env.ISSUE5720_UI_JS, 'utf8'); @@ -304,7 +346,7 @@ function matchesSimple(el, selector){ } global.window={ - _chatActivityDisplayMode:'transparent_stream', + _chatActivityDisplayMode:process.env.ISSUE5720_ACTIVITY_MODE||'transparent_stream', _showThinking:process.env.ISSUE5720_SHOW_THINKING!=='0', _simplifiedToolCalling:true, }; @@ -341,6 +383,28 @@ global.scrollIfPinned=()=>{}; global._moveLiveRunStatusToTurnEnd=()=>{}; global._messageUserUnpinned=false; global._syncTransparentEventControls=()=>{}; +global._syncToolCallGroupSummary=()=>{}; +global._captureWorklogDetailDisclosureState=()=>null; +global._restoreWorklogDetailDisclosureState=()=>{}; +global._startActivityElapsedTimer=()=>{}; +global._dedupeLiveProcessedWorklogAnchors=()=>{}; +global._toolWorklogListEl=group=>group&&group.querySelector&&group.querySelector('.tool-worklog-list')||group; +global.ensureActivityGroup=(blocks,opts)=>{ + opts=opts||{}; + const key=String(opts.activityKey||''); + let group=key&&blocks.querySelector&&blocks.querySelector(`.tool-worklog-group[data-tool-worklog-key="${CSS.escape(key)}"]`); + if(group) return group; + group=new FakeElement('div'); + group.className='tool-worklog-group tool-call-group'; + group.setAttribute('data-tool-worklog-group','1'); + group.setAttribute('data-live-tool-call-group','1'); + group.setAttribute('data-tool-worklog-key',key); + const list=new FakeElement('div'); + list.className='tool-worklog-list'; + group.appendChild(list); + blocks.appendChild(group); + return group; +}; global._thinkingActivityNode=text=>{ const row=new FakeElement('div'); row.className='agent-activity-thinking transparent-thinking-event'; @@ -382,7 +446,10 @@ for(const name of [ '_transparentLiveRowKey','_transparentLiveRowsCompatible', '_transparentLiveRowAttributePairs','_transparentLiveRowInteractiveState', '_refreshTransparentThinkingLiveRow','_refreshTransparentLiveRow', + '_thinkingMarkup','_renderThinkingInto', '_resetMismatchedLiveAssistantTurnForSession', + '_liveAnchorReasoningRowForFallback','_updateLiveAnchorReasoningRowForFallback', + '_anchorSceneNodeForRow','_anchorSceneWorklogGroup','_renderAnchorSceneRowsIntoWorklog', 'isLiveAnchorActivitySceneOwner','_projectLiveAnchorActivitySceneForStream', '_renderLiveAnchorActivitySceneTransparent','renderLiveAnchorActivityScene', '_renderLiveAnchorActivitySceneForStream','appendThinking','updateThinking', @@ -395,10 +462,12 @@ _renderLiveAnchorActivitySceneTransparent=function(...args){ return realTransparentRender(...args); }; const failFirstAnchorRender=process.env.ISSUE5720_FAIL_FIRST_ANCHOR_RENDER==='1'; +const failAnchorRenderOn=Number(process.env.ISSUE5720_FAIL_ANCHOR_RENDER_ON||'0'); let anchorRenderAttempts=0; window._renderLiveAnchorActivitySceneForStream=function(...args){ anchorRenderAttempts+=1; if(failFirstAnchorRender&&anchorRenderAttempts===1) return false; + if(failAnchorRenderOn&&anchorRenderAttempts===failAnchorRenderOn) return false; return _renderLiveAnchorActivitySceneForStream(...args); }; window.isLiveAnchorActivitySceneOwner=isLiveAnchorActivitySceneOwner; @@ -442,15 +511,33 @@ const source=FakeEventSource.instances[0]; if(!source) throw new Error('attachLiveStream did not create EventSource'); if(process.env.ISSUE5720_STALE_ACTIVE_STREAM==='1') S.activeStreamId='stream-new'; +function anchorReasoningRows(){ + return turn.querySelectorAll( + '[data-anchor-scene-row="1"][data-anchor-source-event-type="reasoning"],'+ + '[data-anchor-scene-row="1"][data-anchor-row-role="thinking"]' + ); +} +function fallbackReasoningRows(){ + return turn.querySelectorAll('.agent-activity-thinking[data-live-thinking="1"]') + .filter(row=>row.getAttribute('data-anchor-scene-row')!=='1'); +} +function reasoningText(row){ + if(!row) return null; + const pre=row.querySelector&&row.querySelector('pre'); + return pre?pre.textContent:row.textContent; +} + source.emit('reasoning',{text:'Plan '}); -const first=turn.querySelector('.transparent-event-row[data-event-type="thinking"][data-anchor-scene-row="1"]'); -const firstText=first&&first.querySelector('pre')&&first.querySelector('pre').textContent; -const firstFallback=turn.querySelector('.agent-activity-thinking[data-live-thinking="1"]'); -const firstFallbackText=firstFallback&&firstFallback.querySelector('pre')&&firstFallback.querySelector('pre').textContent; +const first=anchorReasoningRows()[0]||null; +const firstText=reasoningText(first); +const firstFallback=fallbackReasoningRows()[0]||null; +const firstFallbackText=reasoningText(firstFallback); const passAfterFirst=transparentRenderPasses; source.emit('reasoning',{text:'step'}); -const second=turn.querySelector('.transparent-event-row[data-event-type="thinking"][data-anchor-scene-row="1"]'); -const secondText=second&&second.querySelector('pre')&&second.querySelector('pre').textContent; +const second=anchorReasoningRows()[0]||null; +const secondText=reasoningText(second); +const anchorRowsAfterRecovery=anchorReasoningRows(); +const fallbackRowsAfterRecovery=fallbackReasoningRows(); const registry=window._liveAnchorRegistries&&window._liveAnchorRegistries.get('stream-1'); const anchorReasoningEvents=registry&®istry.anchor&&Array.isArray(registry.anchor.activity_events) ? registry.anchor.activity_events.filter(event=>event&&event.source_event_type==='reasoning') @@ -461,8 +548,9 @@ process.stdout.write(JSON.stringify({ first_fallback_text:firstFallbackText, second_text:secondText, same_node:first===second, - live_reasoning_rows:turn.querySelectorAll('.transparent-event-row[data-event-type="thinking"][data-anchor-scene-row="1"]').length, - fallback_rows_after_recovery:turn.querySelectorAll('.agent-activity-thinking[data-live-thinking="1"]').length, + live_reasoning_rows:anchorRowsAfterRecovery.length, + fallback_rows_after_recovery:fallbackRowsAfterRecovery.length, + visible_reasoning_owners:anchorRowsAfterRecovery.length+fallbackRowsAfterRecovery.length, render_passes:[passAfterFirst,transparentRenderPasses], anchor_render_attempts:anchorRenderAttempts, anchor_reasoning_events:anchorReasoningEvents.length,