fix: reenable autoscroll when history hits bottom
Rafael Uzarowski committed
Aug 3, 2025 at 09:47 UTC
ff29b86d95a8529321906c1bca26802164e8e2ff
1 file changed
+106
-61
webui/js/messages.js
+106
-61
@@ -15,6 +15,7 @@ class ScrollPositionManager {
15
this.positions = new Map();
16
this.autoscrollDisabled = false;
17
this.scrollableSelectors = ['.msg-content', '.kvps-val'];
18
+ this.monitoringInterval = null; // Added for continuous monitoring
19
}
20
21
// Store scroll positions for all scrollable elements in a message
@@ -119,77 +120,63 @@ class ScrollPositionManager {
120
121
// Check global scroll state and update autoscroll disabled flag
122
checkGlobalScrollState() {
122
- // Check main chat history
123
+ // Check main chat history - this is the primary indicator for disabling autoscroll
124
const chatHistory = document.getElementById("chat-history");
124
- if (chatHistory && !this.isAtBottom(chatHistory)) {
125
+ if (chatHistory && !this.isAtBottom(chatHistory, 20)) {
126
this.autoscrollDisabled = true;
127
return;
128
}
129
129
- // Check chat input area
130
+ // Check chat input area - also important for disabling autoscroll
131
const chatInput = document.getElementById("chat-input");
131
- if (chatInput && !this.isAtBottom(chatInput)) {
132
+ if (chatInput && !this.isAtBottom(chatInput, 20)) {
133
this.autoscrollDisabled = true;
134
return;
135
}
136
136
- // Check all message content areas
137
- const allMsgContent = document.querySelectorAll('.msg-content');
138
- for (const element of allMsgContent) {
139
- if (!this.isAtBottom(element)) {
140
- this.autoscrollDisabled = true;
141
- return;
142
- }
143
- }
144
-
145
- // Check all message body areas (terminal messages)
146
- const allMsgBody = document.querySelectorAll('.message-body');
147
- for (const element of allMsgBody) {
148
- if (!this.isAtBottom(element)) {
149
- this.autoscrollDisabled = true;
150
- return;
151
- }
152
- }
153
-
154
- // Check all KVP areas
155
- const allKvpValues = document.querySelectorAll('.kvps-val');
156
- for (const element of allKvpValues) {
157
- if (!this.isAtBottom(element)) {
158
- this.autoscrollDisabled = true;
159
- return;
160
- }
161
- }
137
+ // Individual message scroll positions don't disable autoscroll globally
138
+ // They only affect their own scrolling behavior
139
+ // This allows users to scroll up in individual messages while keeping autoscroll enabled
140
163
- // If we get here, everything is at bottom, enable autoscroll
141
+ // If we get here, main areas are at bottom, enable autoscroll
142
this.autoscrollDisabled = false;
143
}
144
145
// Improved method to check if element is scrolled to bottom with better tolerance
146
isAtBottom(element, tolerance = 10) {
147
if (!element) return true;
170
- const scrollDiff = element.scrollHeight - element.scrollTop;
148
+
149
+ // Get current scroll position and dimensions
150
+ const scrollTop = element.scrollTop;
151
+ const scrollHeight = element.scrollHeight;
152
const clientHeight = element.clientHeight;
172
- return scrollDiff <= clientHeight + tolerance;
153
+
154
+ // Calculate how far from bottom we are
155
+ const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
156
+
157
+ // Return true if we're within tolerance of the bottom
158
+ return distanceFromBottom <= tolerance;
159
}
160
175
- // Check if user is at bottom of all scrollable areas
161
+ // Enhanced method to check if user is at bottom of all scrollable areas
162
+ // This method is more robust during active content generation
163
isUserAtBottomOfAllScrollableAreas() {
164
// Check main chat history
165
const chatHistory = document.getElementById("chat-history");
179
- if (chatHistory && !this.isAtBottom(chatHistory)) {
166
+ if (chatHistory && !this.isAtBottom(chatHistory, 20)) {
167
return false;
168
}
169
170
// Check chat input area
171
const chatInput = document.getElementById("chat-input");
185
- if (chatInput && !this.isAtBottom(chatInput)) {
172
+ if (chatInput && !this.isAtBottom(chatInput, 20)) {
173
return false;
174
}
175
176
// Check all message content areas
177
const allMsgContent = document.querySelectorAll('.msg-content');
178
for (const element of allMsgContent) {
192
- if (!this.isAtBottom(element)) {
179
+ if (!this.isAtBottom(element, 20)) {
180
return false;
181
}
182
}
@@ -197,7 +184,7 @@ class ScrollPositionManager {
184
// Check all message body areas (terminal messages)
185
const allMsgBody = document.querySelectorAll('.message-body');
186
for (const element of allMsgBody) {
200
- if (!this.isAtBottom(element)) {
187
+ if (!this.isAtBottom(element, 20)) {
188
return false;
189
}
190
}
@@ -205,7 +192,7 @@ class ScrollPositionManager {
192
// Check all KVP areas
193
const allKvpValues = document.querySelectorAll('.kvps-val');
194
for (const element of allKvpValues) {
208
- if (!this.isAtBottom(element)) {
195
+ if (!this.isAtBottom(element, 20)) {
196
return false;
197
}
198
}
@@ -213,17 +200,53 @@ class ScrollPositionManager {
200
return true;
201
}
202
203
+ // Continuous monitoring method for re-enabling autoscroll during active content generation
204
+ startContinuousMonitoring() {
205
+ // Clear any existing monitoring
206
+ if (this.monitoringInterval) {
207
+ clearInterval(this.monitoringInterval);
208
+ }
209
+
210
+ // Start monitoring every 100ms to check if user has scrolled to bottom
211
+ this.monitoringInterval = setInterval(() => {
212
+ if (this.autoscrollDisabled) {
213
+ // Check if main chat history is at bottom - this is the primary indicator
214
+ const chatHistory = document.getElementById("chat-history");
215
+ const isMainHistoryAtBottom = chatHistory && this.isAtBottom(chatHistory, 20);
216
+
217
+ // Check if chat input is at bottom
218
+ const chatInput = document.getElementById("chat-input");
219
+ const isChatInputAtBottom = chatInput && this.isAtBottom(chatInput, 20);
220
+
221
+ // If main chat history is at bottom, re-enable autoscroll regardless of individual message positions
222
+ if (isMainHistoryAtBottom && isChatInputAtBottom) {
223
+ this.reEnableAutoscrollIfAtBottom();
224
+ }
225
+ }
226
+ }, 100);
227
+ }
228
+
229
+ // Stop continuous monitoring
230
+ stopContinuousMonitoring() {
231
+ if (this.monitoringInterval) {
232
+ clearInterval(this.monitoringInterval);
233
+ this.monitoringInterval = null;
234
+ }
235
+ }
236
+
237
// Set up scroll listeners for a message container
238
setupScrollListeners(messageContainer) {
239
if (!messageContainer) return;
240
241
// Add scroll listeners to detect user scrolling within messages
242
+ // These don't disable autoscroll globally, they just manage their own scroll behavior
243
const msgContent = messageContainer.querySelector('.msg-content');
244
if (msgContent) {
245
msgContent.addEventListener('scroll', () => {
224
- if (!this.isAtBottom(msgContent)) {
225
- this.disableAutoscroll();
226
- } else {
246
+ // Individual message scroll doesn't disable global autoscroll
247
+ // It only affects the scroll behavior of this specific element
248
+ if (this.isAtBottom(msgContent, 20)) {
249
+ // If user scrolls back to bottom of this message, they might want autoscroll
250
this.reEnableAutoscrollIfAtBottom();
251
}
252
});
@@ -233,9 +256,10 @@ class ScrollPositionManager {
256
const msgBody = messageContainer.querySelector('.message-body');
257
if (msgBody) {
258
msgBody.addEventListener('scroll', () => {
236
- if (!this.isAtBottom(msgBody)) {
237
- this.disableAutoscroll();
238
- } else {
259
+ // Individual message scroll doesn't disable global autoscroll
260
+ // It only affects the scroll behavior of this specific element
261
+ if (this.isAtBottom(msgBody, 20)) {
262
+ // If user scrolls back to bottom of this message, they might want autoscroll
263
this.reEnableAutoscrollIfAtBottom();
264
}
265
});
@@ -244,9 +268,10 @@ class ScrollPositionManager {
268
const kvpValues = messageContainer.querySelectorAll('.kvps-val');
269
kvpValues.forEach(kvp => {
270
kvp.addEventListener('scroll', () => {
247
- if (!this.isAtBottom(kvp)) {
248
- this.disableAutoscroll();
249
- } else {
271
+ // Individual KVP scroll doesn't disable global autoscroll
272
+ // It only affects the scroll behavior of this specific element
273
+ if (this.isAtBottom(kvp, 20)) {
274
+ // If user scrolls back to bottom of this KVP, they might want autoscroll
275
this.reEnableAutoscrollIfAtBottom();
276
}
277
});
@@ -261,6 +286,8 @@ class ScrollPositionManager {
286
chatHistory.addEventListener('scroll', () => {
287
if (!this.isAtBottom(chatHistory)) {
288
this.disableAutoscroll();
289
+ // Start continuous monitoring when autoscroll is disabled
290
+ this.startContinuousMonitoring();
291
} else {
292
this.reEnableAutoscrollIfAtBottom();
293
}
@@ -273,6 +300,8 @@ class ScrollPositionManager {
300
chatInput.addEventListener('scroll', () => {
301
if (!this.isAtBottom(chatInput)) {
302
this.disableAutoscroll();
303
+ // Start continuous monitoring when autoscroll is disabled
304
+ this.startContinuousMonitoring();
305
} else {
306
this.reEnableAutoscrollIfAtBottom();
307
}
@@ -282,8 +311,20 @@ class ScrollPositionManager {
311
312
// Enhanced method to re-enable autoscroll when user scrolls to bottom
313
reEnableAutoscrollIfAtBottom() {
285
- if (this.isUserAtBottomOfAllScrollableAreas()) {
314
+ // Check if main chat history is at bottom - this is the primary indicator
315
+ const chatHistory = document.getElementById("chat-history");
316
+ const isMainHistoryAtBottom = chatHistory && this.isAtBottom(chatHistory, 20);
317
+
318
+ // Check if chat input is at bottom
319
+ const chatInput = document.getElementById("chat-input");
320
+ const isChatInputAtBottom = chatInput && this.isAtBottom(chatInput, 20);
321
+
322
+ // If main chat history is at bottom, re-enable autoscroll regardless of individual message positions
323
+ // This allows users to scroll up in individual messages but still have autoscroll when they scroll down the main history
324
+ if (isMainHistoryAtBottom && isChatInputAtBottom) {
325
this.autoscrollDisabled = false;
326
+ // Stop continuous monitoring when autoscroll is re-enabled
327
+ this.stopContinuousMonitoring();
328
// Scroll all elements to bottom when autoscroll is enabled
329
this.scrollAllToBottom();
330
// Update the main autoscroll state
@@ -335,6 +376,8 @@ class ScrollPositionManager {
376
// Disable autoscroll globally
377
disableAutoscroll() {
378
this.autoscrollDisabled = true;
379
+ // Start continuous monitoring when autoscroll is disabled
380
+ this.startContinuousMonitoring();
381
// Don't call window.toggleAutoScroll here to avoid circular dependency
382
// The main autoscroll state will be updated via scrollChanged function
383
}
@@ -342,6 +385,8 @@ class ScrollPositionManager {
385
// Enable autoscroll
386
enableAutoscroll() {
387
this.autoscrollDisabled = false;
388
+ // Stop continuous monitoring when autoscroll is enabled
389
+ this.stopContinuousMonitoring();
390
// Automatically scroll to bottom when autoscroll is enabled
391
this.scrollAllToBottom();
392
}
@@ -585,10 +630,10 @@ export function _drawMessage(
630
631
// Set up scroll listener for new content div
632
contentDiv.addEventListener('scroll', () => {
588
- if (!scrollManager.isAtBottom(contentDiv)) {
589
- scrollManager.disableAutoscroll();
590
- } else {
591
- // Re-enable autoscroll if user scrolls back to bottom
633
+ // Individual message scroll doesn't disable global autoscroll
634
+ // It only affects the scroll behavior of this specific element
635
+ if (scrollManager.isAtBottom(contentDiv, 20)) {
636
+ // If user scrolls back to bottom of this message, they might want autoscroll
637
scrollManager.reEnableAutoscrollIfAtBottom();
638
}
639
});
@@ -636,10 +681,10 @@ export function _drawMessage(
681
682
// Set up scroll listener for new pre element
683
preElement.addEventListener('scroll', () => {
639
- if (!scrollManager.isAtBottom(preElement)) {
640
- scrollManager.disableAutoscroll();
641
- } else {
642
- // Re-enable autoscroll if user scrolls back to bottom
684
+ // Individual message scroll doesn't disable global autoscroll
685
+ // It only affects the scroll behavior of this specific element
686
+ if (scrollManager.isAtBottom(preElement, 20)) {
687
+ // If user scrolls back to bottom of this message, they might want autoscroll
688
scrollManager.reEnableAutoscrollIfAtBottom();
689
}
690
});
@@ -1211,10 +1256,10 @@ function drawKvpsIncremental(container, kvps, latex) {
1256
1257
// Set up scroll listener for new kvp value div
1258
tdiv.addEventListener('scroll', () => {
1214
- if (!scrollManager.isAtBottom(tdiv)) {
1215
- scrollManager.disableAutoscroll();
1216
- } else {
1217
- // Re-enable autoscroll if user scrolls back to bottom
1259
+ // Individual KVP scroll doesn't disable global autoscroll
1260
+ // It only affects the scroll behavior of this specific element
1261
+ if (scrollManager.isAtBottom(tdiv, 20)) {
1262
+ // If user scrolls back to bottom of this KVP, they might want autoscroll
1263
scrollManager.reEnableAutoscrollIfAtBottom();
1264
}
1265
});