fix(#3920): apply Codex gate findings — observe #msgInner, instance-owned RO cleanup, restore .messages overflow-anchor

Codex SHIP-ONLY-WITH-FIXES (3 real issues; Codex+Opus disagreed on #1, stricter wins):
1. CORE — .messages lost overflow-anchor:none, reopening the #1360 (d21c9720) streaming-scroll-anchor regression. Restored it (native anchoring stays OFF; the RO does the settle). Opus had judged this removal acceptable; took Codex's stricter call.
2. SILENT — the ResizeObserver observed #messages (the scroll container, fixed by flex layout, never resizes) instead of #msgInner (.messages-inner, the growing transcript node) → callback would never fire. Now observes #msgInner.
3. SILENT — stale RO callbacks mutated the global _settleRO, which could disconnect a newer active observer after settle re-entry. Now instance-owned: close over const ro, only clear the global when _settleRO===ro.
This commit is contained in:
nesquena-hermes
2026-06-13 03:32:16 +00:00
parent f9055c5d97
commit b5eabe1494
2 changed files with 16 additions and 7 deletions

View File

@@ -1588,7 +1588,7 @@
.workspace-toggle-btn:disabled{opacity:.38;cursor:not-allowed;}
.chip.model{color:var(--accent-text);border-color:var(--accent-bg-strong);background:var(--accent-bg);}
.messages-shell{flex:1;min-height:0;position:relative;display:flex;flex-direction:column;}
.messages{flex:1;overflow-y:auto;display:flex;flex-direction:column;min-height:0;position:relative;z-index:0;-webkit-overflow-scrolling:touch;touch-action:pan-y;overscroll-behavior-y:contain;}
.messages{flex:1;overflow-y:auto;display:flex;flex-direction:column;min-height:0;position:relative;z-index:0;-webkit-overflow-scrolling:touch;touch-action:pan-y;overscroll-behavior-y:contain;overflow-anchor:none;}
/* Overlay scroll controls so they do not affect the transcript's native scroll geometry. */
.scroll-to-bottom-btn{position:absolute;right:20px;bottom:16px;width:32px;height:32px;border-radius:50%;border:1px solid var(--border2);background:var(--code-bg);color:var(--muted);font-size:16px;cursor:pointer;display:flex;align-items:center;justify-content:center;box-shadow:0 2px 8px rgba(0,0,0,.25);z-index:10;transition:color .12s,border-color .12s,background .12s,transform .12s;}
.scroll-to-bottom-btn:hover{color:var(--text);border-color:var(--border);background:var(--hover-bg);}

View File

@@ -3139,11 +3139,19 @@ function _settleMessageScrollToBottom(force){
const el=document.getElementById('messages');
if(!el) return;
// Observe the GROWING content node, not the scroll container. #messages is the
// scroller but its box is fixed by the flex layout, so it never resizes — the
// transcript grows inside #msgInner (.messages-inner). Observing #messages
// would mean the callback never fires. (Codex review #2.)
const observed=document.getElementById('msgInner')||el;
_settleRO=new ResizeObserver(()=>{
if(token!==_bottomSettleToken){ if(_settleRO){ _settleRO.disconnect(); _settleRO=null; } return; }
// Instance-owned cleanup: close over THIS observer so a stale callback (from a
// superseded settle) only ever disconnects its own observer, never the newer
// 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(!_scrollPinned||_messageUserUnpinned||_recentNonMessageScrollIntent()){
if(_settleRO){ _settleRO.disconnect(); _settleRO=null; }
ro.disconnect(); if(_settleRO===ro) _settleRO=null;
_programmaticScroll=false;
return;
}
@@ -3158,11 +3166,12 @@ function _settleMessageScrollToBottom(force){
clearTimeout(_settleTimer);
_settleTimer=setTimeout(()=>{
if(token!==_bottomSettleToken) return;
if(_settleRO){ _settleRO.disconnect(); _settleRO=null; }
ro.disconnect(); if(_settleRO===ro) _settleRO=null;
_setMessageScrollToBottom();
},300);
});
_settleRO.observe(el);
_settleRO=ro;
ro.observe(observed);
// Static-content safety net: a fully-static response (no Prism/KaTeX/Mermaid/
// late images) never resizes after the initial sync write, so the
@@ -3173,7 +3182,7 @@ function _settleMessageScrollToBottom(force){
clearTimeout(_settleFinalTimer);
_settleFinalTimer=setTimeout(()=>{
if(token!==_bottomSettleToken) return;
if(_settleRO){ _settleRO.disconnect(); _settleRO=null; }
ro.disconnect(); if(_settleRO===ro) _settleRO=null;
if(!_scrollPinned||_messageUserUnpinned||_recentNonMessageScrollIntent()){ _programmaticScroll=false; return; }
_settleFinalScroll(token);
},2000);