@@ -52,6 +52,11 @@ export const ModelThinkingDisplay: React.FC<ModelThinkingDisplayProps> = ({
5252 /** Frame the follow has booked, and the sign that it is still travelling. */
5353 const tailFollowFrameRef = useRef < number | null > ( null ) ;
5454 const touchScrollStartYRef = useRef < number | null > ( null ) ;
55+ const lastScrollPositionRef = useRef < {
56+ top : number ;
57+ height : number ;
58+ viewport : number ;
59+ } | null > ( null ) ;
5560
5661 const isActive = isStreaming || status === 'streaming' ;
5762 const { displayText : displayContent , isRevealing } = useTypewriter (
@@ -98,6 +103,38 @@ export const ModelThinkingDisplay: React.FC<ModelThinkingDisplayProps> = ({
98103 tailFollowFrameRef . current = null ;
99104 } , [ ] ) ;
100105
106+ const pauseTailFollowForUserScroll = useCallback ( ( ) => {
107+ shouldFollowTailRef . current = false ;
108+ tailFollowPauseVersionRef . current += 1 ;
109+ tailFollowUserPauseUntilMsRef . current = performance . now ( ) + 700 ;
110+ stopTailFollow ( ) ;
111+ } , [ stopTailFollow ] ) ;
112+
113+ const recordScrollPosition = useCallback ( ( el : HTMLElement ) => {
114+ lastScrollPositionRef . current = {
115+ top : el . scrollTop ,
116+ height : el . scrollHeight ,
117+ viewport : el . clientHeight ,
118+ } ;
119+ } , [ ] ) ;
120+
121+ const detectUpwardScroll = useCallback ( ( el : HTMLElement ) => {
122+ const previous = lastScrollPositionRef . current ;
123+ // Scrollbar drags have no wheel/key event. Compare with our last actual
124+ // offset, allowing rounding noise and excluding layout-driven movement.
125+ const movedUp = isExpanded && previous !== null &&
126+ el . scrollHeight >= previous . height &&
127+ el . clientHeight === previous . viewport &&
128+ el . scrollTop < previous . top - 1 ;
129+ recordScrollPosition ( el ) ;
130+ if ( movedUp ) pauseTailFollowForUserScroll ( ) ;
131+ return movedUp ;
132+ } , [ isExpanded , pauseTailFollowForUserScroll , recordScrollPosition ] ) ;
133+
134+ useLayoutEffect ( ( ) => {
135+ lastScrollPositionRef . current = null ;
136+ } , [ isExpanded ] ) ;
137+
101138 /**
102139 * Follow the tail across the frames it is given, rather than in one write.
103140 *
@@ -119,6 +156,8 @@ export const ModelThinkingDisplay: React.FC<ModelThinkingDisplayProps> = ({
119156 tailFollowFrameRef . current = null ;
120157 const el = contentRef . current ;
121158 if ( ! el ) return ;
159+ // The browser may update the offset before delivering its scroll event.
160+ if ( detectUpwardScroll ( el ) ) return ;
122161 if ( expectedPauseVersion !== tailFollowPauseVersionRef . current ) return ;
123162 if ( ! shouldFollowTailRef . current ) return ;
124163
@@ -132,6 +171,7 @@ export const ModelThinkingDisplay: React.FC<ModelThinkingDisplayProps> = ({
132171 : { offsetPx : targetPx , outcome : 'snapped' as const } ;
133172
134173 el . scrollTop = step . offsetPx ;
174+ recordScrollPosition ( el ) ;
135175 // Read back rather than taken from the step: the browser clamps to the
136176 // scrollable range, and a platform without fractional scroll offsets
137177 // rounds the last part of an ease away entirely. Believing the step there
@@ -155,23 +195,17 @@ export const ModelThinkingDisplay: React.FC<ModelThinkingDisplayProps> = ({
155195 } ;
156196
157197 tailFollowFrameRef . current = requestAnimationFrame ( runFrame ) ;
158- } , [ ] ) ;
198+ } , [ detectUpwardScroll , recordScrollPosition ] ) ;
159199
160200 /** A follow in flight outlives neither the card nor its collapse. */
161201 useEffect ( ( ) => stopTailFollow , [ isExpanded , stopTailFollow ] ) ;
162202
163- const pauseTailFollowForUserScroll = useCallback ( ( ) => {
164- shouldFollowTailRef . current = false ;
165- tailFollowPauseVersionRef . current += 1 ;
166- tailFollowUserPauseUntilMsRef . current = performance . now ( ) + 700 ;
167- } , [ ] ) ;
168-
169203 // Auto-scroll to bottom while content grows.
170204 useEffect ( ( ) => {
171205 if ( isExpanded && contentRef . current ) {
172206 const el = contentRef . current ;
173207 const gap = getThinkingScrollGap ( el ) ;
174- const wasNearBottom = gap < 80 ;
208+ const wasNearBottom = gap < 20 ;
175209 const userPauseActive = performance . now ( ) <= tailFollowUserPauseUntilMsRef . current ;
176210 if ( wasNearBottom && ! userPauseActive ) {
177211 shouldFollowTailRef . current = true ;
@@ -215,6 +249,7 @@ export const ModelThinkingDisplay: React.FC<ModelThinkingDisplayProps> = ({
215249 const checkScrollState = useCallback ( ( ) => {
216250 const el = contentRef . current ;
217251 if ( ! el ) return ;
252+ detectUpwardScroll ( el ) ;
218253 const gap = getThinkingScrollGap ( el ) ;
219254 const nextScrollState = {
220255 hasScroll : el . scrollHeight > el . clientHeight ,
@@ -249,7 +284,7 @@ export const ModelThinkingDisplay: React.FC<ModelThinkingDisplayProps> = ({
249284 atBottom : nextScrollState . atBottom ,
250285 }
251286 ) ) ;
252- } , [ getThinkingScrollGap ] ) ;
287+ } , [ detectUpwardScroll , getThinkingScrollGap ] ) ;
253288
254289 useEffect ( ( ) => {
255290 if ( isExpanded ) {
0 commit comments