Skip to content

Commit c3b043e

Browse files
authored
Merge pull request #6203 from Louis454545/fix/scroll-jump-during-streaming-5939
fix: prevent scroll position jump when AI completes response while user reading
2 parents 0fc3dc6 + ecc9020 commit c3b043e

File tree

1 file changed

+42
-0
lines changed

1 file changed

+42
-0
lines changed

web-app/src/routes/threads/$threadId.tsx

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ function ThreadDetail() {
3737
const [isAtBottom, setIsAtBottom] = useState(true)
3838
const [hasScrollbar, setHasScrollbar] = useState(false)
3939
const lastScrollTopRef = useRef(0)
40+
const userIntendedPositionRef = useRef<number | null>(null)
41+
const wasStreamingRef = useRef(false)
4042
const { currentThreadId, setCurrentThreadId } = useThreads()
4143
const { setCurrentAssistant, assistants } = useAssistant()
4244
const { setMessages, deleteMessage } = useMessages()
@@ -112,6 +114,8 @@ function ThreadDetail() {
112114
scrollToBottom()
113115
setIsAtBottom(true)
114116
setIsUserScrolling(false)
117+
userIntendedPositionRef.current = null
118+
wasStreamingRef.current = false
115119
checkScrollState()
116120
return
117121
}
@@ -123,11 +127,39 @@ function ThreadDetail() {
123127
scrollToBottom()
124128
setIsAtBottom(true)
125129
setIsUserScrolling(false)
130+
userIntendedPositionRef.current = null
131+
wasStreamingRef.current = false
126132
checkScrollState()
127133
}, [threadId])
128134

129135
// Single useEffect for all auto-scrolling logic
130136
useEffect(() => {
137+
// Track streaming state changes
138+
const isCurrentlyStreaming = !!streamingContent
139+
const justFinishedStreaming = wasStreamingRef.current && !isCurrentlyStreaming
140+
wasStreamingRef.current = isCurrentlyStreaming
141+
142+
// If streaming just finished and user had an intended position, restore it
143+
if (justFinishedStreaming && userIntendedPositionRef.current !== null) {
144+
// Small delay to ensure DOM has updated
145+
setTimeout(() => {
146+
if (scrollContainerRef.current && userIntendedPositionRef.current !== null) {
147+
scrollContainerRef.current.scrollTo({
148+
top: userIntendedPositionRef.current,
149+
behavior: 'smooth'
150+
})
151+
userIntendedPositionRef.current = null
152+
setIsUserScrolling(false)
153+
}
154+
}, 100)
155+
return
156+
}
157+
158+
// Clear intended position when streaming starts fresh
159+
if (isCurrentlyStreaming && !wasStreamingRef.current) {
160+
userIntendedPositionRef.current = null
161+
}
162+
131163
// Only auto-scroll when the user is not actively scrolling
132164
// AND either at the bottom OR there's streaming content
133165
if (!isUserScrolling && (streamingContent || isAtBottom) && messagesCount) {
@@ -163,6 +195,11 @@ function ThreadDetail() {
163195
// Detect if this is a user-initiated scroll
164196
if (Math.abs(scrollTop - lastScrollTopRef.current) > 10) {
165197
setIsUserScrolling(!isBottom)
198+
199+
// If user scrolls during streaming and moves away from bottom, record their intended position
200+
if (streamingContent && !isBottom) {
201+
userIntendedPositionRef.current = scrollTop
202+
}
166203
}
167204
setIsAtBottom(isBottom)
168205
setHasScrollbar(hasScroll)
@@ -180,6 +217,11 @@ function ThreadDetail() {
180217
// Detect if this is a user-initiated scroll
181218
if (Math.abs(scrollTop - lastScrollTopRef.current) > 10) {
182219
setIsUserScrolling(!isBottom)
220+
221+
// If user scrolls during streaming and moves away from bottom, record their intended position
222+
if (streamingContent && !isBottom) {
223+
userIntendedPositionRef.current = scrollTop
224+
}
183225
}
184226
setIsAtBottom(isBottom)
185227
setHasScrollbar(hasScroll)

0 commit comments

Comments
 (0)