@@ -28,10 +28,6 @@ export default function MessageList({messages}: MessageListProps) {
2828
2929 // Track if user is at bottom - default to true for initial scroll
3030 const isAtBottomRef = useRef ( true ) ;
31- // Track the last known scroll height to detect new content
32- const lastScrollHeightRef = useRef ( 0 ) ;
33- // Track if we're currently doing a programmatic scroll
34- const isProgrammaticScrollRef = useRef ( false ) ;
3531
3632 const checkIfAtBottom = useCallback ( ( ) => {
3733 if ( ! scrollAreaRef ) return false ;
@@ -60,58 +56,31 @@ export default function MessageList({messages}: MessageListProps) {
6056 } ;
6157 } , [ ] ) ;
6258
63- // Update isAtBottom on scroll
59+ // Track whether the user is scrolled to the bottom. Every scroll event
60+ // updates the ref so auto-scroll decisions are always based on the
61+ // user's actual position.
6462 useEffect ( ( ) => {
6563 if ( ! scrollAreaRef ) return ;
66-
6764 const handleScroll = ( ) => {
68- if ( isProgrammaticScrollRef . current ) return ;
6965 isAtBottomRef . current = checkIfAtBottom ( ) ;
7066 } ;
71-
72- // Initial check
7367 handleScroll ( ) ;
74-
7568 scrollAreaRef . addEventListener ( "scroll" , handleScroll ) ;
76- scrollAreaRef . addEventListener ( "scrollend" , ( ) => isProgrammaticScrollRef . current = false ) ;
77- return ( ) => {
78- scrollAreaRef . removeEventListener ( "scroll" , handleScroll )
79- scrollAreaRef . removeEventListener ( "scrollend" , ( ) => isProgrammaticScrollRef . current = false ) ;
80-
81- } ;
69+ return ( ) => scrollAreaRef . removeEventListener ( "scroll" , handleScroll ) ;
8270 } , [ checkIfAtBottom , scrollAreaRef ] ) ;
8371
84- // Handle auto-scrolling when messages change
72+ // Pin to bottom when new content arrives, but only if the user hasn't
73+ // scrolled away. Always scroll when the latest message is from the user
74+ // (they just sent it and should see it). Direct scrollTop assignment is
75+ // synchronous and avoids the animation conflicts that smooth scrollTo
76+ // causes during streaming.
8577 useLayoutEffect ( ( ) => {
8678 if ( ! scrollAreaRef ) return ;
87-
88- const currentScrollHeight = scrollAreaRef . scrollHeight ;
89-
90- // Check if this is new content (scroll height increased)
91- const hasNewContent = currentScrollHeight > lastScrollHeightRef . current ;
92- const isFirstRender = lastScrollHeightRef . current === 0 ;
93- const isNewUserMessage =
94- messages . length > 0 && messages [ messages . length - 1 ] . role === "user" ;
95-
96- // Auto-scroll only if:
97- // 1. It's the first render, OR
98- // 2. There's new content AND user was at the bottom, OR
99- // 3. The user sent a new message
100- if (
101- hasNewContent &&
102- ( isFirstRender || isAtBottomRef . current || isNewUserMessage )
103- ) {
104- isProgrammaticScrollRef . current = true ;
105- scrollAreaRef . scrollTo ( {
106- top : currentScrollHeight ,
107- behavior : isFirstRender ? "instant" : "smooth" ,
108- } ) ;
109- // After scrolling, we're at the bottom
110- isAtBottomRef . current = true ;
111- }
112-
113- // Update the last known scroll height
114- lastScrollHeightRef . current = currentScrollHeight ;
79+ const lastMessage = messages [ messages . length - 1 ] ;
80+ const isUserMessage = lastMessage && lastMessage . role === "user" ;
81+ if ( ! isAtBottomRef . current && ! isUserMessage ) return ;
82+ scrollAreaRef . scrollTop = scrollAreaRef . scrollHeight ;
83+ isAtBottomRef . current = true ;
11584 } , [ messages , scrollAreaRef ] ) ;
11685
11786 // If no messages, show a placeholder
@@ -126,7 +95,7 @@ export default function MessageList({messages}: MessageListProps) {
12695 return (
12796 < div className = "overflow-y-auto flex-1" ref = { setScrollAreaRef } >
12897 < div
129- className = "p-4 flex flex-col gap-4 max-w-4xl mx-auto transition-all duration-300 ease-in-out min-h-0" >
98+ className = "p-4 flex flex-col gap-4 max-w-4xl mx-auto min-h-0" >
13099 { messages . map ( ( message , index ) => (
131100 < div
132101 key = { message . id ?? "draft" }
@@ -137,7 +106,7 @@ export default function MessageList({messages}: MessageListProps) {
137106 message . role === "user"
138107 ? "bg-accent-foreground rounded-lg max-w-[90%] px-4 py-3 text-accent"
139108 : "max-w-[80ch]"
140- } ${ message . id === undefined ? "animate-pulse" : "" } `}
109+ } `}
141110 >
142111 < div
143112 className = { `whitespace-pre-wrap break-words text-left text-xs md:text-sm leading-relaxed md:leading-normal ${
@@ -186,7 +155,7 @@ const ProcessedMessage = React.memo(function ProcessedMessage({
186155 } : ProcessedMessageProps ) {
187156 // Regex to find URLs
188157 // https://stackoverflow.com/a/17773849
189- const urlRegex = useMemo < RegExp > ( ( ) => / ( h t t p s ? : \/ \/ (?: w w w \. | (? ! w w w ) ) [ a - z A - Z 0 - 9 ] [ a - z A - Z 0 - 9 - ] + [ a - z A - Z 0 - 9 ] \. [ ^ \s ] { 2 , } | w w w \. [ a - z A - Z 0 - 9 ] [ a - z A - Z 0 - 9 - ] + [ a - z A - Z 0 - 9 ] \. [ ^ \s ] { 2 , } | h t t p s ? : \/ \/ (?: w w w \. | (? ! w w w ) ) [ a - z A - Z 0 - 9 ] + \. [ ^ \s ] { 2 , } | w w w \. [ a - z A - Z 0 - 9 ] + \. [ ^ \s ] { 2 , } ) / g , [ ] ) ;
158+ const urlRegex = useMemo < RegExp > ( ( ) => / ( h t t p s ? : \/ \/ (?: w w w \. | (? ! w w w ) ) [ a - z A - Z 0 - 9 ] [ a - z A - Z 0 - 9 - ] + [ a - z A - Z 0 - 9 ] \. [ ^ \s ] { 2 , } | w w w \. [ a - z A - Z 0 - 9 ] [ a - z A - Z 0 - 9 - ] + [ a - z A - Z 0 - 9 ] \. [ ^ \s ] { 2 , } | h t t p s ? : \/ \/ (?: w w w \. | (? ! w w w ) ) [ a - z A - Z 0 - 9 ] + \. [ ^ \s ] { 2 , } | w w w \. [ a - z A - Z 0 - 9 ] + \. [ ^ \s ] { 2 , } ) / , [ ] ) ;
190159
191160 const handleClick = ( e : React . MouseEvent < HTMLAnchorElement > , url : string ) => {
192161 if ( e . metaKey || e . ctrlKey ) {
0 commit comments