Skip to content

Commit e9d8e92

Browse files
committed
fix(react-ui): don't yank chat scroll to bottom while user is reading
The chat and agent-chat pages auto-scrolled to the bottom on every streamed token. If the user scrolled up to re-read part of a response, the next chunk pulled them back down — making long replies unreadable while streaming. Track a stickToBottomRef on each scroll event: if the user is within 80px of the bottom we keep auto-scrolling, otherwise we leave them where they are. On chat switch we snap back to the bottom and re-pin. Same fix applied to both Chat.jsx and AgentChat.jsx since they share the same streaming pattern. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Assisted-by: Claude:claude-opus-4-7 [Claude Code]
1 parent 5b0196c commit e9d8e92

2 files changed

Lines changed: 44 additions & 2 deletions

File tree

core/http/react-ui/src/pages/AgentChat.jsx

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ export default function AgentChat() {
101101
const messagesEndRef = useRef(null)
102102
const messagesRef = useRef(null)
103103
const textareaRef = useRef(null)
104+
const stickToBottomRef = useRef(true)
104105
const eventSourceRef = useRef(null)
105106
const messageIdCounter = useRef(0)
106107
const addMessageRef = useRef(addMessage)
@@ -260,11 +261,31 @@ export default function AgentChat() {
260261
}
261262
}, [name, userId, addToast, nextId])
262263

263-
// Auto-scroll to bottom
264+
// Track whether the user is pinned to the bottom. If they scroll up
265+
// while a response is streaming, stop forcing them back down.
264266
useEffect(() => {
267+
const el = messagesRef.current
268+
if (!el) return
269+
const onScroll = () => {
270+
const distanceFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight
271+
stickToBottomRef.current = distanceFromBottom < 80
272+
}
273+
el.addEventListener('scroll', onScroll, { passive: true })
274+
return () => el.removeEventListener('scroll', onScroll)
275+
}, [])
276+
277+
// Auto-scroll only when the user hasn't scrolled away from the bottom.
278+
useEffect(() => {
279+
if (!stickToBottomRef.current) return
265280
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
266281
}, [messages, streamContent, streamReasoning, streamToolCalls])
267282

283+
// When switching conversations, snap to bottom and re-pin.
284+
useEffect(() => {
285+
stickToBottomRef.current = true
286+
messagesEndRef.current?.scrollIntoView({ behavior: 'auto' })
287+
}, [activeId])
288+
268289
// Highlight code blocks
269290
useEffect(() => {
270291
if (messagesRef.current) highlightAll(messagesRef.current)

core/http/react-ui/src/pages/Chat.jsx

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,7 @@ export default function Chat() {
327327
const fileInputRef = useRef(null)
328328
const messagesRef = useRef(null)
329329
const textareaRef = useRef(null)
330+
const stickToBottomRef = useRef(true)
330331

331332
const artifacts = useMemo(
332333
() => canvasMode ? extractCodeArtifacts(activeChat?.history, 'role', 'assistant') : [],
@@ -561,11 +562,31 @@ export default function Chat() {
561562
}
562563
}, [])
563564

564-
// Auto-scroll
565+
// Track whether the user is pinned to the bottom. If they scroll up
566+
// while a response is streaming, stop forcing them back down.
565567
useEffect(() => {
568+
const el = messagesRef.current
569+
if (!el) return
570+
const onScroll = () => {
571+
const distanceFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight
572+
stickToBottomRef.current = distanceFromBottom < 80
573+
}
574+
el.addEventListener('scroll', onScroll, { passive: true })
575+
return () => el.removeEventListener('scroll', onScroll)
576+
}, [])
577+
578+
// Auto-scroll only when the user hasn't scrolled away from the bottom.
579+
useEffect(() => {
580+
if (!stickToBottomRef.current) return
566581
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
567582
}, [activeChat?.history, streamingContent, streamingReasoning, streamingToolCalls])
568583

584+
// When switching chats, snap to bottom and re-pin.
585+
useEffect(() => {
586+
stickToBottomRef.current = true
587+
messagesEndRef.current?.scrollIntoView({ behavior: 'auto' })
588+
}, [activeChat?.id])
589+
569590
// Highlight code blocks
570591
useEffect(() => {
571592
if (messagesRef.current) {

0 commit comments

Comments
 (0)