fix(#4006): explicit-settle flag + pre-wipe snapshot capture (Codex r3)

1. _settleMessageScrollToBottom(force, explicit): when explicit (End-button /
   scrollToBottom), late-layout settling runs even with Auto-follow OFF — the
   setting only suppresses AUTOMATIC follow, not a deliberate jump. The RO + 2s
   fallback guards now bail on (!_autoScrollFollow && !explicit). scrollToBottom()
   passes explicit=true; the automatic scrollIfPinned() path stays guarded.
2. renderMessages now captures the pre-wipe scrollSnapshot when (Auto-follow off
   && user unpinned) too, so the non-preserve restore lands the reader where they
   were instead of receiving a null snapshot (no-op).
This commit is contained in:
nesquena-hermes
2026-06-13 04:16:48 +00:00
parent 36239700e6
commit 6ced477822

View File

@@ -3112,8 +3112,11 @@ function _followMessagesAfterDomReplace(){
}
return false;
}
function _settleMessageScrollToBottom(force){
// Markdown post-processing (Prism, tables, Mermaid/KaTeX/PDF placeholders)
function _settleMessageScrollToBottom(force, explicit){
// `explicit` = a user-invoked scroll-to-bottom (End button / scrollToBottom()).
// When explicit, late-layout settling runs even if Auto-follow is OFF — the
// setting only suppresses AUTOMATIC streaming follow, not a deliberate jump
// to the bottom. (Codex #4006 r3.)
// can grow the transcript after the first scroll write. Re-apply the bottom
// position when content settles so late layout does not leave the viewport
// above the real end. User scroll increments _bottomSettleToken and cancels.
@@ -3150,7 +3153,7 @@ function _settleMessageScrollToBottom(force){
// active one that may now be in the global _settleRO. (Codex review #3.)
const ro=new ResizeObserver(()=>{
if(token!==_bottomSettleToken){ ro.disconnect(); if(_settleRO===ro) _settleRO=null; return; }
if(!_autoScrollFollow||!_scrollPinned||_messageUserUnpinned||_recentNonMessageScrollIntent()){
if((!_autoScrollFollow&&!explicit)||!_scrollPinned||_messageUserUnpinned||_recentNonMessageScrollIntent()){
ro.disconnect(); if(_settleRO===ro) _settleRO=null;
_programmaticScroll=false;
return;
@@ -3183,7 +3186,7 @@ function _settleMessageScrollToBottom(force){
_settleFinalTimer=setTimeout(()=>{
if(token!==_bottomSettleToken) return;
ro.disconnect(); if(_settleRO===ro) _settleRO=null;
if(!_autoScrollFollow||!_scrollPinned||_messageUserUnpinned||_recentNonMessageScrollIntent()){ _programmaticScroll=false; return; }
if((!_autoScrollFollow&&!explicit)||!_scrollPinned||_messageUserUnpinned||_recentNonMessageScrollIntent()){ _programmaticScroll=false; return; }
_settleFinalScroll(token);
},2000);
}
@@ -3219,7 +3222,7 @@ function scrollToBottom(){
// was skipping the observer and causing Firefox paint jumps when
// renderMessages({preserveScroll:true}) + scrollToBottom() fired back-to-back.
_setMessageScrollToBottom();
_settleMessageScrollToBottom(false);
_settleMessageScrollToBottom(false, true);
_syncScrollToBottomCue(false,{newMessage:false});
if(typeof _updateSessionStartJumpButton==='function') _updateSessionStartJumpButton();
}
@@ -8655,6 +8658,8 @@ function _scrollAfterMessageRender(preserveScroll, scrollSnapshot){
// renderMessages() runs before S.activeStreamId is set, so a stream-start
// wouldn't otherwise be caught by the scrollIfPinned() branch above. A fresh
// session load (not unpinned) still lands at the bottom as expected. (Codex #4006.)
// renderMessages() captures the pre-wipe snapshot for this case too (see its
// scrollSnapshot init), so restoring here lands the reader where they were.
if(!_autoScrollFollow && _messageUserUnpinned){
_restoreMessageScrollSnapshot(scrollSnapshot);
return;
@@ -8664,7 +8669,10 @@ function _scrollAfterMessageRender(preserveScroll, scrollSnapshot){
function renderMessages(options){
const preserveScroll=!!(options&&options.preserveScroll);
const scrollSnapshot=preserveScroll?_captureMessageScrollSnapshot():null;
// Capture the pre-wipe scroll position when preserving OR when Auto-follow is
// off and the user has scrolled up — both need to restore the reader's position
// after the DOM rebuild rather than snap to the bottom. (Codex #4006 r3.)
const scrollSnapshot=(preserveScroll||(!_autoScrollFollow&&_messageUserUnpinned))?_captureMessageScrollSnapshot():null;
const inner=$('msgInner');
const sid=S.session?S.session.session_id:null;
const msgCount=S.messages.length;