let stickyBottom = true; const chatEl = () => document.getElementById('chat'); function isNearBottom(){ const c = chatEl(); return c.scrollHeight - c.scrollTop - c.clientHeight < 60; } // La zone de chat réserve une gouttière de barre de défilement de chaque côté // (scrollbar-gutter: stable both-edges) ; le composer, lui, n'est pas défilant. // On mesure la gouttière réelle et on la reporte sur le composer, sinon les // messages sont en retrait par rapport à la zone de saisie — décalage visible // surtout quand la barre latérale est escamotée. function syncGutter(){ const chat=document.getElementById('chat'); if(!chat) return; const g=Math.max(0,(chat.offsetWidth-chat.clientWidth)/2); document.documentElement.style.setProperty('--sbw', g+'px'); } addEventListener('resize', syncGutter); addEventListener('DOMContentLoaded', syncGutter); function scrollMaybe(){ // Pendant le replay initial on NE force AUCUN reflow : lire scrollHeight à chaque // événement rejoué = un layout synchrone forcé sur un DOM qui grossit → coût // quadratique (20-30 s de rendu au refresh sur un long fil). Le scroll est fait // une seule fois à la fin du replay, via jumpBottom() au signal {caught_up}. if(REPLAYING) return; if(stickyBottom){ const c = chatEl(); c.scrollTop = c.scrollHeight; } document.getElementById('scrollbtn').classList.toggle('show', !stickyBottom); } function jumpBottom(){ stickyBottom = true; scrollMaybe(); } document.addEventListener('DOMContentLoaded', ()=>{ const c = chatEl(); c.addEventListener('scroll', ()=>{ stickyBottom = isNearBottom(); document.getElementById('scrollbtn').classList.toggle('show', !stickyBottom); }); }); function addMsg(role, text){ const el=document.createElement('div'); el.className='msg '+role; const collapsible = (role==='reasoning' || role==='tool'); // .body must be a real block so

/

/