From badb93d47299183edcb351ce98385b3ce36f4c9f Mon Sep 17 00:00:00 2001 From: Stacey2911 Date: Fri, 3 Jul 2026 18:16:56 +1000 Subject: [PATCH] fix: reduce transparent stream live row churn --- static/ui.js | 34 +++++++++- ...ssue5367_transparent_live_row_reconcile.py | 63 +++++++++++++++++++ 2 files changed, 94 insertions(+), 3 deletions(-) diff --git a/static/ui.js b/static/ui.js index 83062af0..c0217806 100644 --- a/static/ui.js +++ b/static/ui.js @@ -10945,6 +10945,7 @@ function _renderLiveAnchorActivitySceneTransparent(streamId, scene, opts){ }); const liveFooter=blocks.querySelector('#liveRunStatus'); let wrote=false; + const targetRenderedRows=[]; for(const row of rows){ const node=_anchorSceneTransparentNodeForRow(row,{ live:true, @@ -10960,10 +10961,35 @@ function _renderLiveAnchorActivitySceneTransparent(streamId, scene, opts){ : node; if(existing) preserveByKey.delete(key); if(!renderedNode) continue; - if(liveFooter&&liveFooter.parentElement===blocks) blocks.insertBefore(renderedNode,liveFooter); - else blocks.appendChild(renderedNode); + targetRenderedRows.push(renderedNode); wrote=true; } + const transparentLiveRowNextSibling=(node)=>{ + if(!node) return null; + if(typeof node.nextSibling!=='undefined') return node.nextSibling; + const parent=node.parentElement||node.parentNode; + if(parent&&parent.children&&typeof parent.children.length==='number'){ + const index=Array.prototype.indexOf.call(parent.children,node); + return index>=0 ? (parent.children[index+1]||null) : null; + } + return null; + }; + const transparentLiveRowAlreadyPositioned=(node, expectedNextSibling)=>!!( + node && + node.parentElement===blocks && + transparentLiveRowNextSibling(node)===expectedNextSibling + ); + let expectedNextSibling=(liveFooter&&liveFooter.parentElement===blocks) ? liveFooter : null; + for(let i=targetRenderedRows.length-1;i>=0;i--){ + const renderedNode=targetRenderedRows[i]; + if(transparentLiveRowAlreadyPositioned(renderedNode,expectedNextSibling)){ + expectedNextSibling=renderedNode; + continue; + } + if(expectedNextSibling&&expectedNextSibling.parentElement===blocks) blocks.insertBefore(renderedNode,expectedNextSibling); + else blocks.appendChild(renderedNode); + expectedNextSibling=renderedNode; + } preserveByKey.forEach(stale=>stale.remove()); if(wrote) _syncTransparentEventControls(turn); if(typeof _moveLiveRunStatusToTurnEnd==='function') _moveLiveRunStatusToTurnEnd(); @@ -11067,7 +11093,9 @@ function _refreshTransparentLiveRow(existing, node){ existing.setAttribute(name, value); } existing.className = node.className || ''; - existing.innerHTML = node.innerHTML || ''; + const newHtml = node.innerHTML || ''; + const htmlChanged = existing.innerHTML !== newHtml; + if(htmlChanged) existing.innerHTML = newHtml; _rehydrateTransparentLiveRow(existing, node, preservedState); return existing; } diff --git a/tests/test_issue5367_transparent_live_row_reconcile.py b/tests/test_issue5367_transparent_live_row_reconcile.py index dc6efd64..3e8c7b0e 100644 --- a/tests/test_issue5367_transparent_live_row_reconcile.py +++ b/tests/test_issue5367_transparent_live_row_reconcile.py @@ -70,6 +70,12 @@ class FakeElement {{ }} get parentElement(){{ return this.parentNode; }} get firstChild(){{ return this.children[0]||null; }} + get nextSibling(){{ + if(!this.parentNode) return null; + const siblings = this.parentNode.children; + const idx = siblings.indexOf(this); + return idx >= 0 ? (siblings[idx + 1] || null) : null; + }} get className(){{ return Array.from(this._classes).join(' '); }} @@ -123,6 +129,7 @@ class FakeElement {{ }} }} appendChild(child){{ + this.appendChildCount = (this.appendChildCount || 0) + 1; if(child && child.parentNode) child.remove(); if(!child) return null; child.parentNode = this; @@ -130,6 +137,7 @@ class FakeElement {{ return child; }} insertBefore(child, refNode){{ + this.insertBeforeCount = (this.insertBeforeCount || 0) + 1; if(child && child.parentNode) child.remove(); if(!child) return null; const idx = this.children.indexOf(refNode); @@ -299,7 +307,11 @@ const keptAfterFirst = turn.querySelector('.transparent-event-row[data-anchor-ro const staleAfterFirst = turn.querySelector('.transparent-event-row[data-anchor-row-id=\"row-stale\"]'); const firstFooter = turn.querySelector('#liveRunStatus'); +turn.insertBeforeCount = 0; +turn.appendChildCount = 0; const secondRender = _renderLiveAnchorActivitySceneTransparent('stream-1', secondScene, {{ sessionId:'session-1' }}); +const secondInsertBeforeCount = turn.insertBeforeCount || 0; +const secondAppendChildCount = turn.appendChildCount || 0; const keptAfterSecond = turn.querySelector('.transparent-event-row[data-anchor-row-id=\"row-kept\"]'); const staleAfterSecond = turn.querySelector('.transparent-event-row[data-anchor-row-id=\"row-stale\"]'); const newAfterSecond = turn.querySelector('.transparent-event-row[data-anchor-row-id=\"row-new\"]'); @@ -314,6 +326,39 @@ const idxs = {{ stale: rows.findIndex((child) => child.getAttribute('data-anchor-row-id') === 'row-stale'), }}; +turn.insertBeforeCount = 0; +turn.appendChildCount = 0; +_renderLiveAnchorActivitySceneTransparent('stream-1', secondScene, {{ sessionId:'session-1' }}); +const stableInsertBeforeCount = turn.insertBeforeCount || 0; +const stableAppendChildCount = turn.appendChildCount || 0; + +const reorderedScene = {{ + version:'activity_scene_v1', + activity_rows:[ + {{ row_id:'row-new', role:'prose', source_event_type:'process_prose', text:'new row appears' }}, + {{ row_id:'row-kept', role:'prose', source_event_type:'process_prose', text:'updated progress line' }}, + ], +}}; +turn.insertBeforeCount = 0; +turn.appendChildCount = 0; +_renderLiveAnchorActivitySceneTransparent('stream-1', reorderedScene, {{ sessionId:'session-1' }}); +const reorderInsertBeforeCount = turn.insertBeforeCount || 0; +const reorderedIds = turn.children + .filter((child) => child.classList.contains('transparent-event-row')) + .map((child) => child.getAttribute('data-anchor-row-id')); + +firstFooter.remove(); +turn.insertBefore(firstFooter, turn.children[0] || null); +turn.insertBeforeCount = 0; +turn.appendChildCount = 0; +_renderLiveAnchorActivitySceneTransparent('stream-1', reorderedScene, {{ sessionId:'session-1' }}); +const footerRepairInsertBeforeCount = turn.insertBeforeCount || 0; +const footerRepairIdxs = {{ + newRow: turn.children.indexOf(turn.querySelector('.transparent-event-row[data-anchor-row-id=\"row-new\"]')), + keptRow: turn.children.indexOf(turn.querySelector('.transparent-event-row[data-anchor-row-id=\"row-kept\"]')), + footer: turn.children.indexOf(firstFooter), +}}; + _renderLiveAnchorActivitySceneTransparent('stream-1', thirdScene, {{ sessionId:'session-1' }}); const keylessRowsAfterThird = turn.querySelectorAll('.transparent-event-row[data-anchor-row-id=\"\"]'); _renderLiveAnchorActivitySceneTransparent('stream-1', fourthScene, {{ sessionId:'session-1' }}); @@ -329,6 +374,14 @@ process.stdout.write(JSON.stringify({{ staleGone: staleAfterSecond === null, staleAfterFirst: staleAfterFirst !== null, idxs, + secondInsertBeforeCount, + secondAppendChildCount, + stableInsertBeforeCount, + stableAppendChildCount, + reorderInsertBeforeCount, + reorderedIds, + footerRepairInsertBeforeCount, + footerRepairIdxs, hasNewRow: !!newAfterSecond, newRowSession: newAfterSecond && newAfterSecond.getAttribute('data-session-id'), keylessAfterThird: keylessRowsAfterThird.length, @@ -355,6 +408,16 @@ process.stdout.write(JSON.stringify({{ assert data["idxs"]["rowNew"] == 1 assert data["idxs"]["stale"] == -1 assert data["idxs"]["staleInVisibleRows"] == -1 + assert data["secondInsertBeforeCount"] >= 1 + assert data["secondAppendChildCount"] == 0 + assert data["stableInsertBeforeCount"] == 0 + assert data["stableAppendChildCount"] == 0 + assert data["reorderInsertBeforeCount"] >= 1 + assert data["reorderedIds"] == ["row-new", "row-kept"] + assert data["footerRepairInsertBeforeCount"] >= 1 + assert data["footerRepairIdxs"]["newRow"] == 0 + assert data["footerRepairIdxs"]["keptRow"] == 1 + assert data["footerRepairIdxs"]["footer"] > data["footerRepairIdxs"]["keptRow"] assert data["hasNewRow"] is True assert data["newRowSession"] == "session-1" assert data["keylessAfterThird"] == 1