Stage 397: PR #2688 — fix: resync virtualized sidebar after scroll clamp

Co-authored-by: ai-ag2026 <ai-ag2026@users.noreply.github.com>
This commit is contained in:
Hermes Agentandai-ag2026 committed 2026-05-21 17:14:36 +00:00
1 parent d5dcd60981
commit e2338f696f
2 files changed
+69

No files matched your search

+17
View File
@@ -2932,6 +2932,22 @@ function _markSessionListPointerUp(){
if(_pendingSessionListPayload) _schedulePendingSessionListApply();
}
let _sessionVirtualResyncRaf = 0;
function _resyncSessionVirtualWindowAfterRender(list, expectedScrollTop, virtualWindow){
if(!list||!virtualWindow||!virtualWindow.virtualized) return;
expectedScrollTop=Number(expectedScrollTop)||0;
if(expectedScrollTop<=0) return;
if(_sessionVirtualResyncRaf) cancelAnimationFrame(_sessionVirtualResyncRaf);
_sessionVirtualResyncRaf=requestAnimationFrame(()=>{
_sessionVirtualResyncRaf=0;
if(_renamingSid) return;
const actualScrollTop=Number(list.scrollTop)||0;
const tolerance=Math.max(2, Number(virtualWindow.itemHeight||SESSION_VIRTUAL_ROW_HEIGHT)/2);
if(Math.abs(actualScrollTop-expectedScrollTop)<=tolerance) return;
renderSessionListFromCache();
});
}
function renderSessionListFromCache(){
// Don't re-render while user is actively renaming a session (would destroy the input)
if(_renamingSid) return;
@@ -3214,6 +3230,7 @@ function renderSessionListFromCache(){
// scrollTop drops to 0 — producing a "scroll keeps jumping back" feel
// when the list scrolls naturally. Fixed for #1669 follow-up.
list.scrollTop=listScrollTopBeforeRender;
_resyncSessionVirtualWindowAfterRender(list, listScrollTopBeforeRender, virtualWindow);
}
// Select mode toggle button (only when NOT in select mode)
if(!_sessionSelectMode){
@@ -138,3 +138,55 @@ def test_session_list_only_moves_to_active_when_active_row_is_not_visible():
assert before_idx < visible_idx < move_idx < final_idx < anchor_idx
assert "activeIndex:-1" in render_body[before_idx:visible_idx]
assert "activeIndex:shouldAnchorActive?activeIndex:-1" not in render_body
def test_session_list_resyncs_when_browser_clamps_virtual_scroll_restore():
"""If a hidden/reflowed sidebar rejects restored scrollTop, re-render the visible window."""
js = SESSIONS_JS_PATH.read_text(encoding="utf-8")
render_start = js.index("function renderSessionListFromCache()")
render_end = js.index("async function _handleActiveSessionStorageEvent", render_start)
render_body = js[render_start:render_end]
assert "_resyncSessionVirtualWindowAfterRender(list, listScrollTopBeforeRender, virtualWindow);" in render_body
source = _extract_func_script(js) + """
let renderCount = 0;
let rafCount = 0;
let _renamingSid = null;
const SESSION_VIRTUAL_ROW_HEIGHT = 52;
function requestAnimationFrame(cb){ rafCount += 1; cb(); return rafCount; }
function cancelAnimationFrame(_id){}
function renderSessionListFromCache(){ renderCount += 1; }
const makeHelper = new Function(
'requestAnimationFrame',
'cancelAnimationFrame',
'renderSessionListFromCache',
`let _sessionVirtualResyncRaf = 0;
let _renamingSid = null;
const SESSION_VIRTUAL_ROW_HEIGHT = 52;
${extractFunc('_resyncSessionVirtualWindowAfterRender')}
return _resyncSessionVirtualWindowAfterRender;`
);
const _resyncSessionVirtualWindowAfterRender = makeHelper(
requestAnimationFrame,
cancelAnimationFrame,
renderSessionListFromCache
);
_resyncSessionVirtualWindowAfterRender(
{scrollTop: 0},
52 * 10,
{virtualized: true, itemHeight: 52}
);
const afterClamp = renderCount;
_resyncSessionVirtualWindowAfterRender(
{scrollTop: 52 * 10},
52 * 10,
{virtualized: true, itemHeight: 52}
);
console.log(JSON.stringify({afterClamp, final: renderCount, rafCount}));
"""
metrics = json.loads(_run_node(source))
assert metrics["afterClamp"] == 1
assert metrics["final"] == 1
assert metrics["rafCount"] == 2