Summary
Reported: opening an existing thread does not land at its newest message. Confirmed to reproduce in a running dev app.
Filing this separately and explicitly UNFIXED, because an attempted fix turned out to be a no-op and the defect does not reproduce in the e2e harness. Recording what was tried so the next attempt does not repeat it.
The suspected cause
useOpenThreadAtBottom (thread.tsx) is a useLayoutEffect that reads viewport.scrollHeight and scrolls in the same layout pass as the transcript's first render — before markdown, code blocks, tool timelines or async content have gained height. One shot, no retry, no rAF, and the latch is burned before the scroll lands.
The cached path is the one that matters. useOpenHumanExternalStore reads state.thread.messagesByThreadId, a cache cleared only on delete or sign-out:
- a thread opened for the first time briefly renders empty, which re-arms assistant-ui's
scrollToBottomOnInitialize, so it scrolls correctly even unfixed;
- a thread visited earlier in the session hands its messages over on the very render its id changes, never passes through the empty state, and keeps the previous thread's
scrollTop.
So any fix must be checked with open A → open B → return to A. A fresh-thread check passes either way and proves nothing.
What was tried, and why it was withdrawn
Re-arming the new bottom-follower (#6563) inside useOpenThreadAtBottom, so that later height changes re-pin the bottom and turn the one-shot scroll into one that survives content arriving.
It is a no-op. Measured against an A → B → A Playwright case that additionally scrolls thread B away from the bottom first, so the follower's flag is false on return and only an explicit re-arm could rescue the landing:
| tree |
cached-path test |
| re-arm present |
passes |
| re-arm removed, follower kept |
passes |
whole thread.tsx reverted to main |
passes |
Passing in all three states means the test cannot detect this defect, so it was never evidence. The change was removed rather than shipped behind a comment claiming it mattered.
Why it does not reproduce in the harness
The e2e fixture's reply is plain markdown, whose full height is available in the first layout pass. The one-shot scroll therefore lands correctly and there is nothing to catch.
A fixture that reproduces this needs content whose height arrives after first paint — images that decode asynchronously, or a transcript whose late layout differs from its first, or a thread large enough that the transcript renders progressively. Until such a fixture exists, this is only reproducible by hand.
Repro by hand
- Open a thread with enough content to overflow, let it settle.
- Open a different thread.
- Return to the first thread.
- It should be at its newest message; it is not.
The regression guard added in #6563's PR (returning to an already-cached thread lands at the bottom) covers the behaviour main already has via #6475, and is labelled in-file as not being evidence for any fix.
Summary
Reported: opening an existing thread does not land at its newest message. Confirmed to reproduce in a running dev app.
Filing this separately and explicitly UNFIXED, because an attempted fix turned out to be a no-op and the defect does not reproduce in the e2e harness. Recording what was tried so the next attempt does not repeat it.
The suspected cause
useOpenThreadAtBottom(thread.tsx) is auseLayoutEffectthat readsviewport.scrollHeightand scrolls in the same layout pass as the transcript's first render — before markdown, code blocks, tool timelines or async content have gained height. One shot, no retry, no rAF, and the latch is burned before the scroll lands.The cached path is the one that matters.
useOpenHumanExternalStorereadsstate.thread.messagesByThreadId, a cache cleared only on delete or sign-out:scrollToBottomOnInitialize, so it scrolls correctly even unfixed;scrollTop.So any fix must be checked with open A → open B → return to A. A fresh-thread check passes either way and proves nothing.
What was tried, and why it was withdrawn
Re-arming the new bottom-follower (#6563) inside
useOpenThreadAtBottom, so that later height changes re-pin the bottom and turn the one-shot scroll into one that survives content arriving.It is a no-op. Measured against an A → B → A Playwright case that additionally scrolls thread B away from the bottom first, so the follower's flag is
falseon return and only an explicit re-arm could rescue the landing:thread.tsxreverted tomainPassing in all three states means the test cannot detect this defect, so it was never evidence. The change was removed rather than shipped behind a comment claiming it mattered.
Why it does not reproduce in the harness
The e2e fixture's reply is plain markdown, whose full height is available in the first layout pass. The one-shot scroll therefore lands correctly and there is nothing to catch.
A fixture that reproduces this needs content whose height arrives after first paint — images that decode asynchronously, or a transcript whose late layout differs from its first, or a thread large enough that the transcript renders progressively. Until such a fixture exists, this is only reproducible by hand.
Repro by hand
The regression guard added in #6563's PR (
returning to an already-cached thread lands at the bottom) covers the behaviourmainalready has via #6475, and is labelled in-file as not being evidence for any fix.