The report
I'm looking at the loop messages… the chat always scrolls down even when I'm scrolling. It
shouldn't interrupt my scrolling — if I scroll up trying to read, it should stop automatically
scrolling down.
The guard already exists. One call site ignores it.
InstanceDetail.tsx tracks whether the user is pinned to the bottom (:134-136), set on scroll
(:1048, within 40px). Three of the four auto-scrolls honour it:
| line |
what |
honours atBottomRef? |
:413 |
new message arrived |
✅ |
:429 |
live dictation growing |
✅ |
:583 |
voice pill appeared |
✅ |
:373 |
inside loadMessages |
❌ unconditional |
// Scroll to bottom after initial load
requestAnimationFrame(() => {
if (chatRef.current) chatRef.current.scrollTop = chatRef.current.scrollHeight;
});
Why it hits hardest on exactly the messages you were reading
The comment says "after initial load", and as a one-shot that would be right. But loadMessages
is also the refresh path, and pollLoop calls it every three seconds while a loop is running
(:495, driven by setInterval(…, 3000) at :680):
the workflow drives and this only reports — so it also refreshes the transcript, since the
agent's turns arrive from the server rather than from calls this component made.
So during an autonomous run the transcript reloads every 3s and yanks you to the bottom every time.
Loop messages are precisely the ones that arrive while you are not the one talking, which is
precisely when you are scrolling back to read them.
There is a second caller at :727 (a general refresh, guarded on not-working/not-thinking) with
the same effect outside loops, just less often.
Fix
Make the scroll in loadMessages conditional on atBottomRef.current, the same as its three
siblings — with an explicit exception for genuine first load, which is what the comment was
describing. Passing an argument (loadMessages({ initial: true })) keeps the intent visible rather
than inferring it from messages.length === 0.
Worth checking the 40px threshold at :1048 while there: with the composer and the voice pill
overlaying the scroll area, "within 40px of the bottom" may resolve differently than it looks, and
a user who is almost at the bottom being snapped down is the same complaint in a milder form.
Verification
- Scroll up during a running loop: position holds across at least three poll cycles.
- Scroll back to the bottom: new messages resume auto-scrolling.
- Opening a conversation still lands at the newest message.
- Loading older messages (
:392-399) still preserves the reading position — that path already
works and must not regress.
The report
The guard already exists. One call site ignores it.
InstanceDetail.tsxtracks whether the user is pinned to the bottom (:134-136), set on scroll(
:1048, within 40px). Three of the four auto-scrolls honour it:atBottomRef?:413:429:583:373loadMessagesWhy it hits hardest on exactly the messages you were reading
The comment says "after initial load", and as a one-shot that would be right. But
loadMessagesis also the refresh path, and
pollLoopcalls it every three seconds while a loop is running(
:495, driven bysetInterval(…, 3000)at:680):So during an autonomous run the transcript reloads every 3s and yanks you to the bottom every time.
Loop messages are precisely the ones that arrive while you are not the one talking, which is
precisely when you are scrolling back to read them.
There is a second caller at
:727(a general refresh, guarded on not-working/not-thinking) withthe same effect outside loops, just less often.
Fix
Make the scroll in
loadMessagesconditional onatBottomRef.current, the same as its threesiblings — with an explicit exception for genuine first load, which is what the comment was
describing. Passing an argument (
loadMessages({ initial: true })) keeps the intent visible ratherthan inferring it from
messages.length === 0.Worth checking the 40px threshold at
:1048while there: with the composer and the voice pilloverlaying the scroll area, "within 40px of the bottom" may resolve differently than it looks, and
a user who is almost at the bottom being snapped down is the same complaint in a milder form.
Verification
:392-399) still preserves the reading position — that path alreadyworks and must not regress.