@@ -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