Skip to content

bug(streaming): small upward scrolls during streaming are overridden until user escapes near-bottom threshold #1731

@Michaelyklam

Description

@Michaelyklam

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions