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'); } // La carte de saisie flotte AU-DESSUS du fil (position:absolute) : c'est au fil // de réserver sa hauteur en rembourrage bas, sinon ses derniers messages passent // dessous et deviennent hors d'atteinte — aucun défilement ne les ramène. // Cette hauteur n'est pas constante : la saisie grandit avec le texte (autoGrow), // et une pièce jointe en attente ajoute une rangée de pastilles. On la mesure // donc au lieu de la deviner, comme on le fait déjà pour la gouttière. // Pas de boucle d'observation : le composeur est en position:absolute, changer le // rembourrage de #chat ne modifie pas sa hauteur. function syncComposer(){ const c=document.getElementById('composer'); if(!c) return; document.documentElement.style.setProperty('--composer-h', c.offsetHeight+'px'); } addEventListener('resize', syncGutter); addEventListener('DOMContentLoaded', syncGutter); addEventListener('resize', syncComposer); addEventListener('DOMContentLoaded', ()=>{ syncComposer(); const c=document.getElementById('composer'); if(c && window.ResizeObserver) new ResizeObserver(syncComposer).observe(c); }); 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); }); }); // Badge de rôle du mode code (builder / verifier) : posé par 09-stream.js à // réception d'un delta {role}, repris par addMsg sur les bulles suivantes. let ROLE_BADGE=''; function addMsg(role, text){ const el=document.createElement('div'); el.className='msg '+role; if(ROLE_BADGE && (role==='assistant'||role==='reasoning'||role==='tool')) el.dataset.agentRole=ROLE_BADGE; const collapsible = (role==='reasoning' || role==='tool'); // .body must be a real block so

/

/