From 010ee59f605520247e616a18cdd067fca4f2dedb Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Fri, 28 Aug 2026 19:39:05 +0300 Subject: [PATCH] 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 --- .../ui/src/hooks/useChatTimelineScroll.ts | 20 ++++++++++++------- .../ui/src/lib/i18n/messages/de.settings.ts | 2 +- .../ui/src/lib/i18n/messages/en.settings.ts | 2 +- .../ui/src/lib/i18n/messages/es.settings.ts | 2 +- .../ui/src/lib/i18n/messages/fr.settings.ts | 2 +- .../ui/src/lib/i18n/messages/ja.settings.ts | 2 +- .../ui/src/lib/i18n/messages/ko.settings.ts | 2 +- .../ui/src/lib/i18n/messages/pl.settings.ts | 2 +- .../src/lib/i18n/messages/pt-BR.settings.ts | 2 +- .../ui/src/lib/i18n/messages/tr.settings.ts | 2 +- .../ui/src/lib/i18n/messages/uk.settings.ts | 2 +- .../src/lib/i18n/messages/zh-CN.settings.ts | 2 +- .../src/lib/i18n/messages/zh-TW.settings.ts | 2 +- 13 files changed, 25 insertions(+), 19 deletions(-) diff --git a/packages/ui/src/hooks/useChatTimelineScroll.ts b/packages/ui/src/hooks/useChatTimelineScroll.ts index 46da713f..91632fb7 100644 --- a/packages/ui/src/hooks/useChatTimelineScroll.ts +++ b/packages/ui/src/hooks/useChatTimelineScroll.ts @@ -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 diff --git a/packages/ui/src/lib/i18n/messages/de.settings.ts b/packages/ui/src/lib/i18n/messages/de.settings.ts index cf91d54d..d7d78114 100644 --- a/packages/ui/src/lib/i18n/messages/de.settings.ts +++ b/packages/ui/src/lib/i18n/messages/de.settings.ts @@ -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', diff --git a/packages/ui/src/lib/i18n/messages/en.settings.ts b/packages/ui/src/lib/i18n/messages/en.settings.ts index 8990d2ec..8de28251 100644 --- a/packages/ui/src/lib/i18n/messages/en.settings.ts +++ b/packages/ui/src/lib/i18n/messages/en.settings.ts @@ -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', diff --git a/packages/ui/src/lib/i18n/messages/es.settings.ts b/packages/ui/src/lib/i18n/messages/es.settings.ts index 6cf7245c..f2f5fbe0 100644 --- a/packages/ui/src/lib/i18n/messages/es.settings.ts +++ b/packages/ui/src/lib/i18n/messages/es.settings.ts @@ -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", diff --git a/packages/ui/src/lib/i18n/messages/fr.settings.ts b/packages/ui/src/lib/i18n/messages/fr.settings.ts index 4e1af53b..05f4b70d 100644 --- a/packages/ui/src/lib/i18n/messages/fr.settings.ts +++ b/packages/ui/src/lib/i18n/messages/fr.settings.ts @@ -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 d’une réponse', - 'settings.openchamber.visual.field.streamingAutoFollowInfo': 'Pendant qu’une 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 qu’une 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', diff --git a/packages/ui/src/lib/i18n/messages/ja.settings.ts b/packages/ui/src/lib/i18n/messages/ja.settings.ts index 4f664809..1cc07322 100644 --- a/packages/ui/src/lib/i18n/messages/ja.settings.ts +++ b/packages/ui/src/lib/i18n/messages/ja.settings.ts @@ -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': '入力欄', diff --git a/packages/ui/src/lib/i18n/messages/ko.settings.ts b/packages/ui/src/lib/i18n/messages/ko.settings.ts index 32d73070..f4cd3033 100644 --- a/packages/ui/src/lib/i18n/messages/ko.settings.ts +++ b/packages/ui/src/lib/i18n/messages/ko.settings.ts @@ -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': '입력창', diff --git a/packages/ui/src/lib/i18n/messages/pl.settings.ts b/packages/ui/src/lib/i18n/messages/pl.settings.ts index 6ca6a1f1..15727cef 100644 --- a/packages/ui/src/lib/i18n/messages/pl.settings.ts +++ b/packages/ui/src/lib/i18n/messages/pl.settings.ts @@ -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', diff --git a/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts b/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts index 47ccf4f8..8582e2bd 100644 --- a/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts +++ b/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts @@ -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", diff --git a/packages/ui/src/lib/i18n/messages/tr.settings.ts b/packages/ui/src/lib/i18n/messages/tr.settings.ts index 9095f19c..43f7f0e6 100644 --- a/packages/ui/src/lib/i18n/messages/tr.settings.ts +++ b/packages/ui/src/lib/i18n/messages/tr.settings.ts @@ -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', diff --git a/packages/ui/src/lib/i18n/messages/uk.settings.ts b/packages/ui/src/lib/i18n/messages/uk.settings.ts index feeafc91..1bba7e21 100644 --- a/packages/ui/src/lib/i18n/messages/uk.settings.ts +++ b/packages/ui/src/lib/i18n/messages/uk.settings.ts @@ -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": "Поле вводу", diff --git a/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts b/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts index 8bebdd26..41f58dbf 100644 --- a/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts +++ b/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts @@ -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': '输入框', diff --git a/packages/ui/src/lib/i18n/messages/zh-TW.settings.ts b/packages/ui/src/lib/i18n/messages/zh-TW.settings.ts index c38af529..f9347a10 100644 --- a/packages/ui/src/lib/i18n/messages/zh-TW.settings.ts +++ b/packages/ui/src/lib/i18n/messages/zh-TW.settings.ts @@ -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': '輸入框',