fix(#4702): co-seed _lastMessageClientHeight with every programmatic _lastScrollTop write (Codex gate)

Codex found the guard had a first-event gap: programmatic seeds of _lastScrollTop
(_setMessageScrollToBottom, scrollToBottom, snapshot restore paths) didn't seed
_lastMessageClientHeight, so the FIRST native toolbar-collapse scroll saw a null
prior height -> grew=false -> the false-unpin still fired. Now every programmatic
_lastScrollTop=el.scrollTop is paired with _lastMessageClientHeight=el.clientHeight.
This commit is contained in:
nesquena-hermes committed 2026-06-24 10:02:52 +00:00
1 parent fe40912eb9
commit 671f4504ae
2 files changed
+19 -5

No files matched your search

+5 -5
View File
@@ -4472,7 +4472,7 @@ function _setMessageScrollToBottom(){
if(!el) return; if(!el) return;
_programmaticScroll=true;_programmaticScrollSetAt=performance.now(); _programmaticScroll=true;_programmaticScrollSetAt=performance.now();
el.scrollTop=el.scrollHeight; el.scrollTop=el.scrollHeight;
_lastScrollTop=el.scrollTop; _lastScrollTop=el.scrollTop;_lastMessageClientHeight=el.clientHeight;
_nearBottomCount=2; _nearBottomCount=2;
_scrollPinned=true; _scrollPinned=true;
requestAnimationFrame(()=>{ requestAnimationFrame(()=>{
@@ -4487,7 +4487,7 @@ function _setMessageScrollToBottom(){
return; return;
} }
el.scrollTop=el.scrollHeight; el.scrollTop=el.scrollHeight;
_lastScrollTop=el.scrollTop; _lastScrollTop=el.scrollTop;_lastMessageClientHeight=el.clientHeight;
_nearBottomCount=2; _nearBottomCount=2;
_scrollPinned=true; _scrollPinned=true;
_deferClearProgrammaticScroll(); _deferClearProgrammaticScroll();
@@ -4612,7 +4612,7 @@ function _settleFinalScroll(token){
} }
_programmaticScroll=true;_programmaticScrollSetAt=performance.now(); _programmaticScroll=true;_programmaticScrollSetAt=performance.now();
el.scrollTop=el.scrollHeight; el.scrollTop=el.scrollHeight;
_lastScrollTop=el.scrollTop; _lastScrollTop=el.scrollTop;_lastMessageClientHeight=el.clientHeight;
_nearBottomCount=2; _nearBottomCount=2;
_scrollPinned=true; _scrollPinned=true;
_deferClearProgrammaticScroll(); _deferClearProgrammaticScroll();
@@ -11002,7 +11002,7 @@ function _restoreMessageScrollSnapshot(snapshot){
el.scrollTop=Math.max(0,Math.min(Number(snapshot.top)||0,maxTop)); el.scrollTop=Math.max(0,Math.min(Number(snapshot.top)||0,maxTop));
} }
// Sync _lastScrollTop after programmatic restore so sticky-unpin does not false-trigger (#1731). // Sync _lastScrollTop after programmatic restore so sticky-unpin does not false-trigger (#1731).
_lastScrollTop=el.scrollTop; _lastScrollTop=el.scrollTop;_lastMessageClientHeight=el.clientHeight;
if(snapshot.userUnpinned===true){ if(snapshot.userUnpinned===true){
_messageUserUnpinned=true; _messageUserUnpinned=true;
_scrollPinned=false; _scrollPinned=false;
@@ -11068,7 +11068,7 @@ function _restoreMessageScrollSnapshotSameFrame(snapshot){
_programmaticScroll=true;_programmaticScrollSetAt=performance.now(); _programmaticScroll=true;_programmaticScrollSetAt=performance.now();
el.scrollTop=Math.max(0,Math.min(target,maxTop)); el.scrollTop=Math.max(0,Math.min(target,maxTop));
} }
_lastScrollTop=el.scrollTop; _lastScrollTop=el.scrollTop;_lastMessageClientHeight=el.clientHeight;
if(snapshot.pinned===true){ if(snapshot.pinned===true){
_messageUserUnpinned=false; _messageUserUnpinned=false;
_scrollPinned=true; _scrollPinned=true;
@@ -18,6 +18,20 @@ REPO = pathlib.Path(__file__).resolve().parent.parent
UI_JS = (REPO / "static" / "ui.js").read_text(encoding="utf-8") UI_JS = (REPO / "static" / "ui.js").read_text(encoding="utf-8")
def test_client_height_seeded_with_scrolltop_on_programmatic_writes():
"""Every programmatic seed of `_lastScrollTop` must also seed
`_lastMessageClientHeight`, else the FIRST native toolbar-collapse scroll event
sees a null/stale prior height, `grew` is false, and the false-unpin still
fires (Codex gate finding). The two must always be written together."""
# No bare `_lastScrollTop=el.scrollTop;` without the height co-seed remains.
assert "_lastScrollTop=el.scrollTop;\n" not in UI_JS, (
"A programmatic _lastScrollTop=el.scrollTop write is missing the paired "
"_lastMessageClientHeight=el.clientHeight seed (#4702)."
)
# The paired form is present at the programmatic-write sites.
assert UI_JS.count("_lastScrollTop=el.scrollTop;_lastMessageClientHeight=el.clientHeight;") >= 4
def test_client_height_growth_guard_declared(): def test_client_height_growth_guard_declared():
"""The scroller-height tracker must be declared so a toolbar-settle reflow can """The scroller-height tracker must be declared so a toolbar-settle reflow can
be distinguished from a real user scroll.""" be distinguished from a real user scroll."""