@@ -123,6 +123,12 @@ const showSettings = ref(false)
123
123
const messagesContainer = ref <HTMLElement | null >(null )
124
124
const autoScrollEnabled = ref (true ) // Track if auto-scroll is enabled
125
125
const 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 )
126
132
127
133
// Check if user is at the bottom of scroll area
128
134
function isAtBottom(element : HTMLElement , threshold = 50 ): boolean {
@@ -134,19 +140,28 @@ function handleContainerScroll() {
134
140
if (! messagesContainer .value )
135
141
return
136
142
143
+ // Ignore scroll events initiated by our programmatic scrollTo calls
144
+ if (isProgrammaticScroll .value )
145
+ return
146
+
137
147
const currentScrollTop = messagesContainer .value .scrollTop
138
148
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 ()
140
151
if (currentScrollTop < lastScrollTop .value ) {
141
- // User is scrolling up - disable auto-scroll
152
+ // User scrolled up
153
+ lastUserScrollDirection .value = ' up'
142
154
autoScrollEnabled .value = false
143
155
}
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
147
162
}
148
163
149
- // Update last scroll position
164
+ // Update last scroll position for future comparisons
150
165
lastScrollTop .value = currentScrollTop
151
166
}
152
167
@@ -160,12 +175,16 @@ function handleWheel(e: WheelEvent) {
160
175
if (! messagesContainer .value )
161
176
return
162
177
163
- // User scrolled up (want older content)
178
+ // Treat wheel as a user-driven scroll; record time and direction
179
+ lastUserScrollTime .value = Date .now ()
164
180
if (e .deltaY < 0 ) {
181
+ // Scrolling up
182
+ lastUserScrollDirection .value = ' up'
165
183
autoScrollEnabled .value = false
166
184
}
167
- else {
168
- // Scrolling down: if near bottom, re-enable
185
+ else if (e .deltaY > 0 ) {
186
+ // Scrolling down
187
+ lastUserScrollDirection .value = ' down'
169
188
if (isAtBottom (messagesContainer .value ))
170
189
autoScrollEnabled .value = true
171
190
}
@@ -189,10 +208,13 @@ function handleTouchMove(e: TouchEvent) {
189
208
const currentY = e .touches [0 ].clientY
190
209
const delta = currentY - touchStartY .value
191
210
// Positive delta means finger moved down -> content scrolls up (towards top) -> user viewing earlier content
211
+ lastUserScrollTime .value = Date .now ()
192
212
if (delta > 0 ) {
213
+ lastUserScrollDirection .value = ' up'
193
214
autoScrollEnabled .value = false
194
215
}
195
- else {
216
+ else if (delta < 0 ) {
217
+ lastUserScrollDirection .value = ' down'
196
218
if (isAtBottom (messagesContainer .value ))
197
219
autoScrollEnabled .value = true
198
220
}
@@ -206,10 +228,13 @@ function handlePointerDown(e: PointerEvent) {
206
228
if (pointerStartY .value == null )
207
229
return
208
230
const delta = ev .clientY - pointerStartY .value
231
+ lastUserScrollTime .value = Date .now ()
209
232
if (delta > 0 ) {
233
+ lastUserScrollDirection .value = ' up'
210
234
autoScrollEnabled .value = false
211
235
}
212
- else {
236
+ else if (delta < 0 ) {
237
+ lastUserScrollDirection .value = ' down'
213
238
if (messagesContainer .value && isAtBottom (messagesContainer .value ))
214
239
autoScrollEnabled .value = true
215
240
}
@@ -285,8 +310,20 @@ watch(content, () => {
285
310
286
311
const el = messagesContainer .value
287
312
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
290
327
291
328
// If height didn't change much or we're at bottom, stop retrying
292
329
if (Math .abs (el .scrollHeight - prevScrollHeight ) < 2 || isAtBottom (el , 2 ))
0 commit comments