You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
desktop: fast scroll up from bottom snaps back down, older history loads one chunk at a time #56285
In a long desktop chat session sitting idle at the latest message (bottom), scrolling up quickly snaps the viewport back down toward the bottom, so it's impossible to scroll up in one gesture. Older history lazy-loads one chunk at a time: mid-scroll loading doesn't happen — you must stop, wait, then resume. Reproducible, not intermittent.
Code pointers (verified against dev, may drift): the scroll handler in packages/app/src/session/timeline/virtualizer.tsx (handleListScroll, ~L753-769) can re-pin for any input via the atEnd && arrived heuristic (~L760-764, endEpsilon 0.5px), but only unpins for pointer-held/touch scrolls (~L765) — wheel unpin comes only from the wheel handler (~L652-657). Measurement-compensation writes can satisfy arrived and cause a spurious re-pin mid-gesture, after which the pinned scrollToEnd() forcers (~L354-359, end-anchoring ~L279-284) yank the viewport down. Separately, history loading is scroll-event-driven only (~L768) with top-edge/single-flight gates (interaction.ts ~L217-251, 20-message pages), so fast scrolling outruns it. Related but distinct: #56130 (row drift during wheel-up).
Plugins
None found in config
OpenCode version
2.0.29 (install/channel: latest)
Steps to reproduce
Open a long session in opencode v2 desktop (Windows build + WSL server), idle/completed, viewport at the bottom.
Scroll up fast (mouse wheel) in one continuous gesture.
Observe the viewport snaps back down; older history appears only after stopping and waiting, one chunk at a time.
Screenshot and/or share link
None
Operating System
Windows desktop app + WSL server; WSL: Linux 6.18.54.1-microsoft-standard-WSL2 x64
Terminal
opencode v2 desktop app (not terminal); shell in WSL: /bin/bash
Expected / Actual
Expected: fast upward scroll travels up through history, loading older chunks mid-scroll. Actual: viewport snaps back down; chunks load only after stopping and waiting.
Reacted by Hector Zerrillo, Esther Ticlla, Aaron Yarleque, Alexander Flores Solar, Luis Esquivel and tapioca2
Thanks for the detailed report and the code pointers. I tried to reproduce this with the app's timeline in Chromium. I used a 150-exchange idle session sitting at the bottom, with paged history loading and a simulated 150 ms server delay. Then I did one fast upward mouse-wheel gesture (60 steps of 150 px, about 1 s), with and without CPU slowdown and with replies of uneven height. I tried both the latest dev (65932e8) and the timeline code from 2.0.29.
In those runs the view never jumped back toward the bottom; the most it moved back down was about 8 px, which is the row shift tracked in #56130. Older history also loaded during the gesture: each time the view reached the top it paused briefly, then continued once the next page arrived.
I couldn't test the Windows desktop app itself, real smooth scrolling, or a server running in WSL. Could you help narrow it down?
Do you scroll with a mouse wheel (notched or free-spinning) or a touchpad? Is Windows smooth scrolling on?
Does it also happen in the browser? To check, run opencode web in WSL and open it in Chrome or Edge on Windows.
About how long is the session, and what kind of content is near the bottom (long tool outputs, diffs, images)?
When it snaps back, does it go all the way to the bottom or only part of the way? A short screen recording would help a lot.
You can reply here, or edit the issue and leave a comment once you've updated it.
Related: #56130 (rows shift while wheeling up), #40322 (snapping back to the bottom while a response streams, closed).
Additional repro details from my side (desktop app only, haven't tried opencode web in a browser):
I can reproduce it two ways, neither involving the mouse wheel: dragging the scrollbar thumb quickly to the top, or middle-clicking inside the session (autoscroll) and moving the cursor to the top of the window. That may explain the non-repro: both paths produce scrolling with no wheel events, so the wheel-handler unpin never fires.
Session length unknown, but there is a transient aspect: after I manage to fight it all the way to the top once, I can then scroll down/up repeatedly with no snapping — until it goes idle for 'a while', after which the snapping comes back. So something seems to settle/cache after a full traversal and then lapse again when idle.
The snap target is not the bottom: it snaps back to (approximately) where the viewport was before the gesture, oscillating at seemingly 1-frame intervals between that position and a little above it for as long as I hold the scroll toward the top. It never progresses upward while held.
No screen recording yet; can provide one if the above isn't enough to narrow it down.
Thanks, those details were the missing piece: I can reproduce this now. It happens on the latest dev (65932e8) and with the timeline code from 2.0.29. I ran the app's frontend in Chromium with a long idle session sitting at the bottom; I couldn't run the Windows desktop app itself.
Dragging the scrollbar thumb up: the view is pulled back to the bottom on every frame for as long as the thumb is held, and stays at the bottom after release.
Autoscroll-style scrolling (scroll position moved up each frame, with no wheel): the view keeps jumping back toward the bottom and ends only a few hundred pixels up.
Mouse-wheel scrolling did not show this, which explains my earlier failed attempt. The test below reproduces both cases.
A likely cause, not yet confirmed by a fix: the custom scrollbar thumb stops its pointer-down event from reaching the timeline. The timeline therefore never registers a held pointer, so it keeps following the bottom and its size updates scroll the view back down. Autoscroll has no wheel or pointer event at all, so it hits the same problem.
Reproduction script (scroll-no-wheel.spec.ts)
// Reproduction for anomalyco/opencode#56285: scrolling up from the bottom of a long idle session WITHOUT wheel// events (dragging the custom scrollbar thumb, or middle-click autoscroll) is pulled back to the bottom.// Run from packages/app:// bunx playwright test --config e2e/performance/timeline-stability/playwright.config.ts scroll-no-wheelimport{expect,test,typePage}from"@playwright/test"import{assistantMessage,setupTimeline,textPart,userMessage,typeTimelineMessage}from"../../utils/timeline"functionhistory(count: number): TimelineMessage[]{returnArray.from({length: count},(_,index)=>{constprefix=`msg_${String(index).padStart(4,"0")}_nowheel`constuserID=`${prefix}_a_user`return[userMessage(undefined,{id: userID,created: 1690000000000+index*10_000}),assistantMessage([textPart(`prt_${String(index).padStart(4,"0")}_nowheel`,`History ${index}. ${"content ".repeat(20+((index*37)%120))}\n\n\`\`\`ts\n${"const x = 1\n".repeat(3+(index%15))}\`\`\`\n`)],{id: `${prefix}_b_assistant`,parentID: userID,created: 1690000001000+index*10_000},),]}).flat()}constVIEWPORT=".scroll-view__viewport:has([data-timeline-row])"asyncfunctionopen(page: Page){awaitsetupTimeline(page,{messages: history(150),messageDelay: 150,viewport: {width: 1400,height: 800},reducedMotion: true})awaitexpect(page.getByText("History 149.",{exact: false})).toBeVisible()awaitpage.waitForTimeout(1500)}asyncfunctionstartSampling(page: Page){awaitpage.evaluate((sel)=>{constel=document.querySelector(sel)asHTMLElementconstsamples: {t: number;fromEnd: number}[]=[];(windowasany).__samples=samplesconststart=performance.now()consttick=()=>{samples.push({t: Math.round(performance.now()-start),fromEnd: Math.round(el.scrollHeight-el.clientHeight-el.scrollTop)})if(performance.now()-start<4000)requestAnimationFrame(tick)}requestAnimationFrame(tick)},VIEWPORT)}asyncfunctionsummarize(page: Page,label: string){constsamples: {t: number;fromEnd: number}[]=awaitpage.evaluate(()=>(windowasany).__samples)letsnaps=0letmaxSnap=0for(leti=1;i<samples.length;i++){constd=samples[i-1]!.fromEnd-samples[i]!.fromEndif(d>50)snaps++maxSnap=Math.max(maxSnap,d)}constpeak=Math.max(...samples.map((s)=>s.fromEnd))console.log(label,JSON.stringify({snapsTowardBottom: snaps,maxSnapPx: maxSnap,peakFromEnd: peak,finalFromEnd: samples.at(-1)!.fromEnd}))console.log(label,"trace",JSON.stringify(samples.filter((_,i)=>i%5===0).map((s)=>s.fromEnd)))return{ snaps, peak,final: samples.at(-1)!.fromEnd}}test("dragging the scrollbar thumb up from the bottom travels up",async({ page })=>{awaitopen(page)constviewport=page.locator(VIEWPORT)constbox=(awaitviewport.boundingBox())!awaitpage.mouse.move(box.x+box.width/2,box.y+box.height/2)// hover reveals the thumbconstthumb=page.locator('.scroll-view__thumb[data-orientation="vertical"]').first()awaitexpect(thumb).toBeVisible()constt=(awaitthumb.boundingBox())!awaitstartSampling(page)awaitpage.mouse.move(t.x+t.width/2,t.y+t.height/2)awaitpage.mouse.down()// Drag the thumb to the top over ~1s and hold it there.for(leti=1;i<=30;i++){awaitpage.mouse.move(t.x+t.width/2,t.y+t.height/2-((t.y-box.y)*i)/30)awaitpage.waitForTimeout(30)}awaitpage.waitForTimeout(1500)awaitpage.mouse.up()awaitpage.waitForTimeout(1500)constr=awaitsummarize(page,"THUMB_DRAG")expect(r.snaps,"viewport snapped back toward the bottom while dragging the thumb up").toBe(0)expect(r.final).toBeGreaterThan(2000)})test("autoscroll-style upward scrolling (no wheel/pointer) travels up",async({ page })=>{awaitopen(page)awaitstartSampling(page)// Middle-click autoscroll moves scrollTop every frame without wheel or pointerdown on the list.awaitpage.evaluate((sel)=>newPromise<void>((resolve)=>{constel=document.querySelector(sel)asHTMLElementconststart=performance.now()conststep=()=>{el.scrollTop=el.scrollTop-60if(performance.now()-start<2000)requestAnimationFrame(step)elseresolve()}requestAnimationFrame(step)}),VIEWPORT,)awaitpage.waitForTimeout(2000)constr=awaitsummarize(page,"AUTOSCROLL")expect(r.snaps,"viewport snapped back toward the bottom during upward autoscroll").toBe(0)expect(r.final).toBeGreaterThan(2000)})
The two reproduction paths you described (dragging the scrollbar thumb, and autoscroll-style position changes with no wheel or pointer events) point at the same place, and I think the cause is slightly wider than the thumb swallowing the pointer-down:
handleListScroll in packages/app/src/session/timeline/virtualizer.tsx only unpins for pointer-held/touch scrolls; wheel gets its own unpin in the wheel handler, and scrolls with neither (thumb drag that is stopped, autoscroll, keyboard paging) never unpin at all. While pinned, the endEpsilonatEnd && arrived heuristic can re-pin on any input, and the scrollToEnd() forcers then pull the viewport back down — which is the 1-frame oscillation the reporter sees while holding the scroll.
History loading is scroll-event-driven only, with a top-edge gate and single-flight, so a fast gesture outruns it: pages arrive only after the gesture stops.
Plan in packages/app/src/session/timeline/:
Unpin on any scroll that moves meaningfully away from the end, whatever produced it — keep the existing pointer/touch and wheel paths, and add the "distance from end grew" case rather than trying to enumerate every input device. Keep the end-anchoring behaviour for content that grows while genuinely pinned.
Make the arrived check not treat measurement-compensation writes as an arrival, so a mid-gesture re-pin cannot happen.
Let history paging proceed during a gesture instead of only on the trailing scroll event, so older chunks load while the view is still moving.
I'll add a regression test using your scroll-no-wheel.spec.ts as the base (thumb drag and autoscroll), and validate it fails before the fix and passes after. Scope stays inside the timeline virtualizer and its loader — no changes to unrelated scroll behaviour.
The cause turned out to be the unpin rule rather than the thumb on its own. handleListScroll only released the pin for a held pointer or a native touch scroll, and the wheel handler covered wheel, so a thumb drag (whose pointer-down the thumb swallows) and autoscroll-style scrolling (no pointer, wheel or key event at all) never unpinned. While pinned, the size updates scrolled the viewport back to the end, which is the 1-frame oscillation you described.
The fix decides from the distance to the end instead of from the input device: a measurement adjustment moves the offset and the scroll range together and leaves that distance alone, while any move away from the end grows it. Two regression cases cover the thumb drag and the autoscroll path; both end fully back at the end without the fix and stay more than 200 px up with it.
On the second half of your report, older history now loads while you keep dragging, because the view can stay near the top long enough for the paged loader to chain. I left the paging thresholds themselves unchanged — say so if you would rather prefetch earlier.
Thanks for reproducing and validating the no-wheel scroll fix in #56308. Its close note says it was closed only for the contributor open-PR cap and the branch remains available; this issue is still open. Could you reopen the PR or confirm the branch is no longer current? I'll avoid duplicating the tested change.
Checked #57831 (merged) against this report. It fixes one of the two repro paths here, not both.
Scrollbar thumb drag — fixed. The capture-phase pointerdown on the ScrollView root makes a thumb drag register as a user scroll, so the timeline unpins. Verified on dev at ac68d6a (fix present in the built bundle) with the repro from above: the drag now reaches the top instead of being pulled back to the bottom.
Autoscroll-style scrolling — still reproduces. Same build, same fixture, moving the scroll position up each frame with no pointer/wheel/key event: 8 pull-backs of up to ~1600 px and the view ends only ~360 px from the bottom, so it never gets away from the end. The reason is unchanged: handleListScroll still releases the pin only for pointerHeld || touchScrolling, and the captured pointerdown only sets pointerHeld while the button is held — nothing in the scroll path unpins a no-input scroll.
Older history still loads one chunk at a time.#57831 doesn't touch the loader; the trigger is still scroll-event-driven with the top-edge gate and single-flight, so a fast gesture outruns it.
So the remaining scope is what #56308 covered (geometry-based unpin, i.e. "distance to end grew" regardless of input device). That PR was closed for the contributor open-PR cap rather than for the change itself — if its branch is still current, that looks like the piece that would close the rest of this.
Environment for the check: dev @ ac68d6a, production build served from packages/app, headless Chromium, 150-turn idle session with paged history loading and uneven row heights.
Windows desktop 2.2.0, middle-click autoscroll — same cause, plus a user-side workaround that isolates it
Windows 11, OpenCode desktop 2.2.0 (Electron build).
In a long session sitting at the end, middle-click inside the timeline and pan up: the viewport twitches in place and never leaves the end. Same as the autoscroll path reported above.
One wheel notch up first, then middle-click autoscroll: panning works normally and the view stays where I put it.
So the pin is what blocks the gesture, not the panning itself: the wheel handler unpins, and the autoscroll path produces nothing the current rule can name. Chromium's middle-click autoscroll works fine in the same Electron shell on a plain scrollable page, so the browser side is not involved.
Request: the geometry rule from #56308 (movedAway = maxScroll - scrollTop > previousMaxScroll - previousTop + endEpsilon) covers this path without naming an input device, and the branch is still on the author's fork. Re-applying that two-line change would close the remaining half of this report — #57831 only covered the thumb drag.
Scrolling up through long conversations using the scrollbar is still pretty frustrating. I'm running into two issues:
When I open a long conversation and drag the scrollbar upward, it only goes so far before older messages start loading. Sometimes, the viewport even jumps back down by roughly 20% before loading the next chunk. I then have to drag the scrollbar up slowly, wait for each chunk to load, and repeat. It makes navigating older messages unnecessarily difficult.
After reaching the beginning of the conversation and scrolling back down, dragging the scrollbar upward again becomes even more problematic. Instead of moving up normally, the viewport keeps jumping back down, sometimes before I've even made it 30% of the way up.
Not sure if both are caused by the same underlying issue, but the scrollbar behavior still seems inconsistent, especially in longer conversations. Hopefully this can be improved so navigating chat history feels smoother and more predictable.
It is a simple Windows scroll bar issue. Why can't it not be fixed quickly? This is very frustrating using a tiny scroll bar and have it snap right back down when simply trying to scroll up from the bottom.
Is there a real tech support reading this and fix it besides the bot?
This is very frustrating when one simply wants to grab the side scroll bar and scroll to the top while working. This is a basic function that should work. I just can't believe why it takes so long and it is still not fixed. It affects people's work.
A tiny scroll bar is also unacceptable. It should be at least the width like the ChatGpt app, twice the width of what is there in OpenCode. Actually your website opencode.ai has exactly the same problem. I have been working as a system and UI interface designer, and Business Analyst before. This is actually a bad UI. Small is NOT better, if it does not improve the user's experience and functions. I hope someone in Opencode can figure it out soon enough.
Activity
opencode-agent commentedon Oct 17, 2026
Thanks for the detailed report and the code pointers. I tried to reproduce this with the app's timeline in Chromium. I used a 150-exchange idle session sitting at the bottom, with paged history loading and a simulated 150 ms server delay. Then I did one fast upward mouse-wheel gesture (60 steps of 150 px, about 1 s), with and without CPU slowdown and with replies of uneven height. I tried both the latest
dev(65932e8) and the timeline code from 2.0.29.In those runs the view never jumped back toward the bottom; the most it moved back down was about 8 px, which is the row shift tracked in #56130. Older history also loaded during the gesture: each time the view reached the top it paused briefly, then continued once the next page arrived.
I couldn't test the Windows desktop app itself, real smooth scrolling, or a server running in WSL. Could you help narrow it down?
opencode webin WSL and open it in Chrome or Edge on Windows.You can reply here, or edit the issue and leave a comment once you've updated it.
Related: #56130 (rows shift while wheeling up), #40322 (snapping back to the bottom while a response streams, closed).
Railander commentedon Oct 17, 2026
Additional repro details from my side (desktop app only, haven't tried
opencode webin a browser):No screen recording yet; can provide one if the above isn't enough to narrow it down.
opencode-agent commentedon Oct 17, 2026
Thanks, those details were the missing piece: I can reproduce this now. It happens on the latest
dev(65932e8) and with the timeline code from 2.0.29. I ran the app's frontend in Chromium with a long idle session sitting at the bottom; I couldn't run the Windows desktop app itself.Mouse-wheel scrolling did not show this, which explains my earlier failed attempt. The test below reproduces both cases.
A likely cause, not yet confirmed by a fix: the custom scrollbar thumb stops its pointer-down event from reaching the timeline. The timeline therefore never registers a held pointer, so it keeps following the bottom and its size updates scroll the view back down. Autoscroll has no wheel or pointer event at all, so it hits the same problem.
Reproduction script (scroll-no-wheel.spec.ts)
Run it:
bun scroll-no-wheel.spec.tsargszero commentedon Oct 17, 2026
I'd like to work on this.
The two reproduction paths you described (dragging the scrollbar thumb, and autoscroll-style position changes with no wheel or pointer events) point at the same place, and I think the cause is slightly wider than the thumb swallowing the pointer-down:
handleListScrollinpackages/app/src/session/timeline/virtualizer.tsxonly unpins for pointer-held/touch scrolls; wheel gets its own unpin in the wheel handler, and scrolls with neither (thumb drag that is stopped, autoscroll, keyboard paging) never unpin at all. While pinned, theendEpsilonatEnd && arrivedheuristic can re-pin on any input, and thescrollToEnd()forcers then pull the viewport back down — which is the 1-frame oscillation the reporter sees while holding the scroll.Plan in
packages/app/src/session/timeline/:arrivedcheck not treat measurement-compensation writes as an arrival, so a mid-gesture re-pin cannot happen.I'll add a regression test using your
scroll-no-wheel.spec.tsas the base (thumb drag and autoscroll), and validate it fails before the fix and passes after. Scope stays inside the timeline virtualizer and its loader — no changes to unrelated scroll behaviour.argszero commentedon Oct 17, 2026
Opened #56308 for this.
The cause turned out to be the unpin rule rather than the thumb on its own.
handleListScrollonly released the pin for a held pointer or a native touch scroll, and the wheel handler covered wheel, so a thumb drag (whose pointer-down the thumb swallows) and autoscroll-style scrolling (no pointer, wheel or key event at all) never unpinned. While pinned, the size updates scrolled the viewport back to the end, which is the 1-frame oscillation you described.The fix decides from the distance to the end instead of from the input device: a measurement adjustment moves the offset and the scroll range together and leaves that distance alone, while any move away from the end grows it. Two regression cases cover the thumb drag and the autoscroll path; both end fully back at the end without the fix and stay more than 200 px up with it.
On the second half of your report, older history now loads while you keep dragging, because the view can stay near the top long enough for the paged loader to chain. I left the paging thresholds themselves unchanged — say so if you would rather prefetch earlier.
dajiaohuang commentedon Oct 22, 2026
Thanks for reproducing and validating the no-wheel scroll fix in #56308. Its close note says it was closed only for the contributor open-PR cap and the branch remains available; this issue is still open. Could you reopen the PR or confirm the branch is no longer current? I'll avoid duplicating the tested change.
Railander commentedon Oct 22, 2026
Checked #57831 (merged) against this report. It fixes one of the two repro paths here, not both.
Scrollbar thumb drag — fixed. The capture-phase
pointerdownon the ScrollView root makes a thumb drag register as a user scroll, so the timeline unpins. Verified ondevatac68d6a(fix present in the built bundle) with the repro from above: the drag now reaches the top instead of being pulled back to the bottom.Autoscroll-style scrolling — still reproduces. Same build, same fixture, moving the scroll position up each frame with no pointer/wheel/key event: 8 pull-backs of up to ~1600 px and the view ends only ~360 px from the bottom, so it never gets away from the end. The reason is unchanged:
handleListScrollstill releases the pin only forpointerHeld || touchScrolling, and the captured pointerdown only setspointerHeldwhile the button is held — nothing in the scroll path unpins a no-input scroll.Older history still loads one chunk at a time. #57831 doesn't touch the loader; the trigger is still scroll-event-driven with the top-edge gate and single-flight, so a fast gesture outruns it.
So the remaining scope is what #56308 covered (geometry-based unpin, i.e. "distance to end grew" regardless of input device). That PR was closed for the contributor open-PR cap rather than for the change itself — if its branch is still current, that looks like the piece that would close the rest of this.
Environment for the check:
dev@ac68d6a, production build served frompackages/app, headless Chromium, 150-turn idle session with paged history loading and uneven row heights.trustworthyHamster commentedon Oct 31, 2026
Windows desktop 2.2.0, middle-click autoscroll — same cause, plus a user-side workaround that isolates it
Windows 11, OpenCode desktop 2.2.0 (Electron build).
So the pin is what blocks the gesture, not the panning itself: the wheel handler unpins, and the autoscroll path produces nothing the current rule can name. Chromium's middle-click autoscroll works fine in the same Electron shell on a plain scrollable page, so the browser side is not involved.
Request: the geometry rule from #56308 (
movedAway = maxScroll - scrollTop > previousMaxScroll - previousTop + endEpsilon) covers this path without naming an input device, and the branch is still on the author's fork. Re-applying that two-line change would close the remaining half of this report — #57831 only covered the thumb drag.AndysTMC commentedon Nov 3, 2026
Scrolling up through long conversations using the scrollbar is still pretty frustrating. I'm running into two issues:
When I open a long conversation and drag the scrollbar upward, it only goes so far before older messages start loading. Sometimes, the viewport even jumps back down by roughly 20% before loading the next chunk. I then have to drag the scrollbar up slowly, wait for each chunk to load, and repeat. It makes navigating older messages unnecessarily difficult.
After reaching the beginning of the conversation and scrolling back down, dragging the scrollbar upward again becomes even more problematic. Instead of moving up normally, the viewport keeps jumping back down, sometimes before I've even made it 30% of the way up.
Not sure if both are caused by the same underlying issue, but the scrollbar behavior still seems inconsistent, especially in longer conversations. Hopefully this can be improved so navigating chat history feels smoother and more predictable.
Environment: Ubuntu 26.04.1 LTS (kernel 7.0.0-42-generic), OpenCode v2.2.5 (desktop app).
qtapioca commentedon Nov 6, 2026
It is a simple Windows scroll bar issue. Why can't it not be fixed quickly? This is very frustrating using a tiny scroll bar and have it snap right back down when simply trying to scroll up from the bottom.
Is there a real tech support reading this and fix it besides the bot?
qtapioca commentedon Nov 11, 2026
I am using Windows 10, OpenCode Version 2.2.7.
This is very frustrating when one simply wants to grab the side scroll bar and scroll to the top while working. This is a basic function that should work. I just can't believe why it takes so long and it is still not fixed. It affects people's work.
A tiny scroll bar is also unacceptable. It should be at least the width like the ChatGpt app, twice the width of what is there in OpenCode. Actually your website opencode.ai has exactly the same problem. I have been working as a system and UI interface designer, and Business Analyst before. This is actually a bad UI. Small is NOT better, if it does not improve the user's experience and functions. I hope someone in Opencode can figure it out soon enough.