Fix transparent live anchor scene replay
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
95f1486567
commit
6ac5b9dbee
2 changed files with 425 additions and 3 deletions
85
static/ui.js
85
static/ui.js
|
|
@ -10028,6 +10028,7 @@ function _anchorSceneNodeForRow(row, opts){
|
|||
}
|
||||
function _anchorSceneTransparentNodeForRow(row, opts){
|
||||
const settled=!!(opts&&opts.settled);
|
||||
const live=!!(opts&&opts.live);
|
||||
if(!row) return null;
|
||||
let node=null;
|
||||
const meta={
|
||||
|
|
@ -10079,10 +10080,14 @@ function _anchorSceneTransparentNodeForRow(row, opts){
|
|||
}
|
||||
if(!node) return null;
|
||||
node.setAttribute('data-anchor-scene-row','1');
|
||||
node.setAttribute('data-anchor-settled-scene-row','1');
|
||||
if(settled) node.setAttribute('data-anchor-settled-scene-row','1');
|
||||
if(live) node.setAttribute('data-anchor-live-scene-row','1');
|
||||
node.setAttribute('data-anchor-row-id',String(row.row_id||row.local_id||''));
|
||||
node.setAttribute('data-anchor-row-role',String(row.role||'activity'));
|
||||
node.setAttribute('data-anchor-source-event-type',String(row.source_event_type||''));
|
||||
if(opts&&opts.streamId) node.setAttribute('data-anchor-stream-id',String(opts.streamId));
|
||||
if(opts&&opts.sessionId) node.setAttribute('data-session-id',String(opts.sessionId));
|
||||
if(live) node.setAttribute('data-live-stream-owned','1');
|
||||
return node;
|
||||
}
|
||||
// Whitespace-insensitive compare so a scene prose row that IS the final answer
|
||||
|
|
@ -10259,6 +10264,9 @@ function _prepareLiveAnchorScrollRebuildGuard(scrollSnapshot){
|
|||
}
|
||||
function renderLiveAnchorActivityScene(streamId, scene, opts){
|
||||
opts=opts||{};
|
||||
if(typeof isTransparentStream==='function'&&isTransparentStream()){
|
||||
return _renderLiveAnchorActivitySceneTransparent(streamId,scene,opts);
|
||||
}
|
||||
if(typeof isCompactWorklogMode==='function'&&!isCompactWorklogMode()) return false;
|
||||
if(!S.session||!S.activeStreamId) return false;
|
||||
if(opts.sessionId&&S.session.session_id!==opts.sessionId) return false;
|
||||
|
|
@ -10269,11 +10277,11 @@ function renderLiveAnchorActivityScene(streamId, scene, opts){
|
|||
if(!turn){
|
||||
turn=_createAssistantTurn();
|
||||
turn.id='liveAssistantTurn';
|
||||
if(S.session) turn.dataset.sessionId=S.session.session_id;
|
||||
$('msgInner').appendChild(turn);
|
||||
}
|
||||
turn.setAttribute('data-anchor-scene-live-owner','1');
|
||||
turn.setAttribute('data-anchor-stream-id',String(streamId||''));
|
||||
// Re-stamp when reusing a turn restored or previously rendered in another mode.
|
||||
if(S.session) turn.dataset.sessionId=S.session.session_id;
|
||||
const blocks=_assistantTurnBlocks(turn);
|
||||
if(!blocks) return false;
|
||||
|
|
@ -10319,8 +10327,79 @@ function renderLiveAnchorActivityScene(streamId, scene, opts){
|
|||
if(!scrollRebuildGuard.readerAwayFromBottom&&typeof scrollIfPinned==='function') scrollIfPinned();
|
||||
return true;
|
||||
}
|
||||
function _renderLiveAnchorActivitySceneTransparent(streamId, scene, opts){
|
||||
opts=opts||{};
|
||||
if(!S.session||!S.activeStreamId) return false;
|
||||
if(opts.sessionId&&S.session.session_id!==opts.sessionId) return false;
|
||||
if(streamId&&S.activeStreamId!==streamId) return false;
|
||||
const rows=_anchorSceneRowsForRendering(scene,{settled:false});
|
||||
if(!rows.length) return false;
|
||||
$('emptyState').style.display='none';
|
||||
let turn=$('liveAssistantTurn');
|
||||
if(!turn){
|
||||
turn=_createAssistantTurn();
|
||||
turn.id='liveAssistantTurn';
|
||||
$('msgInner').appendChild(turn);
|
||||
}
|
||||
turn.setAttribute('data-anchor-scene-live-owner','1');
|
||||
turn.setAttribute('data-anchor-stream-id',String(streamId||''));
|
||||
turn.setAttribute('data-live-assistant-turn','1');
|
||||
if(S.session) turn.dataset.sessionId=S.session.session_id;
|
||||
const blocks=_assistantTurnBlocks(turn);
|
||||
if(!blocks) return false;
|
||||
const scrollSnapshot=_captureMessageScrollSnapshot();
|
||||
const scrollRebuildGuard=_prepareLiveAnchorScrollRebuildGuard(scrollSnapshot);
|
||||
blocks.querySelectorAll('[data-anchor-scene-owner="1"],[data-anchor-scene-row="1"]').forEach(el=>el.remove());
|
||||
// Clear every legacy live activity surface this renderer can replace. The
|
||||
// anchor-scene rows are now the source of truth for visible live activity.
|
||||
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-card-row[data-live-tid],'+
|
||||
'.agent-activity-thinking[data-live-thinking="1"],'+
|
||||
'.transparent-event-row[data-live-tid],'+
|
||||
'[data-live-stream-owned="1"],'+
|
||||
'.interim-collapse-toggle'
|
||||
).forEach(el=>el.remove());
|
||||
// Match the compact path: keep legacy live segments as hidden anchors so
|
||||
// stream-owned metadata survives while the anchor scene owns visible activity.
|
||||
blocks.querySelectorAll('[data-live-assistant="1"]').forEach(el=>{
|
||||
el.classList.add('assistant-segment-worklog-source');
|
||||
el.setAttribute('aria-hidden','true');
|
||||
el.hidden=true;
|
||||
});
|
||||
const liveFooter=blocks.querySelector('#liveRunStatus');
|
||||
let wrote=false;
|
||||
for(const row of rows){
|
||||
const node=_anchorSceneTransparentNodeForRow(row,{
|
||||
live:true,
|
||||
settled:false,
|
||||
streamId:streamId||S.activeStreamId||'',
|
||||
sessionId:S.session&&S.session.session_id,
|
||||
});
|
||||
if(!node) continue;
|
||||
if(liveFooter&&liveFooter.parentElement===blocks) blocks.insertBefore(node,liveFooter);
|
||||
else blocks.appendChild(node);
|
||||
wrote=true;
|
||||
}
|
||||
if(wrote) _syncTransparentEventControls(turn);
|
||||
if(typeof _moveLiveRunStatusToTurnEnd==='function') _moveLiveRunStatusToTurnEnd();
|
||||
_restoreMessageScrollSnapshotSameFrame(scrollSnapshot);
|
||||
if(scrollRebuildGuard&&scrollRebuildGuard.release){
|
||||
requestAnimationFrame(()=>{
|
||||
scrollRebuildGuard.release();
|
||||
if(_messageUserUnpinned) _restoreMessageScrollSnapshotSameFrame(scrollSnapshot);
|
||||
});
|
||||
}
|
||||
if(!scrollRebuildGuard.readerAwayFromBottom&&typeof scrollIfPinned==='function') scrollIfPinned();
|
||||
return wrote;
|
||||
}
|
||||
function _renderLiveAnchorActivitySceneForStream(streamId, sessionId, opts){
|
||||
const scene=_projectLiveAnchorActivitySceneForStream(streamId,(opts&&opts.mode)||'compact_worklog');
|
||||
const mode=(typeof isTransparentStream==='function'&&isTransparentStream())
|
||||
? 'transparent_stream'
|
||||
: ((opts&&opts.mode)||'compact_worklog');
|
||||
const scene=_projectLiveAnchorActivitySceneForStream(streamId,mode);
|
||||
if(!scene) return false;
|
||||
return renderLiveAnchorActivityScene(streamId,scene,{...(opts||{}),sessionId});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -804,6 +804,349 @@ def test_transparent_stream_renders_persisted_anchor_scene_after_reload():
|
|||
assert "if(anchorOwnedAssistantRawIdxs.has(aIdx)) continue;" in render
|
||||
|
||||
|
||||
def test_live_anchor_scene_snapshot_renders_transparent_rows_before_compact_gate():
|
||||
live = _function_body(UI_JS, "renderLiveAnchorActivityScene")
|
||||
transparent = _function_body(UI_JS, "_renderLiveAnchorActivitySceneTransparent")
|
||||
stream = _function_body(UI_JS, "_renderLiveAnchorActivitySceneForStream")
|
||||
row = _function_body(UI_JS, "_anchorSceneTransparentNodeForRow")
|
||||
|
||||
transparent_gate = "return _renderLiveAnchorActivitySceneTransparent(streamId,scene,opts);"
|
||||
compact_gate = "if(typeof isCompactWorklogMode==='function'&&!isCompactWorklogMode()) return false;"
|
||||
assert transparent_gate in live
|
||||
assert compact_gate in live
|
||||
assert live.index(transparent_gate) < live.index(compact_gate), (
|
||||
"transparent live snapshots must render before the compact-only guard"
|
||||
)
|
||||
|
||||
assert "_anchorSceneRowsForRendering(scene,{settled:false})" in transparent
|
||||
assert "turn.id='liveAssistantTurn'" in transparent
|
||||
assert "turn.dataset.sessionId=S.session.session_id" in transparent
|
||||
assert "turn.setAttribute('data-anchor-scene-live-owner','1')" in transparent
|
||||
assert "turn.setAttribute('data-anchor-stream-id',String(streamId||''))" in transparent
|
||||
assert "turn.setAttribute('data-live-assistant-turn','1')" in transparent
|
||||
assert "_anchorSceneTransparentNodeForRow(row,{" in transparent
|
||||
assert "live:true" in transparent
|
||||
assert "streamId:streamId||S.activeStreamId||''" in transparent
|
||||
assert "sessionId:S.session&&S.session.session_id" in transparent
|
||||
assert "_syncTransparentEventControls(turn)" in transparent
|
||||
assert "blocks.querySelectorAll('[data-live-assistant=\"1\"]').forEach" in transparent
|
||||
assert ".transparent-event-row[data-live-tid]" in transparent
|
||||
assert "[data-live-stream-owned=\"1\"]" in transparent
|
||||
|
||||
assert "'transparent_stream'" in stream
|
||||
assert "_projectLiveAnchorActivitySceneForStream(streamId,mode)" in stream
|
||||
assert "data-anchor-live-scene-row" in row
|
||||
assert "data-live-stream-owned" in row
|
||||
assert "if(settled) node.setAttribute('data-anchor-settled-scene-row','1')" in row
|
||||
|
||||
|
||||
@pytest.mark.skipif(NODE is None, reason="node is required for DOM-executed anchor render tests")
|
||||
def test_live_anchor_scene_transparent_snapshot_render_is_idempotent_and_hides_legacy_rows():
|
||||
script = f"""
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const src = fs.readFileSync({json.dumps(str(ROOT / "static" / "ui.js"))}, 'utf8');
|
||||
function extractFunc(name){{
|
||||
const re = new RegExp('function\\\\s+' + name + '\\\\s*\\\\(');
|
||||
const start = src.search(re);
|
||||
if(start < 0) throw new Error(name + ' not found');
|
||||
const params = src.indexOf('(', start);
|
||||
let depth = 0, close = -1;
|
||||
for(let i=params; i<src.length; i++){{
|
||||
if(src[i] === '(') depth++;
|
||||
else if(src[i] === ')'){{
|
||||
depth--;
|
||||
if(depth === 0){{ close = i; break; }}
|
||||
}}
|
||||
}}
|
||||
const brace = src.indexOf('{{', close);
|
||||
depth = 0;
|
||||
for(let i=brace; i<src.length; i++){{
|
||||
if(src[i] === '{{') depth++;
|
||||
else if(src[i] === '}}'){{
|
||||
depth--;
|
||||
if(depth === 0) return src.slice(start, i + 1);
|
||||
}}
|
||||
}}
|
||||
throw new Error(name + ' body did not close');
|
||||
}}
|
||||
|
||||
class FakeElement {{
|
||||
constructor(tag='div'){{
|
||||
this.tagName=tag.toUpperCase();
|
||||
this.children=[];
|
||||
this.parentNode=null;
|
||||
this.attributes={{}};
|
||||
this.dataset={{}};
|
||||
this.style={{}};
|
||||
this.hidden=false;
|
||||
this.id='';
|
||||
this.textContent='';
|
||||
this._classes=new Set();
|
||||
const self=this;
|
||||
this.classList={{
|
||||
add(...names){{ names.forEach(name=>self._classes.add(name)); }},
|
||||
remove(...names){{ names.forEach(name=>self._classes.delete(name)); }},
|
||||
contains(name){{ return self._classes.has(name); }},
|
||||
toggle(name, force){{
|
||||
const on=force===undefined?!self._classes.has(name):!!force;
|
||||
if(on) self._classes.add(name); else self._classes.delete(name);
|
||||
return on;
|
||||
}},
|
||||
}};
|
||||
}}
|
||||
get parentElement(){{ return this.parentNode; }}
|
||||
get firstChild(){{ return this.children[0]||null; }}
|
||||
setAttribute(name,value){{
|
||||
const str=String(value);
|
||||
this.attributes[name]=str;
|
||||
if(name==='id') this.id=str;
|
||||
if(name.startsWith('data-')){{
|
||||
const key=name.slice(5).replace(/-([a-z])/g,(_,c)=>c.toUpperCase());
|
||||
this.dataset[key]=str;
|
||||
}}
|
||||
}}
|
||||
getAttribute(name){{ return Object.prototype.hasOwnProperty.call(this.attributes,name)?this.attributes[name]:null; }}
|
||||
hasAttribute(name){{ return Object.prototype.hasOwnProperty.call(this.attributes,name); }}
|
||||
removeAttribute(name){{
|
||||
delete this.attributes[name];
|
||||
if(name==='id') this.id='';
|
||||
if(name.startsWith('data-')){{
|
||||
const key=name.slice(5).replace(/-([a-z])/g,(_,c)=>c.toUpperCase());
|
||||
delete this.dataset[key];
|
||||
}}
|
||||
}}
|
||||
appendChild(child){{
|
||||
if(child.parentNode) child.remove();
|
||||
child.parentNode=this;
|
||||
this.children.push(child);
|
||||
return child;
|
||||
}}
|
||||
insertBefore(child, ref){{
|
||||
if(child.parentNode) child.remove();
|
||||
child.parentNode=this;
|
||||
const idx=this.children.indexOf(ref);
|
||||
if(idx<0) this.children.push(child);
|
||||
else this.children.splice(idx,0,child);
|
||||
return child;
|
||||
}}
|
||||
remove(){{
|
||||
if(!this.parentNode) return;
|
||||
const siblings=this.parentNode.children;
|
||||
const idx=siblings.indexOf(this);
|
||||
if(idx>=0) siblings.splice(idx,1);
|
||||
this.parentNode=null;
|
||||
}}
|
||||
matches(selector){{ return matchesSelector(this, selector); }}
|
||||
querySelector(selector){{ return this.querySelectorAll(selector)[0]||null; }}
|
||||
querySelectorAll(selector){{
|
||||
const out=[];
|
||||
const walk=(node)=>{{
|
||||
for(const child of node.children){{
|
||||
if(matchesSelector(child, selector)) out.push(child);
|
||||
walk(child);
|
||||
}}
|
||||
}};
|
||||
walk(this);
|
||||
return out;
|
||||
}}
|
||||
closest(selector){{
|
||||
let node=this;
|
||||
while(node){{
|
||||
if(matchesSelector(node, selector)) return node;
|
||||
node=node.parentNode;
|
||||
}}
|
||||
return null;
|
||||
}}
|
||||
}}
|
||||
function matchesSelector(el, selector){{
|
||||
return String(selector||'').split(',').some(part=>matchesSimple(el, part.trim()));
|
||||
}}
|
||||
function matchesSimple(el, selector){{
|
||||
if(!selector) return false;
|
||||
selector=selector.replace(/^:scope\\s*>\\s*/, '').trim();
|
||||
if(selector.includes(' ')) selector=selector.split(/\\s+/).pop();
|
||||
const idMatch=selector.match(/#([A-Za-z0-9_-]+)/);
|
||||
if(idMatch && el.id!==idMatch[1]) return false;
|
||||
for(const match of selector.matchAll(/\\.([A-Za-z0-9_-]+)/g)){{
|
||||
if(!el._classes.has(match[1])) return false;
|
||||
}}
|
||||
for(const match of selector.matchAll(/\\[([^=\\]]+)(?:="([^"]*)")?\\]/g)){{
|
||||
const attr=match[1];
|
||||
const expected=match[2];
|
||||
if(!el.hasAttribute(attr)) return false;
|
||||
if(expected!==undefined && el.getAttribute(attr)!==expected) return false;
|
||||
}}
|
||||
return !!(idMatch || selector.includes('.') || selector.includes('['));
|
||||
}}
|
||||
function findById(root, id){{
|
||||
if(root.id===id) return root;
|
||||
for(const child of root.children){{
|
||||
const found=findById(child,id);
|
||||
if(found) return found;
|
||||
}}
|
||||
return null;
|
||||
}}
|
||||
|
||||
const emptyState=new FakeElement('div');
|
||||
const msgInner=new FakeElement('div');
|
||||
const messages=new FakeElement('div');
|
||||
messages.scrollHeight=1000;
|
||||
messages.scrollTop=1000;
|
||||
messages.clientHeight=500;
|
||||
global.document={{createElement:(tag)=>new FakeElement(tag)}};
|
||||
global.window={{}};
|
||||
global.CSS={{escape:(value)=>String(value)}};
|
||||
global.requestAnimationFrame=(fn)=>fn();
|
||||
global.S={{session:{{session_id:'sid-1', pending_started_at:123}}, activeStreamId:'stream-1'}};
|
||||
global.$=(id)=>{{
|
||||
if(id==='emptyState') return emptyState;
|
||||
if(id==='msgInner') return msgInner;
|
||||
if(id==='messages') return messages;
|
||||
return findById(msgInner,id);
|
||||
}};
|
||||
let transparentMode=true;
|
||||
global.isTransparentStream=()=>transparentMode;
|
||||
global.isCompactWorklogMode=()=>!transparentMode;
|
||||
global._anchorSceneRowsForRendering=(scene)=>scene.activity_rows||[];
|
||||
global._createAssistantTurn=()=>{{ const el=new FakeElement('div'); el.classList.add('assistant-turn'); return el; }};
|
||||
global._assistantTurnBlocks=(turn)=>turn;
|
||||
global._captureMessageScrollSnapshot=()=>({{scrollHeight:1000}});
|
||||
global._prepareLiveAnchorScrollRebuildGuard=()=>({{readerAwayFromBottom:false, release:null}});
|
||||
global._restoreMessageScrollSnapshotSameFrame=()=>{{}};
|
||||
global.scrollIfPinned=()=>{{}};
|
||||
global._moveLiveRunStatusToTurnEnd=()=>{{}};
|
||||
global._messageUserUnpinned=false;
|
||||
global._anchorSceneNodeForRow=(row)=>{{ const node=new FakeElement('div'); node.classList.add('assistant-segment'); node.textContent=row.text||''; return node; }};
|
||||
global._thinkingActivityNode=(text)=>{{ const node=new FakeElement('div'); node.classList.add('agent-activity-thinking'); node.textContent=text||''; return node; }};
|
||||
global._anchorSceneToolCallFromRow=(row)=>({{name:row.tool&&row.tool.name||row.tool_name||'tool', done:true}});
|
||||
global.buildToolCard=(toolCall)=>{{ const node=new FakeElement('div'); node.classList.add('tool-card-row'); node.setAttribute('data-tool-name',toolCall.name); return node; }};
|
||||
global._decorateTransparentEventRow=(node,opts)=>{{
|
||||
node.classList.add('transparent-event-row');
|
||||
node.setAttribute('data-transparent-event-row','1');
|
||||
node.setAttribute('data-event-type',opts.type);
|
||||
if(opts.text) node.setAttribute('data-text',opts.text);
|
||||
if(opts.name) node.setAttribute('data-tool-name',opts.name);
|
||||
if(opts.status) node.setAttribute('data-event-status',opts.status);
|
||||
return node;
|
||||
}};
|
||||
global._transparentToolStatus=()=>'Completed';
|
||||
let syncCalls=0;
|
||||
global._syncTransparentEventControls=(turn)=>{{
|
||||
syncCalls++;
|
||||
turn.setAttribute('data-sync-count',String(syncCalls));
|
||||
}};
|
||||
let compactGroups=0;
|
||||
let compactRenders=0;
|
||||
global._captureWorklogDetailDisclosureState=()=>null;
|
||||
global._restoreWorklogDetailDisclosureState=()=>{{}};
|
||||
global._startActivityElapsedTimer=()=>{{}};
|
||||
global._dedupeLiveProcessedWorklogAnchors=()=>{{}};
|
||||
global._anchorSceneWorklogGroup=(blocks, opts)=>{{
|
||||
compactGroups++;
|
||||
const group=new FakeElement('div');
|
||||
group.classList.add('tool-worklog-group');
|
||||
group.setAttribute('data-anchor-scene-owner','1');
|
||||
group.setAttribute('data-anchor-stream-id',opts.streamId||'');
|
||||
blocks.appendChild(group);
|
||||
return group;
|
||||
}};
|
||||
global._renderAnchorSceneRowsIntoWorklog=(group, rows)=>{{
|
||||
compactRenders++;
|
||||
group.setAttribute('data-rendered-count',String(rows.length));
|
||||
return true;
|
||||
}};
|
||||
global._syncToolCallGroupSummary=()=>{{}};
|
||||
|
||||
eval(extractFunc('_anchorSceneTransparentNodeForRow'));
|
||||
eval(extractFunc('renderLiveAnchorActivityScene'));
|
||||
eval(extractFunc('_renderLiveAnchorActivitySceneTransparent'));
|
||||
|
||||
const existingTurn=global._createAssistantTurn();
|
||||
existingTurn.id='liveAssistantTurn';
|
||||
existingTurn.dataset.sessionId='sid-1';
|
||||
msgInner.appendChild(existingTurn);
|
||||
const legacyOne=new FakeElement('div');
|
||||
legacyOne.setAttribute('data-live-assistant','1');
|
||||
legacyOne.textContent='legacy process one';
|
||||
existingTurn.appendChild(legacyOne);
|
||||
const legacyTwo=new FakeElement('div');
|
||||
legacyTwo.setAttribute('data-live-assistant','1');
|
||||
legacyTwo.textContent='legacy process two';
|
||||
existingTurn.appendChild(legacyTwo);
|
||||
const staleTool=new FakeElement('div');
|
||||
staleTool.classList.add('transparent-event-row');
|
||||
staleTool.classList.add('tool-card-row');
|
||||
staleTool.setAttribute('data-live-tid','legacy-tool');
|
||||
existingTurn.appendChild(staleTool);
|
||||
|
||||
const scene={{
|
||||
version:'activity_scene_v1',
|
||||
activity_rows:[
|
||||
{{row_id:'p1', role:'prose', source_event_type:'process_prose', text:'progress one'}},
|
||||
{{row_id:'t1', role:'tool', source_event_type:'tool_completed', tool:{{name:'read_file'}}, tool_call_id:'call-1'}},
|
||||
{{row_id:'r1', role:'thinking', source_event_type:'reasoning', text:'thinking'}},
|
||||
{{row_id:'p2', role:'prose', source_event_type:'process_prose', text:'progress two'}},
|
||||
],
|
||||
}};
|
||||
|
||||
assert.strictEqual(renderLiveAnchorActivityScene('stream-1', scene, {{sessionId:'sid-1'}}), true);
|
||||
assert.strictEqual(renderLiveAnchorActivityScene('stream-1', scene, {{sessionId:'sid-1'}}), true);
|
||||
const liveTurn=$('liveAssistantTurn');
|
||||
const rows=liveTurn.querySelectorAll('[data-anchor-live-scene-row="1"]');
|
||||
const staleRows=liveTurn.querySelectorAll('[data-live-tid]');
|
||||
const legacySegments=liveTurn.querySelectorAll('[data-live-assistant="1"]');
|
||||
const roles=rows.map(row=>row.getAttribute('data-anchor-row-role'));
|
||||
const eventTypes=rows.map(row=>row.getAttribute('data-anchor-source-event-type'));
|
||||
const hiddenLegacy=legacySegments.map(row=>({{
|
||||
hidden:row.hidden,
|
||||
ariaHidden:row.getAttribute('aria-hidden'),
|
||||
source:row.classList.contains('assistant-segment-worklog-source'),
|
||||
}}));
|
||||
const settledProbe=_anchorSceneTransparentNodeForRow(
|
||||
{{row_id:'settled-p1', role:'prose', source_event_type:'process_prose', text:'settled progress'}},
|
||||
{{settled:true, finalAnswer:''}}
|
||||
);
|
||||
|
||||
transparentMode=false;
|
||||
global.S.activeStreamId='stream-compact';
|
||||
delete liveTurn.dataset.sessionId;
|
||||
assert.strictEqual(renderLiveAnchorActivityScene('stream-compact', scene, {{sessionId:'sid-1'}}), true);
|
||||
|
||||
process.stdout.write(JSON.stringify({{
|
||||
rowCount:rows.length,
|
||||
roles,
|
||||
eventTypes,
|
||||
staleRowCount:staleRows.length,
|
||||
hiddenLegacy,
|
||||
settledMarker:settledProbe&&settledProbe.getAttribute('data-anchor-settled-scene-row'),
|
||||
settledLiveMarker:settledProbe&&settledProbe.getAttribute('data-anchor-live-scene-row'),
|
||||
syncCalls,
|
||||
compactGroups,
|
||||
compactRenders,
|
||||
compactSessionId:liveTurn.dataset.sessionId,
|
||||
compactRenderedCount:liveTurn.querySelector('.tool-worklog-group').getAttribute('data-rendered-count'),
|
||||
}}));
|
||||
"""
|
||||
result = _run_node_script(script)
|
||||
|
||||
assert result["rowCount"] == 4
|
||||
assert result["roles"] == ["prose", "tool", "thinking", "prose"]
|
||||
assert result["eventTypes"] == ["process_prose", "tool_completed", "reasoning", "process_prose"]
|
||||
assert result["staleRowCount"] == 0
|
||||
assert result["hiddenLegacy"] == [
|
||||
{"hidden": True, "ariaHidden": "true", "source": True},
|
||||
{"hidden": True, "ariaHidden": "true", "source": True},
|
||||
]
|
||||
assert result["settledMarker"] == "1"
|
||||
assert result["settledLiveMarker"] is None
|
||||
assert result["syncCalls"] == 2
|
||||
assert result["compactGroups"] == 1
|
||||
assert result["compactRenders"] == 1
|
||||
assert result["compactSessionId"] == "sid-1"
|
||||
assert result["compactRenderedCount"] == "4"
|
||||
|
||||
|
||||
def test_transparent_anchor_intermediate_prose_preserved_only_final_answer_suppressed():
|
||||
"""#4568 combined fix: intermediate between-tool progress prose must render in
|
||||
Transparent Stream reload (not be blanket-dropped like the first pass did);
|
||||
|
|
|
|||
Loading…
Reference in a new issue