fix(chat): keep the viewport in place on send when auto-follow is off

With "Follow new content while streaming" disabled, sending from mid-history
no longer anchors the new message at the top of the viewport; the reader stays
where they were and the scroll-to-bottom pill leads to the new turn. Sending
from the live edge still parks the message as before. The setting's info text
now describes this in every locale.

Claude-Session: https://claude.ai/code/session_017TK5JAYDfT3Fotc23UEg98
This commit is contained in:
Bohdan Triapitsyn
2026-08-28 20:08:58 +03:00
parent b291eb0b03
commit 010ee59f60
13 changed files with 25 additions and 19 deletions
+13 -7
View File
@@ -318,6 +318,14 @@ export const useChatTimelineScroll = ({
}
}, [clearAnchor, clearGoToBottomReasserts, hideScrollButton]);
// User preference: with auto-follow off, streaming growth never moves the
// viewport. Sending from the live edge still parks the new message at the
// top, but no glide or end-follow correction runs afterwards; sending from
// mid-history leaves the viewport untouched.
const streamingAutoFollowEnabled = useUIStore((state) => state.streamingAutoFollowEnabled);
const streamingAutoFollowEnabledRef = React.useRef(streamingAutoFollowEnabled);
streamingAutoFollowEnabledRef.current = streamingAutoFollowEnabled;
// Sending arms the anchor. The message id is not known here (the optimistic
// row is created by the store), so the next new user message id claims it.
// Whether the send-time anchor positioning may animate. Sending from the
@@ -328,6 +336,11 @@ export const useChatTimelineScroll = ({
const anchorPositionInstantRef = React.useRef(false);
const scrollToBottomOnSend = React.useCallback(() => {
// With auto-follow off, a reader who scrolled away from the end stays
// exactly where they are: the sent message is not anchored and the
// scroll-to-bottom pill (already showing) leads to it. From the live
// edge, sending anchors the new turn as usual.
if (!streamingAutoFollowEnabledRef.current && !isAtEndRef.current) return;
anchorPositionInstantRef.current = !isAtEndRef.current;
isAtEndRef.current = true;
setUserOwnsScroll(false);
@@ -540,13 +553,6 @@ export const useChatTimelineScroll = ({
first: null,
second: null,
});
// User preference: with auto-follow off, streaming growth never moves the
// viewport — the anchored user message still parks at the top on send, but
// no glide or end-follow correction runs afterwards.
const streamingAutoFollowEnabled = useUIStore((state) => state.streamingAutoFollowEnabled);
const streamingAutoFollowEnabledRef = React.useRef(streamingAutoFollowEnabled);
streamingAutoFollowEnabledRef.current = streamingAutoFollowEnabled;
// While the list width is resizing, every pinning write fights the
// per-frame row re-measure and the pinned viewport shakes. Corrections
// stand down for the whole resize and the visible content is held by the