Skip to content

fix(app): stop following the end when a scroll moves away from it - #56308

Closed
argszero wants to merge 1 commit into
anomalyco:devfrom
argszero:timeline-scroll-unpin
Closed

argszero wants to merge 1 commit into
anomalyco:devfrom
argszero:timeline-scroll-unpin

Conversation

@argszero

@argszero argszero commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Issue for this PR

Closes #56285

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

handleListScroll released the follow-the-end pin only for a held pointer or a native touch scroll, and every other upward scroll depended on an input handler to unpin first: the wheel handler for wheel, the touch handlers for touch. Two reported gestures produce neither. The custom scrollbar thumb calls stopPropagation() on its own pointer-down (packages/ui/src/components/scroll-view.tsx), so the timeline never records a held pointer and pointerHeld stays false for the whole drag. Autoscroll-style scrolling moves the offset with no pointer, wheel or key event at all. Both stayed pinned, so every size update while pinned scrolled the viewport back to the end on the next frame, which is the 1-frame oscillation in the report.

The fix decides from geometry rather than from the input device. The distance to the end (maxScroll - scrollTop) is what tells an arrival from a move away from it:

  • A measurement adjustment moves the offset and the scroll range together, so the distance does not change. Prepending a history page and re-measuring a row above the fold both land here.
  • A move away from the end grows the distance, whatever produced the scroll.
  • Content shrinking under a viewport that is already at the end leaves the distance at zero, so the existing arrival rule still pins.

So the unpin condition becomes "the distance to the end grew", and the previous pointer/touch clause stays as an additional way in, since content can shrink during a held drag. The pin path is unchanged: every scrollToEnd() forcer is gated on input.pinned(), so once the view is unpinned no size update can pull it back.

I did not change the arrived heuristic or the paging thresholds. With the unpin in place the re-pin branch cannot run during a gesture, and onHistoryScroll is already called on every scroll event, so paging continues mid-gesture — the older-page loading in the report was reachable only after the view could stay away from the end at all. If a maintainer would rather see the pre-fetch threshold raised, that is a separate change.

How did you verify your code works?

Added packages/app/e2e/regression/timeline-scroll-unpin.spec.ts, two cases over an 80-exchange session opened at the end:

  • Thumb drag: drags the real .scroll-view__thumb to the top over 1 s and holds it, sampling the distance to the end every frame.
  • Autoscroll: moves the offset up 600 px over frames with no pointer, wheel or key event.

Both assert the gesture reaches more than 200 px from the end, stays there, and never loses more than 50 px in a single frame.

Proof it is the change and not the harness — with only virtualizer.tsx reverted and the spec unchanged:

case before after
thumb drag final distance 0 px (snapped fully back to the end) final distance > 200 px, no snap-back frame
autoscroll one frame carrying the view back toward the end no snap-back frame

Both files pass oxlint. bun run typecheck in packages/app is clean; bun run typecheck:e2e reports only the four errors that are already on dev (performance/terminals/probe.ts, performance/timeline/session-timeline-stream-probe.ts, regression/timeline-stream.spec.ts), none in the new spec.

Neighbouring timeline specs on the same build: timeline-history, timeline-geometry, timeline-cache, session-panel-scrollbar, timeline-stream — 71 passed, 1 failed. The failure is timeline-cache.spec.ts "focuses Find in the selected cached timeline", which fails identically with the source reverted, so it predates this change.

Screenshots / recordings

No screenshot: the change has no visual surface of its own.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

Added by the opencode bot (AI-generated).

Related: #55699

The timeline only released its follow-the-end pin for a held pointer or a
native touch scroll, and the custom scrollbar thumb stops its own
pointer-down from reaching the timeline. Dragging the thumb up from the end
therefore stayed pinned, and every size update scrolled the viewport back
down. Autoscroll-style scrolling carries no pointer or wheel event at all,
so it hit the same path.

Decide from geometry instead of input devices: the distance to the end grows
only when the view moves away from it, while a measurement adjustment moves
the offset and the scroll range together and leaves it alone.
@github-actions

Copy link
Copy Markdown
Contributor

The following comment was made by an LLM, it may be inaccurate:

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fixes #56285. I ran the bot's reproduction in Chromium on the base and on this branch. On the base, dragging the scrollbar thumb snapped back to 0 px from the bottom on every frame, and autoscroll jumped back toward the bottom 8 times. With this change, autoscroll has no jumps and the thumb drag reaches the top and stays there. The new regression spec passes here and fails on the base. The full test:stability suite fails the same 12 tests with and without the change, so it shows no regression, though the follow-the-bottom and keyboard-scroll tests are among those 12. typecheck in packages/app is clean.

Public API is unchanged.

Using the change in scroll distance to decide when to stop following the bottom looks sound. Measurement adjustments move the scroll position and the scroll range together, so they don't release it. I have no blocking concerns.

Related: #55699

@argszero

Copy link
Copy Markdown
Contributor Author

Thanks — the base-versus-branch reproduction is what I wanted checked, and your read of the geometry rule matches mine.

On the twelve test:stability failures: I re-ran the two cases closest to this change in my own environment, with the same spec files and the same system Chrome, once on the base file and once on this branch. follows an expanded patch that arrives as the user reaches the bottom and does not pull a keyboard-scrolled user during shell remeasurement pass on both sides here, 2/2 in under seven seconds each.

One environment prerequisite was in the way first: the stability config turns on video recording, and without the bundled ffmpeg binary every test in the suite dies in browserContext.newPage before reaching the timeline. With video disabled the suite runs. If your twelve failures are all of that shape, they are a runner prerequisite rather than a property of this change — worth knowing for anyone who runs test:stability locally without npx playwright install ffmpeg.

@argszero

Copy link
Copy Markdown
Contributor Author

Closing this to stay within GitHub's 3-open-pull-request limit for contributors without write access, which currently blocks every new pull request here. The branch remains on my fork, so I can reopen it on request.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

desktop: fast scroll up from bottom snaps back down, older history loads one chunk at a time

1 participant