Skip to content

The transcript jumps to the top of the conversation when a Bot's first token arrives #338

Description

@zopeVaibhav

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

  1. Open a channel with enough history to scroll — eight messages is plenty.
  2. Send a message. It settles near the top of the viewport, correctly.
  3. Wait for the Bot's first token.
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions