Skip to content

bug(streaming): auto-scroll overrides user scroll position during streaming — cannot read up while AI is responding #677

@nesquena-hermes

Description

@nesquena-hermes

Bug report

While the AI is streaming a response, scrolling up to read earlier content causes the view to snap back to the bottom. The user cannot read earlier parts of the conversation until streaming is fully complete.

Expected behavior

Scrolling up during streaming should pause auto-scroll. The page should stay where the user put it. Auto-scroll should only resume if the user manually scrolls back to the bottom.

Current behavior

The view is pulled back to the bottom continuously during streaming, even after the user has deliberately scrolled up.

Root cause analysis

The code has the right architecture in place — a _scrollPinned flag, an 80px-threshold scroll listener, and scrollIfPinned() used throughout messages.js during streaming. On paper this should work.

The bug: scrollToBottom() (distinct from scrollIfPinned()) unconditionally sets _scrollPinned = true and scrolls. It's called inside renderMessages() (ui.js:1628), which can fire during an active stream in certain conditions (session switch, tool completion, re-render). When this happens, the user's manually-set scroll position is overridden and the pin is re-engaged, making subsequent scrollIfPinned() calls scroll again.

Secondary issue on touch/mobile: The scroll event listener is attached at script load time via an IIFE on #messages. On iOS with -webkit-overflow-scrolling: touch, scroll events fire after momentum has already carried the view — the listener can fire after a scrollIfPinned() call from a requestAnimationFrame, creating a race where the user appears scrolled up but _scrollPinned is already true again.

Fix

1. Guard scrollToBottom() calls during active streaming

In renderMessages(), replace the unconditional scrollToBottom() with scrollIfPinned(). scrollToBottom() should only be called when loading a session fresh (not during a live stream) or when the user explicitly sends a message.

// In renderMessages() — only force-scroll when not actively streaming
if (!S.activeStreamId) {
  scrollToBottom();
} else {
  scrollIfPinned();
}

2. Add a "scroll to bottom" button

When _scrollPinned is false (user has scrolled up), show a floating button in the bottom-right of the messages area. Clicking it calls scrollToBottom(). This is the standard pattern in Claude, ChatGPT, and every other streaming chat UI — it gives users a clear escape hatch rather than requiring them to manually scroll all the way down.

<button id="scrollToBottomBtn" class="scroll-to-bottom-btn" aria-label="Scroll to bottom"></button>

Show/hide it by toggling a class based on _scrollPinned state in the scroll listener.

3. Increase the unpin threshold

80px is tight — a single scroll wheel tick on a fast mouse can cover 100–120px. Raising the threshold to 150px or 200px makes the re-pin less hair-trigger: the user has to visibly scroll back toward the bottom to re-engage auto-scroll.

Related UX request

Reported by a community member on Discord (April 18 2026) — the inability to scroll up and read during a long response is a friction point, especially for long tool-calling sequences or extended reasoning traces.

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