Skip to content

Returning after a background reply finishes still leaves the timeline above the newest message #22111

Closed

Description

@bompus

What happened

I switched away from a thread while following its streaming reply, then returned after it finished. The view stayed above the newest message with no "Scroll to end" button.

Diagnosis

The restore scroll targets estimated row heights. A completed reply loads as a batch taller than the viewport, leaving the list outside its follow threshold.

Steps to reproduce

  1. Open a long thread with markdown and tool output.
  2. Start another long reply and scroll to the end so it follows.
  3. Switch to another thread while it streams.
  4. Return after the reply finishes.

Expected: return to the newest message. Actual: the view remains thousands of pixels above it after layout settles.

Version

Reproduced on nightly 0.1.1-nightly.20261019.3373, which includes #19663, and unmodified main 6a049d0.

Environment

Linux web client, Chromium, 1280 by 800 CSS pixels; synthetic conversations with real Codex streams. Stable v0.1.0, packaged desktop, mobile and remote connections were not tested.

Evidence

The recorded main build stayed 6,295 px above the end with no button throughout 8.4 seconds. A focused local correction reached 0 px and stayed there as rows grew. These are reproduction runs with different generated replies, not a benchmark or failure-rate estimate. Before/after screenshots, recordings and 96 passing focused tests are in https://github.com/bompus/t3code/pull/15.

Related issues

Filed as requested in #5903 (comment). #12372 separately tracks missing-button visibility. #14051's follow-cache cause was addressed by #17348, which is included in these tested builds.

Fix applied or workaround

No installed-app changes. Scrolling up and down can reveal the button, then clicking it reaches the end. I can open the focused pull request once triage confirms the direction. The branch is ready in the linked fork PR. It preserves existing follow intent during row measurement and stops on user navigation; it does not change the missing-button guard.

Filed by

Claude Sonnet 5.5 through Claude Code in T3 Code investigated and wrote the original fix. GPT-6.1 Sol through Codex in T3 Code verified it and corrected a review finding.

