fix(#3920): address review — restore sidebar overflow-anchor, wire _settleFinalScroll via 2s fallback, + CHANGELOG v0.51.377

3 maintainer-review items the contributor hadn't addressed:
1. (blocking) restored .session-list overflow-anchor:none — the PR accidentally reverted the deliberate Firefox sidebar fix (2dfe765b); only the on-topic .messages removal is kept.
2/3. _settleFinalScroll was dead code + the promised 2s static-content safety timeout was missing — wired _settleFinalScroll(token) via a single 2s top-level fallback after _settleRO.observe(el), declared _settleFinalTimer + cleared it in _cancelBottomSettle and at settle re-entry.
This commit is contained in:
nesquena-hermes
2026-06-13 03:25:48 +00:00
parent 965baeb78c
commit f9055c5d97
3 changed files with 24 additions and 2 deletions

View File

@@ -3,6 +3,12 @@
## [Unreleased]
## [v0.51.377] — 2026-06-13 — Release MP (Firefox post-stream scroll jitter, #3920)
### Fixed
- **Firefox no longer jitters/steps the chat scroll after a streamed turn settles (#3920).** The post-stream bottom-settle replaced its `setTimeout` fan-out + double-`requestAnimationFrame` `scrollHeight` polling (which forced a reflow per frame on Firefox) with a single synchronous write plus a `ResizeObserver` that re-anchors once per frame as late layout (Prism/KaTeX/Mermaid/images) grows, disconnecting after 300ms of quiet. A 2s top-level fallback covers fully-static responses that never resize, and the deliberate Firefox session-sidebar `overflow-anchor:none` fix is preserved. (#3920)
## [v0.51.376] — 2026-06-13 — Release MO (Hide Thinking also hides Worklog reasoning, #3903)
### Fixed

View File

@@ -992,7 +992,7 @@
.sidebar-section{padding:14px 14px 8px;}
.new-chat-btn{width:100%;padding:9px 12px;border-radius:9px;background:var(--accent-bg);border:1px solid var(--accent-bg-strong);color:var(--accent-text);font-size:13px;cursor:pointer;display:flex;align-items:center;gap:8px;transition:all .15s;margin-bottom:8px;font-weight:500;}
.new-chat-btn:hover{background:var(--accent-bg-strong);border-color:var(--accent);}
.session-list{flex:1;overflow-y:auto;padding:0 8px 8px;min-height:0;overscroll-behavior-y:contain;touch-action:pan-y;}
.session-list{flex:1;overflow-y:auto;padding:0 8px 8px;min-height:0;overscroll-behavior-y:contain;touch-action:pan-y;overflow-anchor:none;}
.sidebar-search{position:relative;padding:8px 12px;flex-shrink:0;}
.session-search-field{position:relative;display:flex;align-items:center;width:100%;}
.sidebar-search input{width:100%;background:var(--bg);border:1px solid var(--border);border-radius:8px;color:var(--text);padding:7px 10px 7px 32px;font-size:13px;outline:none;transition:border-color .15s,box-shadow .15s,background .15s;box-sizing:border-box;}

View File

@@ -2428,10 +2428,11 @@ let _bottomSettleToken=0;
let _settleRAF=0;
let _settleRO=null;
let _settleTimer=0;
let _settleFinalTimer=0;
const NON_MESSAGE_SCROLL_INTENT_SUPPRESS_MS=350;
let _touchStartY=null;
let _newMessageCueVisible=false;
function _cancelBottomSettle(){ _bottomSettleToken++; if(_settleRO){ _settleRO.disconnect(); _settleRO=null; } clearTimeout(_settleTimer); cancelAnimationFrame(_settleRAF); }
function _cancelBottomSettle(){ _bottomSettleToken++; if(_settleRO){ _settleRO.disconnect(); _settleRO=null; } clearTimeout(_settleTimer); clearTimeout(_settleFinalTimer); cancelAnimationFrame(_settleRAF); }
function _recordNonMessageScrollIntent(e){
const el=document.getElementById('messages');
const target=e&&e.target;
@@ -3129,6 +3130,7 @@ function _settleMessageScrollToBottom(force){
cancelAnimationFrame(_settleRAF);
if(_settleRO){ _settleRO.disconnect(); _settleRO=null; }
clearTimeout(_settleTimer);
clearTimeout(_settleFinalTimer);
// Sync write anchors the viewport immediately.
_setMessageScrollToBottom();
@@ -3161,6 +3163,20 @@ function _settleMessageScrollToBottom(force){
},300);
});
_settleRO.observe(el);
// Static-content safety net: a fully-static response (no Prism/KaTeX/Mermaid/
// late images) never resizes after the initial sync write, so the
// ResizeObserver callback above never fires and its 300ms quiet-timer is never
// armed. Arm a single 2s top-level fallback so a late settle still runs for
// that case. The token check inside _settleFinalScroll makes this a no-op if a
// newer settle started, and it self-skips if the user unpinned. (Review #2/#3.)
clearTimeout(_settleFinalTimer);
_settleFinalTimer=setTimeout(()=>{
if(token!==_bottomSettleToken) return;
if(_settleRO){ _settleRO.disconnect(); _settleRO=null; }
if(!_scrollPinned||_messageUserUnpinned||_recentNonMessageScrollIntent()){ _programmaticScroll=false; return; }
_settleFinalScroll(token);
},2000);
}
function _settleFinalScroll(token){