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.
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
_scrollPinnedflag, an 80px-threshold scroll listener, andscrollIfPinned()used throughoutmessages.jsduring streaming. On paper this should work.The bug:
scrollToBottom()(distinct fromscrollIfPinned()) unconditionally sets_scrollPinned = trueand scrolls. It's called insiderenderMessages()(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 subsequentscrollIfPinned()calls scroll again.Secondary issue on touch/mobile: The
scrollevent 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 ascrollIfPinned()call from arequestAnimationFrame, creating a race where the user appears scrolled up but_scrollPinnedis alreadytrueagain.Fix
1. Guard
scrollToBottom()calls during active streamingIn
renderMessages(), replace the unconditionalscrollToBottom()withscrollIfPinned().scrollToBottom()should only be called when loading a session fresh (not during a live stream) or when the user explicitly sends a message.2. Add a "scroll to bottom" button
When
_scrollPinnedis false (user has scrolled up), show a floating↓button in the bottom-right of the messages area. Clicking it callsscrollToBottom(). 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.Show/hide it by toggling a class based on
_scrollPinnedstate 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
150pxor200pxmakes 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.