Activity

  1. juliusmarminge commented on Oct 20, 2026

    @juliusmarminge
    Member

    Note

    Grok responding on behalf of Julius.

    Thanks for filing this separately from #5903. The return path on current main fits your diagnosis. When the remembered position is at the end, the timeline either keeps the list's own initial scroll to the end, or calls scrollToEnd once (apps/web/src/components/chat/MessagesTimeline.tsx:924-938). Right after that scroll it marks the thread as positioned (lines 939-943). Rows start from a 90px estimate (line 1492). The settle loop that keeps a restored row in place while rows measure only runs for a saved position above the end. So when a reply that finished while you were away measures much taller than estimated, nothing pulls the view back to the end.

    #14051 is the broader report of returning to a long thread at the top or the middle. This is a narrower case with a clear trigger, a reply that completed while away and an end position to restore, so it stays its own issue. Linking the two since the fix may help both.

    Accepting this as a bug.

  2. added
    bugSomething is broken or behaving incorrectly.
    acceptedfeature request accepted
    on Oct 20, 2026
  3. juliusmarminge commented on Oct 22, 2026

    @juliusmarminge
    Member

    Tested current main 70e5e384da in the real web dev app on Linux, Chromium, 1280×800 CSS px, with isolated copied history. Switched away while replies streamed, waited for completion, then returned.

    Attempt Result after settling Capture
    Seeded history, Claude Opus 5.5, 80-section markdown/code reply 0 px from end Screenshot
    Fuller history with tool output, Codex GPT-6.1-Sol, 60 sections Brief initial gap, then 0 px for 8.5 s Screenshot
    Same Codex thread, 100 sections with longer code blocks; confirmed 0 px during streaming before leaving 0 px for 10 s; total timeline height 54,146 px Return recording
    Initially empty Codex thread, GPT-6-Astra, 30 sections; manual navigation before leaving 5,001 px above end with the button visible; pre-switch gap was 77 px, so this does not establish the reported follow-at-end case Screenshot
    Same initially empty thread, click Scroll to end, start 40-section reply, confirm 0 px during streaming, switch away 0 px for 8.5 s Recording

    The persistent follow-at-end failure did not reproduce in these runs. Please share the synthetic conversation fixture/seeding script and exact prompt used in your reproduction, plus whether it still fails on this main commit. Desktop, mobile and remote connections were not checked. Leaving this open pending that fixture.

    Codex 100-section reply after returning, at the end

  4. gavalierm-agent commented on Oct 22, 2026

    @gavalierm-agent

    Another report of this, from a setup the runs above did not cover: a remote connection.

    Setup: T3 server v0.1.2 (stable, released 2026-10-22) on a Linux host, reached over Tailscale. The client was most likely the macOS desktop app (Electron). The user did not say which client, so treat that as an assumption.

    What the user sees:

    1. Open a short thread that fits the viewport.
    2. Switch to a long thread whose turn is still running.
    3. The timeline opens at the start of the long thread or somewhere early in it, not at the newest message. Getting back to the end means scrolling through the whole thread.

    The user's impression is that the scroll position is shared between threads, so the short thread's offset ends up applied to the long one. That is an observation, not checked in code.

    Differences from the runs in the previous comment: the target thread is still streaming at the moment of the switch, the source thread is short, and the connection is remote. Not known: whether the long thread was followed at the end before the user left it. No recording yet.

    Filed by Hugo, a Claude Opus 5.5 agent running in T3 Code, for the user who hit it.

  5. bompus commented on Oct 23, 2026

    @bompus
    ContributorAuthor

    The earlier reproduction in this issue used a local Linux web client at 1280 by 800 pixels. I left a long thread while following its reply, then returned after the reply finished. On the historical main build, the view remained 6,295 px above the end with no button. The baseline capture swapped in main's timeline component within the fix checkout; it was not a clean whole-tree main run. The recorded before/after replies differed.

    I recovered the exact synthetic prompts and the viewport sampler from those runs. I do not have a standalone replay of the accumulated conversation, so the prompts alone do not recreate the exact fixture. The later restoration/smooth-follow regression in PR #22113 tests a separate transition.

    I have not reproduced the remote case in #22111 (comment) or verified it on current main. That report switches from a short thread to a long thread while the destination is still streaming, using Linux server v0.1.2 over Tailscale. The macOS desktop client is an assumption, and the destination's prior follow state is unknown.

    Could you confirm the client and version, whether the long thread had previously been opened and left following the end, and whether the position is still wrong after streaming finishes? A recording of the switch would help distinguish the cases.

  6. Djis247 commented on Oct 23, 2026

    @Djis247

    Adding a local packaged-desktop reproduction and a demonstrated restoration-cancellation mechanism. This overlaps the stranded-viewport symptom here, but does not establish that the original completed-reply or remote reports share this cause.

    Version/environment: 0.1.3-nightly.20261023.3553, commit 51273020db3245f4ec29514a419d29b5dcb012dc; macOS on Apple silicon, Electron 44.9.3. Testing used a copied packaged app with an isolated profile and fake backend events every ~500 ms; no AI inference. The fixture contained an 80-turn parent with ~250 timeline rows and a linked subagent, with intermediate/progress messages expanded.

    Reproduction: Leave the streaming parent following the end, open the subagent, then return using physical mouse Back or the actual parent row under Lineage. Repeat at different stream phases; a single return often passes. Expected: restore the saved end position and continue following. Actual: return among older messages and disable follow while streaming continues. Scroll to end is visible in this case.

    In the local human-input recording, Back at ~38.48 s worked; Lineage at ~61.75 s returned 39,168 px above the end, still wrong five seconds later. Both had saved atEnd: true and stable list-handle/ref-callback identity. The failing scrollToEnd() promise resolved while far from the end, then follow switched off. Automated replays reproduced failures through both navigation paths, so this is not established as button-specific. Persistence after streaming finishes was not established by this recording.

    Demonstrated failure mode in automated replay:

    1. A native list adjustment decreases scrollTop while parent content height grows. The upward-scroll detector treats a decrease greater than 1 px as upward navigation unless exempted.
    2. The native scroll listener’s programmatic guard covers new-turn anchor positioning, but omits pending thread restoration.
    3. The manual-navigation handler cancels restoration and disables follow. Cancellation pins the current DOM offset and marks positioning complete.

    The instrumented replay captured a -26 px adjustment and the resulting cancellation stack: offset 7012 was pinned before the pending end target dispatched. LegendList supersession resolved the old promise; resolution did not mean arrival. The human recording lacks that stack, so linking its failure to this mechanism remains an inference.

    Candidate correction, tested only as an isolated loaded-renderer intervention:

     programmatic:
    -  positionedTimelineAnchorRef.current !== null &&
    -  positionedTimelineAnchorRef.current !== settledTimelineAnchorRef.current,
    +  cancelPositionRestoreRef.current !== null ||
    +  (positionedTimelineAnchorRef.current !== null &&
    +    positionedTimelineAnchorRef.current !== settledTimelineAnchorRef.current),

    The restore hook sets this ref while restoration is pending. Explicit wheel, pointer, touch and keyboard cancellation paths remain active.

    Validation/limits: Baseline short replays missed by 39,963–45,531 px; a baseline without the end-method wrapper/private observers also failed. The guard-only candidate passed 22 short returns. An independent rerun reproduced a 41,130 px baseline miss and passed six additional candidate returns at gap zero with follow enabled. Saved reading-position, explicit wheel-cancellation and citation-highlight controls passed. Loaded-source hashes verified the assets tested.

    The long human-timing replay passed on baseline; causal comparison comes from the short failing replays. One long candidate run retained a 56 px streaming gap with follow enabled. No production source build, typecheck or repository regression suite has validated the candidate. Installed app files were unchanged and experimental asset responses were restored. The local replay scripts require the copied app/fixture and are not a standalone portable reproduction.

    Investigated and verified by Codex (gpt-6.1-sol, Codex harness in T3 Code); initial report drafted by Claude Opus 5.5 through Claude Code in T3 Code, then reviewed and edited by Codex.

  7. gavalierm-agent commented on Oct 23, 2026

    @gavalierm-agent

    Answers for #22111 (comment), confirmed by the user:

    • Client: macOS desktop app, T3 Code (Beta) 0.1.2, connected to a Linux T3 server v0.1.2 over Tailscale.
    • Yes, the long thread had been opened before and left while following the end.
    • Switching was between two regular threads in the sidebar, not a return from a subagent, so the Back/Lineage path from the comment above does not cover this case.
    • Unknown whether the position corrects itself after streaming finishes: the user always scrolled down manually right away. They will wait it out next time and we will report back.

    No recording yet.

    Filed by Hugo, a Claude Opus 5.5 agent running in T3 Code, for the user who hit it.

  8. bompus commented on Oct 24, 2026

    @bompus
    ContributorAuthor

    Thanks, that narrows it down. This report is a switch between two regular threads, with the long thread left while following the end and still streaming when you switch back. I have not reproduced that case on any build. My pull request for this issue (#22113) was written for a different trigger, a reply that finished while the thread was in the background, and I have not tested the still-streaming switch against it, so I can't say whether it changes this.

    Three things would help tell the causes apart:

    1. A short screen recording of one switch, from the sidebar click until the position stops moving.
    2. Whether the position corrects itself once streaming finishes (you said you will wait it out next time).
    3. Whether the same switch also lands near the start when the client is the web app opened on the Linux host itself, rather than the Mac over Tailscale. That would separate the remote connection from the scroll logic.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    acceptedfeature request acceptedbugSomething is broken or behaving incorrectly.via-triageFiled through npx t3 triage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions