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
@@ -1882,7 +1882,7 @@ export const settingsDict = {
'settings.openchamber.visual.section.streaming': 'Streaming',
'settings.openchamber.visual.field.streamingAutoFollow': 'Neuen Inhalten beim Streaming folgen',
'settings.openchamber.visual.field.streamingAutoFollowAria': 'Neuen Inhalten automatisch folgen, während eine Antwort gestreamt wird',
'settings.openchamber.visual.field.streamingAutoFollowInfo': 'Während eine Antwort eintrifft, folgt die Ansicht laufend dem neuesten Inhalt. Deaktivieren, um die Ansicht ruhig zu halten und manuell zu scrollen.',
'settings.openchamber.visual.field.streamingAutoFollowInfo': 'Während eine Antwort eintrifft, folgt die Ansicht laufend dem neuesten Inhalt. Deaktivieren, um die Ansicht ruhig zu halten und manuell zu scrollen; das Senden einer Nachricht aus der Mitte des Chats lässt die Ansicht dann ebenfalls an Ort und Stelle.',
'settings.openchamber.visual.section.messageAppearance': 'Nachrichten-Erscheinungsbild',
'settings.openchamber.visual.section.toolsAndFiles': 'Werkzeuge & Dateien',
'settings.openchamber.visual.section.composer': 'Komponist',
@@ -1955,7 +1955,7 @@ export const settingsDict = {
'settings.openchamber.visual.section.streaming': 'Streaming',
'settings.openchamber.visual.field.streamingAutoFollow': 'Follow new content while streaming',
'settings.openchamber.visual.field.streamingAutoFollowAria': 'Automatically follow new content while a response streams',
'settings.openchamber.visual.field.streamingAutoFollowInfo': 'While a reply streams in, the view keeps gliding to the newest content. Turn this off to keep the view still and scroll manually.',
'settings.openchamber.visual.field.streamingAutoFollowInfo': 'While a reply streams in, the view keeps gliding to the newest content. Turn this off to keep the view still and scroll manually; sending a message while scrolled up then also leaves the view where it is.',
'settings.openchamber.visual.section.messageAppearance': 'Message Appearance',
'settings.openchamber.visual.section.toolsAndFiles': 'Tools & Files',
'settings.openchamber.visual.section.composer': 'Composer',
@@ -1932,7 +1932,7 @@ export const settingsDict = {
"settings.openchamber.visual.section.streaming": "Streaming",
"settings.openchamber.visual.field.streamingAutoFollow": "Seguir el contenido nuevo durante el streaming",
"settings.openchamber.visual.field.streamingAutoFollowAria": "Seguir automáticamente el contenido nuevo mientras se transmite una respuesta",
"settings.openchamber.visual.field.streamingAutoFollowInfo": "Mientras llega una respuesta, la vista se desplaza hacia el contenido más reciente. Desactívalo para mantener la vista quieta y desplazarte manualmente.",
"settings.openchamber.visual.field.streamingAutoFollowInfo": "Mientras llega una respuesta, la vista se desplaza hacia el contenido más reciente. Desactívalo para mantener la vista quieta y desplazarte manualmente; enviar un mensaje desde la mitad del chat tampoco moverá la vista.",
"settings.openchamber.visual.section.messageAppearance": "Apariencia de los mensajes",
"settings.openchamber.visual.section.toolsAndFiles": "Herramientas y archivos",
"settings.openchamber.visual.section.composer": "Compositor",
@@ -1846,7 +1846,7 @@ export const settingsDict = {
'settings.openchamber.visual.section.streaming': 'Streaming',
'settings.openchamber.visual.field.streamingAutoFollow': 'Suivre le nouveau contenu pendant le streaming',
'settings.openchamber.visual.field.streamingAutoFollowAria': 'Suivre automatiquement le nouveau contenu pendant la diffusion dune réponse',
'settings.openchamber.visual.field.streamingAutoFollowInfo': 'Pendant quune réponse arrive, la vue glisse vers le contenu le plus récent. Désactivez pour garder la vue immobile et défiler manuellement.',
'settings.openchamber.visual.field.streamingAutoFollowInfo': 'Pendant quune réponse arrive, la vue glisse vers le contenu le plus récent. Désactivez pour garder la vue immobile et défiler manuellement ; envoyer un message depuis le milieu de la conversation laisse alors aussi la vue en place.',
'settings.openchamber.visual.section.messageAppearance': 'Apparence des messages',
'settings.openchamber.visual.section.toolsAndFiles': 'Outils et fichiers',
'settings.openchamber.visual.section.composer': 'Zone de saisie',
@@ -1965,7 +1965,7 @@ export const settingsDict = {
'settings.openchamber.visual.section.streaming': 'ストリーミング',
'settings.openchamber.visual.field.streamingAutoFollow': '応答のストリーミング中に新しい内容を追従',
'settings.openchamber.visual.field.streamingAutoFollowAria': '応答のストリーミング中に新しい内容へ自動スクロールする',
'settings.openchamber.visual.field.streamingAutoFollowInfo': '応答の受信中、ビューは常に最新の内容へスクロールします。オフにするとビューは動かず、手動でスクロールできます。',
'settings.openchamber.visual.field.streamingAutoFollowInfo': '応答の受信中、ビューは常に最新の内容へスクロールします。オフにするとビューは動かず、手動でスクロールできます。その場合、チャットの途中からメッセージを送信してもビューは移動しません。',
'settings.openchamber.visual.section.messageAppearance': 'メッセージの外観',
'settings.openchamber.visual.section.toolsAndFiles': 'ツールとファイル',
'settings.openchamber.visual.section.composer': '入力欄',
@@ -1932,7 +1932,7 @@ export const settingsDict = {
'settings.openchamber.visual.section.streaming': '스트리밍',
'settings.openchamber.visual.field.streamingAutoFollow': '스트리밍 중 새 내용 따라가기',
'settings.openchamber.visual.field.streamingAutoFollowAria': '응답 스트리밍 중 새 내용으로 자동 스크롤',
'settings.openchamber.visual.field.streamingAutoFollowInfo': '응답이 스트리밍되는 동안 화면이 최신 내용으로 계속 이동합니다. 끄면 화면이 고정되어 직접 스크롤할 수 있습니다.',
'settings.openchamber.visual.field.streamingAutoFollowInfo': '응답이 스트리밍되는 동안 화면이 최신 내용으로 계속 이동합니다. 끄면 화면이 고정되어 직접 스크롤할 수 있으며, 채팅 중간에서 메시지를 보내도 화면이 이동하지 않습니다.',
'settings.openchamber.visual.section.messageAppearance': '메시지 모양',
'settings.openchamber.visual.section.toolsAndFiles': '도구 및 파일',
'settings.openchamber.visual.section.composer': '입력창',
@@ -1236,7 +1236,7 @@ export const settingsDict = {
'settings.openchamber.visual.section.streaming': 'Streaming',
'settings.openchamber.visual.field.streamingAutoFollow': 'Podążaj za nową treścią podczas streamingu',
'settings.openchamber.visual.field.streamingAutoFollowAria': 'Automatycznie podążaj za nową treścią podczas streamowania odpowiedzi',
'settings.openchamber.visual.field.streamingAutoFollowInfo': 'Podczas napływania odpowiedzi widok płynnie podąża za najnowszą treścią. Wyłącz, aby widok pozostał nieruchomy i przewijać ręcznie.',
'settings.openchamber.visual.field.streamingAutoFollowInfo': 'Podczas napływania odpowiedzi widok płynnie podąża za najnowszą treścią. Wyłącz, aby widok pozostał nieruchomy i przewijać ręcznie; wysłanie wiadomości ze środka czatu również nie przesunie wtedy widoku.',
'settings.openchamber.visual.section.messageAppearance': 'Wygląd wiadomości',
'settings.openchamber.visual.section.toolsAndFiles': 'Narzędzia i pliki',
'settings.openchamber.visual.section.composer': 'Pole wiadomości',
@@ -1932,7 +1932,7 @@ export const settingsDict = {
"settings.openchamber.visual.section.streaming": "Streaming",
"settings.openchamber.visual.field.streamingAutoFollow": "Seguir o novo conteúdo durante o streaming",
"settings.openchamber.visual.field.streamingAutoFollowAria": "Seguir automaticamente o novo conteúdo enquanto uma resposta é transmitida",
"settings.openchamber.visual.field.streamingAutoFollowInfo": "Enquanto uma resposta chega, a visualização acompanha o conteúdo mais recente. Desative para manter a visualização parada e rolar manualmente.",
"settings.openchamber.visual.field.streamingAutoFollowInfo": "Enquanto uma resposta chega, a visualização acompanha o conteúdo mais recente. Desative para manter a visualização parada e rolar manualmente; enviar uma mensagem do meio da conversa também deixará a visualização onde está.",
"settings.openchamber.visual.section.messageAppearance": "Aparência das mensagens",
"settings.openchamber.visual.section.toolsAndFiles": "Ferramentas e arquivos",
"settings.openchamber.visual.section.composer": "Campo de mensagem",
@@ -2205,7 +2205,7 @@ export const settingsDict = {
'settings.openchamber.visual.section.streaming': 'Streaming',
'settings.openchamber.visual.field.streamingAutoFollow': 'Streaming sırasında yeni içeriği takip et',
'settings.openchamber.visual.field.streamingAutoFollowAria': 'Bir yanıt akarken yeni içeriği otomatik olarak takip et',
'settings.openchamber.visual.field.streamingAutoFollowInfo': 'Bir yanıt akarken görünüm en yeni içeriğe doğru kayar. Görünümün sabit kalması için bunu kapatın ve elle kaydırın.',
'settings.openchamber.visual.field.streamingAutoFollowInfo': 'Bir yanıt akarken görünüm en yeni içeriğe doğru kayar. Görünümün sabit kalması için bunu kapatın ve elle kaydırın; bu durumda sohbetin ortasından mesaj göndermek de görünümü yerinden oynatmaz.',
'settings.openchamber.visual.field.sessionTabsGroup': 'Session sekmeleri',
'settings.openchamber.visual.field.sessionTabs': 'Session\'ları başlıkta sekme olarak göster',
'settings.openchamber.visual.field.sessionTabsAria': 'Başlıktaki session sekmelerini aç/kapat',
@@ -1932,7 +1932,7 @@ export const settingsDict = {
"settings.openchamber.visual.section.streaming": "Стримінг",
"settings.openchamber.visual.field.streamingAutoFollow": "Слідкувати за новим вмістом під час стримінгу",
"settings.openchamber.visual.field.streamingAutoFollowAria": "Автоматично слідкувати за новим вмістом під час стримінгу відповіді",
"settings.openchamber.visual.field.streamingAutoFollowInfo": "Поки відповідь надходить, вигляд плавно рухається до найновішого вмісту. Вимкніть, щоб вигляд залишався нерухомим і гортати вручну.",
"settings.openchamber.visual.field.streamingAutoFollowInfo": "Поки відповідь надходить, вигляд плавно рухається до найновішого вмісту. Вимкніть, щоб вигляд залишався нерухомим і гортати вручну; тоді й надсилання повідомлення з середини чату не зсуватиме вигляд.",
"settings.openchamber.visual.section.messageAppearance": "Вигляд повідомлень",
"settings.openchamber.visual.section.toolsAndFiles": "Інструменти та файли",
"settings.openchamber.visual.section.composer": "Поле вводу",
@@ -1932,7 +1932,7 @@ export const settingsDict = {
'settings.openchamber.visual.section.streaming': '流式输出',
'settings.openchamber.visual.field.streamingAutoFollow': '流式输出时跟随新内容',
'settings.openchamber.visual.field.streamingAutoFollowAria': '在回复流式输出时自动跟随新内容',
'settings.openchamber.visual.field.streamingAutoFollowInfo': '回复流式输出时,视图会持续滚动到最新内容。关闭后视图保持不动,可手动滚动。',
'settings.openchamber.visual.field.streamingAutoFollowInfo': '回复流式输出时,视图会持续滚动到最新内容。关闭后视图保持不动,可手动滚动;此时从聊天中间发送消息也不会移动视图。',
'settings.openchamber.visual.section.messageAppearance': '消息外观',
'settings.openchamber.visual.section.toolsAndFiles': '工具和文件',
'settings.openchamber.visual.section.composer': '输入框',
@@ -1839,7 +1839,7 @@ export const settingsDict = {
'settings.openchamber.visual.section.streaming': '串流',
'settings.openchamber.visual.field.streamingAutoFollow': '串流時跟隨新內容',
'settings.openchamber.visual.field.streamingAutoFollowAria': '回覆串流時自動跟隨新內容',
'settings.openchamber.visual.field.streamingAutoFollowInfo': '回覆串流時,畫面會持續捲動到最新內容。關閉後畫面保持不動,可手動捲動。',
'settings.openchamber.visual.field.streamingAutoFollowInfo': '回覆串流時,畫面會持續捲動到最新內容。關閉後畫面保持不動,可手動捲動;此時從聊天中間傳送訊息也不會移動畫面。',
'settings.openchamber.visual.section.messageAppearance': '訊息外觀',
'settings.openchamber.visual.section.toolsAndFiles': '工具與檔案',
'settings.openchamber.visual.section.composer': '輸入框',