You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The chat transcript is the surface people spend the most time in front of, and every turn it throws away where they were. The moment a Bot's first token lands, the conversation scrolls to its very first message, leaving the question just asked several screens below the fold.
What it looks like
Sending behaves correctly: the new question travels up and settles under the top edge, with the previous turn peeking above it. Then the answer starts, and the transcript is somewhere else entirely.
Measured in Chromium, on a channel with eight messages of history in a 600px viewport, reading the position of the question that was just sent:
step
scrollTop
question, relative to viewport top
after send
600
72px — correct
first token arrives
0
672px below the fold
next send
812
72px — correct
that turn's first token
114
770px below the fold
Where the reader was does not matter. Sending from the bottom, from halfway up the history, or straight after a previous answer all anchor correctly and all lose it on the first token.
issue.mp4
Why it happens
MessageScroller works out what changed between two renders by counting the element children of its content against the previous count. When the count is unchanged it concludes the list was rewritten rather than appended to, and re-anchors on the first anchored message it has not already handled. On a channel opened from history that is message one.
A turn's first token is exactly that shape. Thinking, Stopped and the queued lines are not messages, but they are element children of the same content node, rendered as direct siblings of the message rows in chat-transcript.tsx:735. The Thinking line leaves and the Bot's first line arrives in the same render — one child out, one child in, the count holds, and the scroller jumps to the top.
The same arithmetic has a second consequence: the scan for the newly appended message starts at the old child count, so any element that persists in that list between renders pushes the scan past the new message and the anchoring scroll on send does not happen at all. A Stopped line left over from a turn the reader ended is such an element.
Structural rather than a typo. The scroller's contract is that the children of its content are its items, and this transcript puts three things in there that are not.
Reproduction
Open a channel with enough history to scroll — eight messages is plenty.
Send a message. It settles near the top of the viewport, correctly.
Wait for the Bot's first token.
The transcript is now at the top of the conversation, with the answer being written off screen.
Why it matters
Nothing about the path is unusual: it is one message in an existing conversation, which is the ordinary thing to do with this product. Every turn ends with the reader scrolling back down to an answer that is already half written, and on a long conversation that is a long way back. It reads as the transcript having been replaced rather than moved, so the reader loses their place and briefly loses the thread.
What a fix probably has to do
Stop putting non-messages in the list the scroller counts. Either move Thinking, Stopped and the queued lines out of the scroller's content, or keep them there and make their presence invariant — an always-rendered wrapper, placed ahead of the messages so the append arithmetic still lines up, with the visual order restored afterwards. Any conditional sibling left in that list will keep drifting back into this.
Worth deciding separately: whether the scroller should be defending itself against children carrying no message id, in which case the fix belongs upstream in @shadcn/react rather than here.
Severity
High for how ordinary it is, low for what it costs. Nothing is lost — every answer still arrives and is readable once you scroll to it. But this is the main surface, it happens on every turn of every conversation, and it makes the app feel broken in the first second of every answer.
The chat transcript is the surface people spend the most time in front of, and every turn it throws away where they were. The moment a Bot's first token lands, the conversation scrolls to its very first message, leaving the question just asked several screens below the fold.
What it looks like
Sending behaves correctly: the new question travels up and settles under the top edge, with the previous turn peeking above it. Then the answer starts, and the transcript is somewhere else entirely.
Measured in Chromium, on a channel with eight messages of history in a 600px viewport, reading the position of the question that was just sent:
scrollTopWhere the reader was does not matter. Sending from the bottom, from halfway up the history, or straight after a previous answer all anchor correctly and all lose it on the first token.
issue.mp4
Why it happens
MessageScrollerworks out what changed between two renders by counting the element children of its content against the previous count. When the count is unchanged it concludes the list was rewritten rather than appended to, and re-anchors on the first anchored message it has not already handled. On a channel opened from history that is message one.A turn's first token is exactly that shape.
Thinking,Stoppedand the queued lines are not messages, but they are element children of the same content node, rendered as direct siblings of the message rows in chat-transcript.tsx:735. TheThinkingline leaves and the Bot's first line arrives in the same render — one child out, one child in, the count holds, and the scroller jumps to the top.The same arithmetic has a second consequence: the scan for the newly appended message starts at the old child count, so any element that persists in that list between renders pushes the scan past the new message and the anchoring scroll on send does not happen at all. A
Stoppedline left over from a turn the reader ended is such an element.Structural rather than a typo. The scroller's contract is that the children of its content are its items, and this transcript puts three things in there that are not.
Reproduction
Why it matters
Nothing about the path is unusual: it is one message in an existing conversation, which is the ordinary thing to do with this product. Every turn ends with the reader scrolling back down to an answer that is already half written, and on a long conversation that is a long way back. It reads as the transcript having been replaced rather than moved, so the reader loses their place and briefly loses the thread.
What a fix probably has to do
Stop putting non-messages in the list the scroller counts. Either move
Thinking,Stoppedand the queued lines out of the scroller's content, or keep them there and make their presence invariant — an always-rendered wrapper, placed ahead of the messages so the append arithmetic still lines up, with the visual order restored afterwards. Any conditional sibling left in that list will keep drifting back into this.Worth deciding separately: whether the scroller should be defending itself against children carrying no message id, in which case the fix belongs upstream in
@shadcn/reactrather than here.Severity
High for how ordinary it is, low for what it costs. Nothing is lost — every answer still arrives and is readable once you scroll to it. But this is the main surface, it happens on every turn of every conversation, and it makes the app feel broken in the first second of every answer.