[Bug]: Scroll-to-end pill stays hidden after thread switch strands viewport above the end
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 23k
- Forks
- 5.9k
- Avg merge
- 11h 14m
- Merged PRs (30d)
- 357
Description
Before submitting
- I searched existing issues and did not find a duplicate.
- I included enough detail to reproduce or investigate the problem.
Related: #5903 (this is the stale-pill half split out per discussion there).
Area
apps/web
Steps to reproduce
- Open a thread with enough history to overflow the viewport.
- Send a prompt, then switch to a different thread while the turn runs.
- Return to the first thread after its response has landed.
- Observe the viewport: it can be stranded above the newest message, often farther up than where it was left.
- Observe the "Scroll to end" pill: it does not appear. Nudge the scroll up/down a little and the pill appears.
Expected behavior
If the viewport is not at the live edge, the "Scroll to end" pill is visible. The end-state tracking reflects the actual viewport position after a thread switch settles, including content whose height changes after rendering.
Actual behavior
The pill stays hidden on the stranded viewport, as if the state believes it is already scrolled to the end when it is not. Only a manual scroll nudge (which fires real scroll events) makes the pill appear.
Impact
Major degradation or frequent failure
The stranded viewport looks settled, and the missing pill removes the visible recovery path; END key is the only recourse.
Version or commit
Local checkout @ dc49767ae5
Environment
T3 Code web/desktop client (Linux). Provider-independent; affects the shared web timeline.
Logs or stack traces
No associated exception or console error.
Screenshots, recordings, or supporting files
No response
Workaround
Nudge the scroll wheel up/down until the pill appears, or press END to jump to the live edge.
Suspected mechanism (from code reading, not runtime-verified)
- The position cached for the background thread was saved with
atEnd: true(timelineScrollAnchoring.ts), so the thread-switch effect inChatView.tsxtakes thescrollToEndbranch and hides the pill. - Transient
isAtEnd=falsescroll events during settle are suppressed by the post-open live-follow guard (onIsAtEndChangeearly return when the scroll generation still matches) plus the 150ms show-debounce. - If row heights settle after that (streamed markdown/tool output measuring late), the viewport strands with
isAtEndRefstale-trueand no further scroll event corrects it. - A manual wheel gesture bumps the generation via
cancelTimelineLiveFollowForUserNavigation, letting the nextisAtEnd=falsethrough so the pill appears.
A post-settle reconciliation (re-resolve isAtEnd from the list state a frame or two after position restore completes and sync the pill) would cover this class of stranding regardless of which layout pass caused it.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by reading timelineScrollAnchoring.ts and the thread-switch and scroll-state paths in apps/web, especially ChatView.tsx and onIsAtEndChange. Reproduce the switch-and-return sequence with late-rendering content, then trace position restoration, scroll-generation guards, and the show debounce. Done means a restored viewport that is not at the live edge reliably shows the "Scroll to end" pill without requiring a manual scroll.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- frontend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 68/100