fix: scrolling of content in code-exe should honor global autoscroll
Rafael Uzarowski committed
Aug 1, 2025 at 21:50 UTC
fbf9028f285917011f4902da4c5291cd535b2144
2 files changed
+228
-83
webui/index.js
+9
-1
@@ -233,7 +233,8 @@ function setMessage(id, type, heading, content, temp, kvps = null) {
233
234
// Use enhanced autoscroll logic that considers scrollable elements within messages
235
if (autoScroll && !msgs.getScrollManager().autoscrollDisabled) {
236
- chatHistory.scrollTop = chatHistory.scrollHeight;
236
+ // Scroll all scrollable elements to bottom when autoscroll is enabled
237
+ msgs.getScrollManager().scrollAllToBottom();
238
}
239
240
return result;
@@ -775,6 +776,8 @@ window.toggleAutoScroll = async function (_autoScroll) {
776
if (msgs.getScrollManager) {
777
if (_autoScroll) {
778
msgs.getScrollManager().enableAutoscroll();
779
+ // Scroll to bottom when autoscroll is enabled
780
+ msgs.getScrollManager().scrollAllToBottom();
781
} else {
782
msgs.getScrollManager().disableAutoscroll();
783
}
@@ -869,6 +872,11 @@ window.restart = async function () {
872
document.addEventListener("DOMContentLoaded", () => {
873
const isDarkMode = localStorage.getItem("darkMode") !== "false";
874
toggleDarkMode(isDarkMode);
875
+
876
+ // Initialize scroll manager global listeners
877
+ if (msgs.getScrollManager) {
878
+ msgs.getScrollManager().setupGlobalScrollListeners();
879
+ }
880
});
881
882
window.loadChats = async function () {
webui/js/messages.js
+219
-82
@@ -70,56 +70,150 @@ class ScrollPositionManager {
70
const positions = this.positions.get(messageId);
71
if (!positions) return;
72
73
- // Restore main message content scroll
74
- if (positions.msgContent) {
75
- const msgContent = messageContainer.querySelector('.msg-content');
76
- if (msgContent) {
77
- if (this.autoscrollDisabled || !positions.msgContent.isAtBottom) {
78
- // User had scrolled up, restore their position
79
- msgContent.scrollTop = positions.msgContent.scrollTop;
80
- } else {
81
- // User was at bottom, keep at bottom (autoscroll)
82
- msgContent.scrollTop = msgContent.scrollHeight;
73
+ // Use a small delay to ensure DOM is fully updated
74
+ setTimeout(() => {
75
+ // Restore main message content scroll
76
+ if (positions.msgContent) {
77
+ const msgContent = messageContainer.querySelector('.msg-content');
78
+ if (msgContent) {
79
+ if (this.autoscrollDisabled || !positions.msgContent.isAtBottom) {
80
+ // User had scrolled up, restore their position
81
+ msgContent.scrollTop = positions.msgContent.scrollTop;
82
+ } else {
83
+ // User was at bottom, keep at bottom (autoscroll)
84
+ msgContent.scrollTop = msgContent.scrollHeight;
85
+ }
86
}
87
}
85
- }
88
87
- // Restore message body scroll position (for terminal messages)
88
- if (positions.msgBody) {
89
- const msgBody = messageContainer.querySelector('.message-body');
90
- if (msgBody) {
91
- if (this.autoscrollDisabled || !positions.msgBody.isAtBottom) {
92
- // User had scrolled up, restore their position
93
- msgBody.scrollTop = positions.msgBody.scrollTop;
94
- } else {
95
- // User was at bottom, keep at bottom (autoscroll)
96
- msgBody.scrollTop = msgBody.scrollHeight;
89
+ // Restore message body scroll position (for terminal messages)
90
+ if (positions.msgBody) {
91
+ const msgBody = messageContainer.querySelector('.message-body');
92
+ if (msgBody) {
93
+ if (this.autoscrollDisabled || !positions.msgBody.isAtBottom) {
94
+ // User had scrolled up, restore their position
95
+ msgBody.scrollTop = positions.msgBody.scrollTop;
96
+ } else {
97
+ // User was at bottom, keep at bottom (autoscroll)
98
+ msgBody.scrollTop = msgBody.scrollHeight;
99
+ }
100
}
101
}
99
- }
102
101
- // Restore KVP scroll positions
102
- const kvpValues = messageContainer.querySelectorAll('.kvps-val');
103
- kvpValues.forEach((kvp, index) => {
104
- const posKey = `kvp_${index}`;
105
- if (positions[posKey]) {
106
- if (this.autoscrollDisabled || !positions[posKey].isAtBottom) {
107
- // User had scrolled up, restore their position
108
- kvp.scrollTop = positions[posKey].scrollTop;
109
- } else {
110
- // User was at bottom, keep at bottom (autoscroll)
111
- kvp.scrollTop = kvp.scrollHeight;
103
+ // Restore KVP scroll positions
104
+ const kvpValues = messageContainer.querySelectorAll('.kvps-val');
105
+ kvpValues.forEach((kvp, index) => {
106
+ const posKey = `kvp_${index}`;
107
+ if (positions[posKey]) {
108
+ if (this.autoscrollDisabled || !positions[posKey].isAtBottom) {
109
+ // User had scrolled up, restore their position
110
+ kvp.scrollTop = positions[posKey].scrollTop;
111
+ } else {
112
+ // User was at bottom, keep at bottom (autoscroll)
113
+ kvp.scrollTop = kvp.scrollHeight;
114
+ }
115
}
116
+ });
117
+ }, 10); // Small delay to ensure DOM is ready
118
+ }
119
+
120
+ // Check global scroll state and update autoscroll disabled flag
121
+ checkGlobalScrollState() {
122
+ // Check main chat history
123
+ const chatHistory = document.getElementById("chat-history");
124
+ if (chatHistory && !this.isAtBottom(chatHistory)) {
125
+ this.autoscrollDisabled = true;
126
+ return;
127
+ }
128
+
129
+ // Check chat input area
130
+ const chatInput = document.getElementById("chat-input");
131
+ if (chatInput && !this.isAtBottom(chatInput)) {
132
+ this.autoscrollDisabled = true;
133
+ return;
134
+ }
135
+
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
}
114
- });
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
+ }
162
+
163
+ // If we get here, everything is at bottom, enable autoscroll
164
+ this.autoscrollDisabled = false;
165
}
166
117
- // Check if element is scrolled to bottom
167
+ // Improved method to check if element is scrolled to bottom with better tolerance
168
isAtBottom(element, tolerance = 10) {
119
- return element.scrollHeight - element.scrollTop <= element.clientHeight + tolerance;
169
+ if (!element) return true;
170
+ const scrollDiff = element.scrollHeight - element.scrollTop;
171
+ const clientHeight = element.clientHeight;
172
+ return scrollDiff <= clientHeight + tolerance;
173
}
174
122
- // Set up scroll event listeners for autoscroll detection
175
+ // Check if user is at bottom of all scrollable areas
176
+ isUserAtBottomOfAllScrollableAreas() {
177
+ // Check main chat history
178
+ const chatHistory = document.getElementById("chat-history");
179
+ if (chatHistory && !this.isAtBottom(chatHistory)) {
180
+ return false;
181
+ }
182
+
183
+ // Check chat input area
184
+ const chatInput = document.getElementById("chat-input");
185
+ if (chatInput && !this.isAtBottom(chatInput)) {
186
+ return false;
187
+ }
188
+
189
+ // Check all message content areas
190
+ const allMsgContent = document.querySelectorAll('.msg-content');
191
+ for (const element of allMsgContent) {
192
+ if (!this.isAtBottom(element)) {
193
+ return false;
194
+ }
195
+ }
196
+
197
+ // Check all message body areas (terminal messages)
198
+ const allMsgBody = document.querySelectorAll('.message-body');
199
+ for (const element of allMsgBody) {
200
+ if (!this.isAtBottom(element)) {
201
+ return false;
202
+ }
203
+ }
204
+
205
+ // Check all KVP areas
206
+ const allKvpValues = document.querySelectorAll('.kvps-val');
207
+ for (const element of allKvpValues) {
208
+ if (!this.isAtBottom(element)) {
209
+ return false;
210
+ }
211
+ }
212
+
213
+ return true;
214
+ }
215
+
216
+ // Set up scroll listeners for a message container
217
setupScrollListeners(messageContainer) {
218
if (!messageContainer) return;
219
@@ -130,8 +224,7 @@ class ScrollPositionManager {
224
if (!this.isAtBottom(msgContent)) {
225
this.disableAutoscroll();
226
} else {
133
- // Re-enable autoscroll if user scrolls back to bottom
134
- this.checkGlobalScrollState();
227
+ this.reEnableAutoscrollIfAtBottom();
228
}
229
});
230
}
@@ -143,8 +236,7 @@ class ScrollPositionManager {
236
if (!this.isAtBottom(msgBody)) {
237
this.disableAutoscroll();
238
} else {
146
- // Re-enable autoscroll if user scrolls back to bottom
147
- this.checkGlobalScrollState();
239
+ this.reEnableAutoscrollIfAtBottom();
240
}
241
});
242
}
@@ -155,63 +247,103 @@ class ScrollPositionManager {
247
if (!this.isAtBottom(kvp)) {
248
this.disableAutoscroll();
249
} else {
158
- // Re-enable autoscroll if user scrolls back to bottom
159
- this.checkGlobalScrollState();
250
+ this.reEnableAutoscrollIfAtBottom();
251
}
252
});
253
});
254
}
255
165
- // Disable autoscroll globally
166
- disableAutoscroll() {
167
- this.autoscrollDisabled = true;
168
- // Don't call window.toggleAutoScroll here to avoid circular dependency
169
- // The main autoscroll state will be updated via scrollChanged function
256
+ // Set up scroll listeners for global elements (chat history, chat input)
257
+ setupGlobalScrollListeners() {
258
+ // Set up scroll listener for main chat history
259
+ const chatHistory = document.getElementById("chat-history");
260
+ if (chatHistory) {
261
+ chatHistory.addEventListener('scroll', () => {
262
+ if (!this.isAtBottom(chatHistory)) {
263
+ this.disableAutoscroll();
264
+ } else {
265
+ this.reEnableAutoscrollIfAtBottom();
266
+ }
267
+ });
268
+ }
269
+
270
+ // Set up scroll listener for chat input
271
+ const chatInput = document.getElementById("chat-input");
272
+ if (chatInput) {
273
+ chatInput.addEventListener('scroll', () => {
274
+ if (!this.isAtBottom(chatInput)) {
275
+ this.disableAutoscroll();
276
+ } else {
277
+ this.reEnableAutoscrollIfAtBottom();
278
+ }
279
+ });
280
+ }
281
}
282
172
- // Enable autoscroll
173
- enableAutoscroll() {
174
- this.autoscrollDisabled = false;
283
+ // Enhanced method to re-enable autoscroll when user scrolls to bottom
284
+ reEnableAutoscrollIfAtBottom() {
285
+ if (this.isUserAtBottomOfAllScrollableAreas()) {
286
+ this.autoscrollDisabled = false;
287
+ // Scroll all elements to bottom when autoscroll is enabled
288
+ this.scrollAllToBottom();
289
+ // Update the main autoscroll state
290
+ if (window.updateAfterScroll) {
291
+ window.updateAfterScroll();
292
+ }
293
+ }
294
}
295
177
- // Check if autoscroll should be disabled based on any scrollable element
178
- checkGlobalScrollState() {
179
- // Check main chat history
296
+ // Method to scroll all scrollable elements to the bottom
297
+ scrollAllToBottom() {
298
+ // Scroll main chat history to bottom
299
const chatHistory = document.getElementById("chat-history");
181
- if (chatHistory && !this.isAtBottom(chatHistory, 50)) {
182
- this.autoscrollDisabled = true;
183
- return;
300
+ if (chatHistory) {
301
+ chatHistory.scrollTop = chatHistory.scrollHeight;
302
}
303
186
- // Check all message content areas
304
+ // Scroll chat input to bottom
305
+ const chatInput = document.getElementById("chat-input");
306
+ if (chatInput) {
307
+ chatInput.scrollTop = chatInput.scrollHeight;
308
+ }
309
+
310
+ // Scroll all message content areas to bottom
311
const allMsgContent = document.querySelectorAll('.msg-content');
188
- for (let element of allMsgContent) {
189
- if (element.scrollHeight > element.clientHeight && !this.isAtBottom(element)) {
190
- this.autoscrollDisabled = true;
191
- return;
312
+ allMsgContent.forEach(element => {
313
+ if (element.scrollHeight > element.clientHeight) {
314
+ element.scrollTop = element.scrollHeight;
315
}
193
- }
316
+ });
317
195
- // Check all message body areas (terminal messages)
318
+ // Scroll all message body areas (terminal messages) to bottom
319
const allMsgBody = document.querySelectorAll('.message-body');
197
- for (let element of allMsgBody) {
198
- if (element.scrollHeight > element.clientHeight && !this.isAtBottom(element)) {
199
- this.autoscrollDisabled = true;
200
- return;
320
+ allMsgBody.forEach(element => {
321
+ if (element.scrollHeight > element.clientHeight) {
322
+ element.scrollTop = element.scrollHeight;
323
}
202
- }
324
+ });
325
204
- // Check all KVP areas
326
+ // Scroll all KVP areas to bottom
327
const allKvpValues = document.querySelectorAll('.kvps-val');
206
- for (let element of allKvpValues) {
207
- if (element.scrollHeight > element.clientHeight && !this.isAtBottom(element)) {
208
- this.autoscrollDisabled = true;
209
- return;
328
+ allKvpValues.forEach(element => {
329
+ if (element.scrollHeight > element.clientHeight) {
330
+ element.scrollTop = element.scrollHeight;
331
}
211
- }
332
+ });
333
+ }
334
213
- // If we get here, everything is at bottom, enable autoscroll
335
+ // Disable autoscroll globally
336
+ disableAutoscroll() {
337
+ this.autoscrollDisabled = true;
338
+ // Don't call window.toggleAutoScroll here to avoid circular dependency
339
+ // The main autoscroll state will be updated via scrollChanged function
340
+ }
341
+
342
+ // Enable autoscroll
343
+ enableAutoscroll() {
344
this.autoscrollDisabled = false;
345
+ // Automatically scroll to bottom when autoscroll is enabled
346
+ this.scrollAllToBottom();
347
}
348
}
349
@@ -457,7 +589,7 @@ export function _drawMessage(
589
scrollManager.disableAutoscroll();
590
} else {
591
// Re-enable autoscroll if user scrolls back to bottom
460
- scrollManager.checkGlobalScrollState();
592
+ scrollManager.reEnableAutoscrollIfAtBottom();
593
}
594
});
595
} else {
@@ -508,7 +640,7 @@ export function _drawMessage(
640
scrollManager.disableAutoscroll();
641
} else {
642
// Re-enable autoscroll if user scrolls back to bottom
511
- scrollManager.checkGlobalScrollState();
643
+ scrollManager.reEnableAutoscrollIfAtBottom();
644
}
645
});
646
} else {
@@ -1083,7 +1215,7 @@ function drawKvpsIncremental(container, kvps, latex) {
1215
scrollManager.disableAutoscroll();
1216
} else {
1217
// Re-enable autoscroll if user scrolls back to bottom
1086
- scrollManager.checkGlobalScrollState();
1218
+ scrollManager.reEnableAutoscrollIfAtBottom();
1219
}
1220
});
1221
}
@@ -1108,11 +1240,16 @@ function drawKvpsIncremental(container, kvps, latex) {
1240
if (!scrollManager.autoscrollDisabled && isAtBottom) {
1241
// User was at bottom, keep at bottom
1242
tdiv.scrollTop = tdiv.scrollHeight;
1243
+ } else if (!scrollManager.autoscrollDisabled) {
1244
+ // User was at bottom, keep at bottom (autoscroll)
1245
+ tdiv.scrollTop = tdiv.scrollHeight;
1246
} else {
1112
- // Restore previous position
1113
- tdiv.scrollTop = currentScrollTop;
1247
+ // Restore previous position only if user had scrolled up
1248
+ if (currentScrollTop > 0) {
1249
+ tdiv.scrollTop = currentScrollTop;
1250
+ }
1251
}
1115
- }, 0);
1252
+ }, 10);
1253
});
1254
1255
// Remove extra rows if we have fewer kvps now