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