agent terminal: sticky-bottom auto-scroll with new-row pill

new rows no longer yank the view if the operator is scrolled up.
threshold for 'near bottom' is 48px. when not near bottom, an amber
'↓ N new' pill appears in the bottom-right of the terminal-wrap;
clicking it jumps to bottom. scrolling back near bottom clears the
counter. backfilled (history-replay) rows always scroll to bottom
since the operator hasn't started reading yet.
This commit is contained in:
müde 2026-05-15 19:30:34 +02:00
parent 875a8f5be4
commit 08f2ec5232
2 changed files with 78 additions and 2 deletions

View file

@ -331,13 +331,58 @@
row: (cls, text) => row(cls, text),
clear: () => { log.innerHTML = ''; placeholder = null; },
};
// Sticky-bottom auto-scroll. If the user is reading scrolled-up, new
// rows do NOT yank the view. A floating "↓ N new" pill appears in
// the bottom-right corner; clicking it jumps to bottom and clears
// the counter. Scrolling back near the bottom also clears it.
const NEAR_BOTTOM_PX = 48;
let unseen = 0;
let pill = null;
function isNearBottom() {
return log.scrollHeight - log.scrollTop - log.clientHeight <= NEAR_BOTTOM_PX;
}
function ensurePill() {
if (pill) return pill;
pill = document.createElement('button');
pill.type = 'button';
pill.className = 'tail-pill';
pill.addEventListener('click', () => {
log.scrollTop = log.scrollHeight;
});
log.parentElement.appendChild(pill);
return pill;
}
function updatePill() {
if (unseen <= 0) {
if (pill) pill.classList.remove('visible');
return;
}
ensurePill();
pill.textContent = '↓ ' + unseen + ' new';
pill.classList.add('visible');
}
log.addEventListener('scroll', () => {
if (isNearBottom()) {
unseen = 0;
updatePill();
}
});
function afterAppend() {
if (currentNoAnim || isNearBottom()) {
log.scrollTop = log.scrollHeight;
} else {
unseen += 1;
updatePill();
}
}
function row(cls, text) {
clearPlaceholder();
const e = document.createElement('div');
e.className = 'row ' + (cls || '') + (currentNoAnim ? ' no-anim' : '');
e.textContent = text;
log.appendChild(e);
log.scrollTop = log.scrollHeight;
afterAppend();
return e;
}
function details(cls, summary, body) {
@ -352,7 +397,7 @@
pre.textContent = body;
d.appendChild(pre);
log.appendChild(d);
log.scrollTop = log.scrollHeight;
afterAppend();
return d;
}
function trim(s, n) { return s.length > n ? s.slice(0, n) + '…' : s; }