Repository navigation
[Bug]: Returning to a thread that streamed in the background lands far above the latest message #13601
Description
Activity
Triage: #13601
Verdict: Confirmed bug. Not a duplicate. Not fixed on
main.Confidence: High. The reported path matches the current source at
86054b6df2(currentHEAD, the commit in the report). Not reproduced in a browser here. The reporter's runtime log matches the code.What is wrong
Returning to a thread that was following a stream can open thousands of pixels above the latest message, with "Scroll to end" visible. The timeline was following when you left. The session cache says it was a reading position.
Cause
While a turn is running, follow uses an animated glide (
TIMELINE_MAINTAIN_SCROLL_AT_END_SMOOTHinMessagesTimeline.tsx). New output can sit more than 40px below the viewport until that glide catches up.resolveTimelineIsAtEndtreats a gap larger than 40px as not at the end (MessagesTimeline.logic.ts).handleScrollwrites that flag straight into the per-thread cache:rememberTimelinePosition(listIdentityKey, { ...position, // DOM geometry includes the header and the virtualizer's layout adjustment. offsetWithinRow: element.getBoundingClientRect().top - row.getBoundingClientRect().top, scrollOffset: element.scrollTop, atEnd: isAtEnd,
It does not look at live follow.
onIsAtEndChangeinChatView.tsxdoes: while the follow latch is still held,isAtEnd === falseis ignored, so the pill stays hidden and follow stays on. The cache does not get that exception. A row-length effect also callshandleScrollon every growth, which matches the report of 68atEnd: falsewrites during one reply with no manual scroll. The last write (atEnd: false,scrollOffset: 14876) is what return uses.On return,
ChatViewtreatsatEnd !== falseas follow (ChatView.tsxaround the thread-switch effect).falseselects free-scrolling and shows the pill.MessagesTimelinethen restoresscrollOffsetor the saved row instead ofscrollToEnd, and turnsmaintainScrollAtEndoff for that restore.Desktop uses this web timeline. Mobile
ThreadFeeddoes not userememberTimelinePosition. Provider does not matter.Not a duplicate
Issue Why it is different #5903 Same broad symptom (open or finish above the latest message). Still open. This report is one confirmed cause: follow lag saved as a reading position, pill visible. #12372 Opposite cache bug. Position saved as atEnd: true, viewport stranded, pill hidden until a nudge. Open, with #12376.#12222 Restore runs against the previous thread's rows and lands at the top. This bug restores the saved offset and shows the pill. Cross-link #13601 to #5903. Do not close it as a duplicate.
Already fixed?
No. Nothing after
86054b6df2touches this path. #13602 (fix(web): reopen a thread that was following its stream at the end, same author, open, not merged) is the fix: rememberatEnd: isAtEnd || liveFollowEnabled, plus a test where the list is 500px short of the end.Review note for #13602
Do not start a second fix. The change matches the in-session rule, but
liveFollowEnabledis React state and the follow latch updates a ref first. A scroll event before the re-render can still storeatEnd: trueafter the user has left the end. Later scroll events usually overwrite that. A switch in that window would not.The first message of an empty thread also sets live follow while anchored end space keeps
maintainScrollAtEndoff. OR-ing live follow would remember that as at-end andscrollToEndon return. That is outside this repro (history already overflows, so send takesscrollToEnd, not anchoring).Next step
Keep #13601 open. Label it a bug, link #5903 and #12372, and review #13602. Workaround until that lands: "Scroll to end" or End.
- addedvia-triageFiled through npx t3 triageFiled through npx t3 triagebugSomething is broken or behaving incorrectly.Something is broken or behaving incorrectly.acceptedfeature request acceptedfeature request accepted
on Sep 25, 2026 - added a commit that references this issue
on Sep 25, 2026
Before submitting
Related: #5903 describes this symptom broadly, and #12372 covers the case where the "Scroll to end" pill stays hidden. This issue isolates one cause confirmed at runtime. In this case the pill does show.
Area
apps/web
Steps to reproduce
It depends on timing. It happens when the last scroll event before the switch fired while the follow scroll was still catching up with new output. During a long reply that is true for much of the stream.
Expected behavior
The thread opens at its latest message, because the timeline was following the stream when you left it.
Actual behavior
The thread opens where the viewport was when you left, often thousands of pixels above the end, with "Scroll to end" showing. In the recording below it lands 2,657 px above the end.
Impact
Minor bug or occasional failure
Version or commit
main @ 86054b6
Environment
Web client from
vp run devon Windows 11, Codex provider. The logic is provider independent and lives in the shared web timeline, so desktop is affected too.Logs or stack traces
No errors. Logging each
rememberTimelinePositioncall during one streamed reply showed 68 writes ofatEnd: falsefor the thread, although the timeline was following the stream and nothing scrolled it by hand. The last write before switching away was{ atEnd: false, scrollOffset: 14876 }, and on return the restore scrolled to that offset.Screenshots, recordings, or supporting files
timeline-before-fix.mp4
Workaround
Click "Scroll to end" or press End.
Cause
handleScrollinMessagesTimeline.tsxsavesatEnd: resolveTimelineIsAtEnd(state)into the per-thread position cache. While a turn streams,maintainScrollAtEndglides to the end, so new output can sit more than 40 px below the viewport until the follow scroll catches up. Scroll events in that window report "not at end".ChatView already treats that gap as follow lag:
onIsAtEndChangeignoresisAtEnd === falsewhile live follow is active. The remembered position does not, so the thread is stored as a reading position. On return, the restore effect inMessagesTimelineand the thread-switch effect inChatViewreadatEnd: falseand restore that offset instead of the end.