Bug Description
Reporter: Cygnus (Discord; please ping in replies).
While an assistant response is streaming, the message viewport remains pinned to the bottom unless the user scrolls upward several times. A small/normal upward scroll near the bottom is immediately overridden by the next streamed text update.
Cygnus described it as:
It's still a fight to scroll up while the AI is printing. I can do it if I scroll upward like 3 or 4 times with determination lol, eventually it lets me, but it's annoying every time. I only want it to scroll with the printing text if I'm all the way at the bottom.
Validation
Validated against current origin/master (d8cd556, checked out in a clean worktree) on 2026-05-05.
The current scroll pinning logic in static/ui.js treats the user as still pinned when the viewport is within 250px of the bottom:
const nearBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 250;
_nearBottomCount = nearBottom ? _nearBottomCount + 1 : 0;
_scrollPinned = _nearBottomCount >= 2;
Because scrollIfPinned() only checks _scrollPinned, an upward scroll that lands inside that 250px zone gets snapped back to the bottom by the next streamed token. This matches the observed “scroll upward 3 or 4 times” behavior: the user has to escape the 250px near-bottom hysteresis zone before auto-scroll finally stops.
Browser-console reproduction on a temporary local WebUI server:
// Seed a tall transcript, scroll to bottom, then simulate streamed tokens while
// repeatedly attempting a small upward scroll.
const el = document.getElementById('messages');
const inner = document.getElementById('msgInner');
inner.innerHTML = '';
for (let i = 0; i < 80; i++) {
const d = document.createElement('div');
d.textContent = 'history line ' + i;
d.style.padding = '12px';
inner.appendChild(d);
}
const live = document.createElement('div');
live.id = 'live-test';
live.style.padding = '12px';
inner.appendChild(live);
scrollToBottom();
for (let t = 0; t < 12; t++) {
el.scrollTop = Math.max(0, el.scrollTop - 120); // user scrolls up
el.dispatchEvent(new Event('scroll', { bubbles: true }));
await new Promise(r => requestAnimationFrame(r));
live.textContent += ' token' + t;
scrollIfPinned(); // next streaming update
}
Observed sample: every iteration ended with bottomDelta: 0, i.e. the viewport was forced back to the bottom despite the user scroll attempt.
Expected Behavior
Auto-scroll should continue only when the user is actually at the bottom (or within a tiny tolerance for sub-pixel/layout jitter). Any intentional upward scroll should immediately unpin streaming auto-scroll, even if the viewport is still only 100-200px from the bottom.
Actual Behavior
The UI uses a broad <250px from bottom threshold to remain/re-pin. This makes small upward scrolls ineffective during streaming until the user scrolls far enough away from the bottom.
Notes / Related Issues
Likely related to the earlier closed auto-scroll issues (#677, #1360, #1469), but this is the remaining threshold/hysteresis behavior rather than the exact previous finish-time jump (#1690).
A likely fix is to separate the concepts:
- Remain pinned: only if the viewport is at/near the exact bottom, with a very small tolerance.
- Offer “scroll to bottom” / optionally re-pin: can use a wider threshold, but should not silently override an explicit upward scroll during streaming.
Environment
- WebUI current
origin/master (d8cd556)
- Browser validation: Chromium against temporary local WebUI server
- Reporter environment: Safari/macOS or iOS Safari likely involved, but the underlying threshold behavior reproduces without Safari-specific APIs.
Bug Description
Reporter: Cygnus (Discord; please ping in replies).
While an assistant response is streaming, the message viewport remains pinned to the bottom unless the user scrolls upward several times. A small/normal upward scroll near the bottom is immediately overridden by the next streamed text update.
Cygnus described it as:
Validation
Validated against current
origin/master(d8cd556, checked out in a clean worktree) on 2026-05-05.The current scroll pinning logic in
static/ui.jstreats the user as still pinned when the viewport is within 250px of the bottom:Because
scrollIfPinned()only checks_scrollPinned, an upward scroll that lands inside that 250px zone gets snapped back to the bottom by the next streamed token. This matches the observed “scroll upward 3 or 4 times” behavior: the user has to escape the 250px near-bottom hysteresis zone before auto-scroll finally stops.Browser-console reproduction on a temporary local WebUI server:
Observed sample: every iteration ended with
bottomDelta: 0, i.e. the viewport was forced back to the bottom despite the user scroll attempt.Expected Behavior
Auto-scroll should continue only when the user is actually at the bottom (or within a tiny tolerance for sub-pixel/layout jitter). Any intentional upward scroll should immediately unpin streaming auto-scroll, even if the viewport is still only 100-200px from the bottom.
Actual Behavior
The UI uses a broad
<250px from bottomthreshold to remain/re-pin. This makes small upward scrolls ineffective during streaming until the user scrolls far enough away from the bottom.Notes / Related Issues
Likely related to the earlier closed auto-scroll issues (#677, #1360, #1469), but this is the remaining threshold/hysteresis behavior rather than the exact previous finish-time jump (#1690).
A likely fix is to separate the concepts:
Environment
origin/master(d8cd556)