@@ -123,6 +123,12 @@ const showSettings = ref(false)
123123const messagesContainer = ref <HTMLElement | null >(null )
124124const autoScrollEnabled = ref (true ) // Track if auto-scroll is enabled
125125const lastScrollTop = ref (0 ) // Track last scroll position to detect scroll direction
126+ // Track the last user-driven scroll direction: 'none' (no user scroll yet), 'up', or 'down'
127+ const lastUserScrollDirection = ref <' none' | ' up' | ' down' >(' none' )
128+ // Timestamp of last user scroll event (ms)
129+ const lastUserScrollTime = ref (0 )
130+ // Flag to ignore scroll events caused by our own programmatic scrolling
131+ const isProgrammaticScroll = ref (false )
126132
127133// Check if user is at the bottom of scroll area
128134function isAtBottom(element : HTMLElement , threshold = 50 ): boolean {
@@ -134,19 +140,28 @@ function handleContainerScroll() {
134140 if (! messagesContainer .value )
135141 return
136142
143+ // Ignore scroll events initiated by our programmatic scrollTo calls
144+ if (isProgrammaticScroll .value )
145+ return
146+
137147 const currentScrollTop = messagesContainer .value .scrollTop
138148
139- // Detect scroll direction: if user scrolls up (scrollTop decreased), disable auto-scroll immediately
149+ // Update timestamp and determine direction
150+ lastUserScrollTime .value = Date .now ()
140151 if (currentScrollTop < lastScrollTop .value ) {
141- // User is scrolling up - disable auto-scroll
152+ // User scrolled up
153+ lastUserScrollDirection .value = ' up'
142154 autoScrollEnabled .value = false
143155 }
144- else if (isAtBottom (messagesContainer .value )) {
145- // User is scrolling down and near bottom - re-enable auto-scroll
146- autoScrollEnabled .value = true
156+ else if (currentScrollTop > lastScrollTop .value ) {
157+ // User scrolled down
158+ lastUserScrollDirection .value = ' down'
159+ // If near bottom, re-enable auto-scroll
160+ if (isAtBottom (messagesContainer .value ))
161+ autoScrollEnabled .value = true
147162 }
148163
149- // Update last scroll position
164+ // Update last scroll position for future comparisons
150165 lastScrollTop .value = currentScrollTop
151166}
152167
@@ -160,12 +175,16 @@ function handleWheel(e: WheelEvent) {
160175 if (! messagesContainer .value )
161176 return
162177
163- // User scrolled up (want older content)
178+ // Treat wheel as a user-driven scroll; record time and direction
179+ lastUserScrollTime .value = Date .now ()
164180 if (e .deltaY < 0 ) {
181+ // Scrolling up
182+ lastUserScrollDirection .value = ' up'
165183 autoScrollEnabled .value = false
166184 }
167- else {
168- // Scrolling down: if near bottom, re-enable
185+ else if (e .deltaY > 0 ) {
186+ // Scrolling down
187+ lastUserScrollDirection .value = ' down'
169188 if (isAtBottom (messagesContainer .value ))
170189 autoScrollEnabled .value = true
171190 }
@@ -189,10 +208,13 @@ function handleTouchMove(e: TouchEvent) {
189208 const currentY = e .touches [0 ].clientY
190209 const delta = currentY - touchStartY .value
191210 // Positive delta means finger moved down -> content scrolls up (towards top) -> user viewing earlier content
211+ lastUserScrollTime .value = Date .now ()
192212 if (delta > 0 ) {
213+ lastUserScrollDirection .value = ' up'
193214 autoScrollEnabled .value = false
194215 }
195- else {
216+ else if (delta < 0 ) {
217+ lastUserScrollDirection .value = ' down'
196218 if (isAtBottom (messagesContainer .value ))
197219 autoScrollEnabled .value = true
198220 }
@@ -206,10 +228,13 @@ function handlePointerDown(e: PointerEvent) {
206228 if (pointerStartY .value == null )
207229 return
208230 const delta = ev .clientY - pointerStartY .value
231+ lastUserScrollTime .value = Date .now ()
209232 if (delta > 0 ) {
233+ lastUserScrollDirection .value = ' up'
210234 autoScrollEnabled .value = false
211235 }
212- else {
236+ else if (delta < 0 ) {
237+ lastUserScrollDirection .value = ' down'
213238 if (messagesContainer .value && isAtBottom (messagesContainer .value ))
214239 autoScrollEnabled .value = true
215240 }
@@ -285,8 +310,20 @@ watch(content, () => {
285310
286311 const el = messagesContainer .value
287312 const prevScrollHeight = el .scrollHeight
288- // Force immediate jump to bottom
289- el .scrollTo ({ top: el .scrollHeight , behavior: ' auto' })
313+ // Force immediate jump to bottom. Mark as programmatic so our scroll handlers ignore it.
314+ try {
315+ isProgrammaticScroll .value = true
316+ el .scrollTo ({ top: el .scrollHeight , behavior: ' auto' })
317+ }
318+ finally {
319+ // Allow handlers to run again after a short tick so lastScrollTop can be updated correctly
320+ // We clear the flag after next frame below (so handlers triggered this frame are ignored).
321+ }
322+
323+ // Yield a frame to ensure the scroll event (if emitted) happens while isProgrammaticScroll is true
324+ await new Promise (resolve => requestAnimationFrame (() => resolve (undefined )))
325+ // Clear programmatic flag now so future user scrolls are handled
326+ isProgrammaticScroll .value = false
290327
291328 // If height didn't change much or we're at bottom, stop retrying
292329 if (Math .abs (el .scrollHeight - prevScrollHeight ) < 2 || isAtBottom (el , 2 ))
0 commit comments