Bug report
Auto-scroll still overrides the user's scroll position during streaming on the macOS app (latest), despite the fixes shipped in #677 (April 18). Nathan reports this is his single biggest day-to-day pain point: while a long response is streaming, scrolling up to read earlier content gets snapped back to the bottom, and you can't read up until the response finishes.
Source: AvidFuturist, April 30 2026.
Verification — #677 fixes are live in master (v0.50.248)
The three fixes from #677 are present in static/ui.js:
_scrollPinned flag with 150px unpin threshold — static/ui.js:828–845
renderMessages() guards force-scroll behind S.activeStreamId — static/ui.js:3401–3405 and the cache path at :2985
- Scroll-to-bottom button toggled by the scroll listener —
static/index.html:210, static/ui.js:838–839
So the obvious regressions are ruled out. The bug is something else.
Reproduction (macOS app, v1.4.0 + WebUI v0.50.248)
- Open a long-running session, send a prompt that produces a long response (or kicks off many tool calls)
- While streaming, scroll up to re-read earlier content
- Observe: the view snaps back to the bottom within ~100–500ms
Candidate root causes (need verification)
A. WKWebView scroll-event timing under momentum
On macOS WKWebView (the Swift Mac app's container), scroll events can fire after momentum has already carried the scrollTop past the listener's snapshot of scrollHeight - scrollTop - clientHeight. The 150px nearBottom check at ui.js:836 then re-asserts _scrollPinned = true while the user is mid-flick, so the next scrollIfPinned() call (from the smd token feed in messages.js) yanks them back down. Browser Safari may behave the same; needs testing.
How to verify: instrument ui.js:835–844 with a console.log of {scrollTop, scrollHeight, clientHeight, nearBottom, _scrollPinned} and see whether _scrollPinned is flipping back to true mid-streaming when the user thinks they're scrolled up.
B. Browser scroll anchoring during tool-card insertions
When the streaming response adds a new tool/activity card to the DOM, the browser may scroll-anchor the viewport relative to the inserted node — pulling the view downward even though no JS scrollIfPinned() fired. The CSS overflow-anchor property on #messages and child cards needs auditing. overflow-anchor: none on #messages is the standard fix.
C. Implicit scrollToBottom() during streaming (queue card / pill paths)
Two scrollToBottom() calls in ui.js are NOT gated on !S.activeStreamId:
ui.js:1758 — queue card setTimeout(360ms) after a queue card opens
ui.js:1947 — queue pill click handler fallback
If the user has a queued message and Nathan happens to expand the queue card while streaming is active, scrollToBottom() fires unconditionally — both scrolling and re-pinning. This is a corner case but matches "I scrolled up, then it snapped back."
Same pattern in terminal.js:295 and :314 for the terminal panel — fine, separate scroll container.
D. 150px unpin threshold may be too tight on the small macOS app window
The default Mac app window is narrower/shorter than a desktop browser. A single trackpad scroll might not move the user past 150px from the bottom, so the view never actually unpins. Worth raising to ~250px or making the threshold a fraction of clientHeight (e.g. Math.max(150, clientHeight * 0.25)).
Suggested investigation order
- Add the diagnostic log from §A to a dev build, reproduce on the Mac app, and capture the scroll-event sequence — this is the highest-leverage data point.
- If §A confirms the race, debounce the scroll listener with
requestAnimationFrame so the nearBottom check sees post-momentum positions, or require two consecutive nearBottom samples before re-pinning.
- Add
overflow-anchor: none on #messages (cheap, one-line) and re-test — rules out §B.
- Gate the queue-card
scrollToBottom() calls (ui.js:1758, 1947) on !S.activeStreamId — defensive.
- Make the unpin threshold a fraction of
clientHeight (defensive, helps small windows).
Severity
M1. This is the single biggest pain point in daily usage of the app. Marking sprint-candidate for the next release window.
Bug report
Auto-scroll still overrides the user's scroll position during streaming on the macOS app (latest), despite the fixes shipped in #677 (April 18). Nathan reports this is his single biggest day-to-day pain point: while a long response is streaming, scrolling up to read earlier content gets snapped back to the bottom, and you can't read up until the response finishes.
Source: AvidFuturist, April 30 2026.
Verification — #677 fixes are live in master (v0.50.248)
The three fixes from #677 are present in
static/ui.js:_scrollPinnedflag with 150px unpin threshold —static/ui.js:828–845renderMessages()guards force-scroll behindS.activeStreamId—static/ui.js:3401–3405and the cache path at:2985static/index.html:210,static/ui.js:838–839So the obvious regressions are ruled out. The bug is something else.
Reproduction (macOS app, v1.4.0 + WebUI v0.50.248)
Candidate root causes (need verification)
A. WKWebView scroll-event timing under momentum
On macOS WKWebView (the Swift Mac app's container),
scrollevents can fire after momentum has already carried the scrollTop past the listener's snapshot ofscrollHeight - scrollTop - clientHeight. The 150px nearBottom check atui.js:836then re-asserts_scrollPinned = truewhile the user is mid-flick, so the nextscrollIfPinned()call (from the smd token feed inmessages.js) yanks them back down. Browser Safari may behave the same; needs testing.How to verify: instrument
ui.js:835–844with a console.log of{scrollTop, scrollHeight, clientHeight, nearBottom, _scrollPinned}and see whether_scrollPinnedis flipping back totruemid-streaming when the user thinks they're scrolled up.B. Browser scroll anchoring during tool-card insertions
When the streaming response adds a new tool/activity card to the DOM, the browser may scroll-anchor the viewport relative to the inserted node — pulling the view downward even though no JS
scrollIfPinned()fired. The CSSoverflow-anchorproperty on#messagesand child cards needs auditing.overflow-anchor: noneon#messagesis the standard fix.C. Implicit scrollToBottom() during streaming (queue card / pill paths)
Two
scrollToBottom()calls inui.jsare NOT gated on!S.activeStreamId:ui.js:1758— queue card setTimeout(360ms) after a queue card opensui.js:1947— queue pill click handler fallbackIf the user has a queued message and Nathan happens to expand the queue card while streaming is active,
scrollToBottom()fires unconditionally — both scrolling and re-pinning. This is a corner case but matches "I scrolled up, then it snapped back."Same pattern in
terminal.js:295and:314for the terminal panel — fine, separate scroll container.D. 150px unpin threshold may be too tight on the small macOS app window
The default Mac app window is narrower/shorter than a desktop browser. A single trackpad scroll might not move the user past 150px from the bottom, so the view never actually unpins. Worth raising to ~250px or making the threshold a fraction of
clientHeight(e.g.Math.max(150, clientHeight * 0.25)).Suggested investigation order
requestAnimationFrameso the nearBottom check sees post-momentum positions, or require two consecutive nearBottom samples before re-pinning.overflow-anchor: noneon#messages(cheap, one-line) and re-test — rules out §B.scrollToBottom()calls (ui.js:1758, 1947) on!S.activeStreamId— defensive.clientHeight(defensive, helps small windows).Severity
M1. This is the single biggest pain point in daily usage of the app. Marking sprint-candidate for the next release window.