try to fix scroll stuttering in output message
Rafael Uzarowski committed
Aug 3, 2025 at 15:14 UTC
37b8a23f2d4c8feeff80b34a2a4f5c8bff35e5a2
3 files changed
+144
-211
webui/css/messages.css
+5
-3
@@ -402,9 +402,11 @@
402
}
403
404
.msg-content {
405
- margin-bottom: 0;
406
- padding: 0;
407
- overflow: hidden;
405
+ max-height: 30em;
406
+ overflow-y: auto;
407
+ overflow-x: hidden;
408
+ word-wrap: break-word;
409
+ white-space: pre-wrap;
410
}
411
412
.message-temp {
webui/index.js
+105
-1
@@ -1113,11 +1113,112 @@ function updateAfterScroll() {
1113
chatHistory.scrollHeight - chatHistory.scrollTop <=
1114
chatHistory.clientHeight + tolerancePx;
1115
1116
- scrollChanged(isAtBottom);
1116
+ // Check if any scrollable message content areas are scrolled up
1117
+ const allMsgContent = document.querySelectorAll('.msg-content');
1118
+ let anyMsgContentScrolledUp = false;
1119
+ for (const element of allMsgContent) {
1120
+ if (element.scrollHeight > element.clientHeight) {
1121
+ const elementIsAtBottom = element.scrollHeight - element.scrollTop <= element.clientHeight + tolerancePx;
1122
+ if (!elementIsAtBottom) {
1123
+ anyMsgContentScrolledUp = true;
1124
+ break;
1125
+ }
1126
+ }
1127
+ }
1128
+
1129
+ // Check if any KVP value areas are scrolled up
1130
+ const allKvpValues = document.querySelectorAll('.kvps-val');
1131
+ let anyKvpScrolledUp = false;
1132
+ for (const element of allKvpValues) {
1133
+ if (element.scrollHeight > element.clientHeight) {
1134
+ const elementIsAtBottom = element.scrollHeight - element.scrollTop <= element.clientHeight + tolerancePx;
1135
+ if (!elementIsAtBottom) {
1136
+ anyKvpScrolledUp = true;
1137
+ break;
1138
+ }
1139
+ }
1140
+ }
1141
+
1142
+ // Check if any message body areas (terminal messages) are scrolled up
1143
+ const allMsgBody = document.querySelectorAll('.message-body');
1144
+ let anyMsgBodyScrolledUp = false;
1145
+ for (const element of allMsgBody) {
1146
+ if (element.scrollHeight > element.clientHeight) {
1147
+ const elementIsAtBottom = element.scrollHeight - element.scrollTop <= element.clientHeight + tolerancePx;
1148
+ if (!elementIsAtBottom) {
1149
+ anyMsgBodyScrolledUp = true;
1150
+ break;
1151
+ }
1152
+ }
1153
+ }
1154
+
1155
+ // Only consider at bottom if main history is at bottom AND no individual areas are scrolled up
1156
+ const isReallyAtBottom = isAtBottom && !anyMsgContentScrolledUp && !anyKvpScrolledUp && !anyMsgBodyScrolledUp;
1157
+
1158
+ scrollChanged(isReallyAtBottom);
1159
}
1160
1161
chatHistory.addEventListener("scroll", updateAfterScroll);
1162
1163
+// Set up scroll listeners for all scrollable message content areas
1164
+function setupMessageScrollListeners() {
1165
+ // Set up scroll listeners for msg-content elements
1166
+ const allMsgContent = document.querySelectorAll('.msg-content');
1167
+ allMsgContent.forEach(element => {
1168
+ if (element.scrollHeight > element.clientHeight) {
1169
+ element.addEventListener('scroll', updateAfterScroll);
1170
+ }
1171
+ });
1172
+
1173
+ // Set up scroll listeners for kvps-val elements
1174
+ const allKvpValues = document.querySelectorAll('.kvps-val');
1175
+ allKvpValues.forEach(element => {
1176
+ if (element.scrollHeight > element.clientHeight) {
1177
+ element.addEventListener('scroll', updateAfterScroll);
1178
+ }
1179
+ });
1180
+
1181
+ // Set up scroll listeners for message-body elements (terminal messages)
1182
+ const allMsgBody = document.querySelectorAll('.message-body');
1183
+ allMsgBody.forEach(element => {
1184
+ if (element.scrollHeight > element.clientHeight) {
1185
+ element.addEventListener('scroll', updateAfterScroll);
1186
+ }
1187
+ });
1188
+}
1189
+
1190
+// Function to add scroll listeners to new message content
1191
+function addScrollListenersToMessage(messageContainer) {
1192
+ if (!messageContainer) return;
1193
+
1194
+ // Add scroll listeners to msg-content elements
1195
+ const msgContentElements = messageContainer.querySelectorAll('.msg-content');
1196
+ msgContentElements.forEach(element => {
1197
+ if (element.scrollHeight > element.clientHeight) {
1198
+ element.addEventListener('scroll', updateAfterScroll);
1199
+ }
1200
+ });
1201
+
1202
+ // Add scroll listeners to kvps-val elements
1203
+ const kvpElements = messageContainer.querySelectorAll('.kvps-val');
1204
+ kvpElements.forEach(element => {
1205
+ if (element.scrollHeight > element.clientHeight) {
1206
+ element.addEventListener('scroll', updateAfterScroll);
1207
+ }
1208
+ });
1209
+
1210
+ // Add scroll listeners to message-body elements
1211
+ const msgBodyElements = messageContainer.querySelectorAll('.message-body');
1212
+ msgBodyElements.forEach(element => {
1213
+ if (element.scrollHeight > element.clientHeight) {
1214
+ element.addEventListener('scroll', updateAfterScroll);
1215
+ }
1216
+ });
1217
+}
1218
+
1219
+// Make function available globally
1220
+window.addScrollListenersToMessage = addScrollListenersToMessage;
1221
+
1222
chatInput.addEventListener("input", adjustTextareaHeight);
1223
1224
// setInterval(poll, 250);
@@ -1154,6 +1255,9 @@ document.addEventListener("DOMContentLoaded", function () {
1255
setupSidebarToggle();
1256
setupTabs();
1257
initializeActiveTab();
1258
+
1259
+ // Set up scroll listeners for existing message content
1260
+ setupMessageScrollListeners();
1261
});
1262
1263
// Setup tabs functionality
webui/js/messages.js
+34
-207
@@ -20,102 +20,14 @@ class ScrollPositionManager {
20
21
// Store scroll positions for all scrollable elements in a message
22
storeMessageScrollPositions(messageContainer) {
23
- if (!messageContainer) return;
24
-
25
- const messageId = messageContainer.id;
26
- const positions = {};
27
-
28
- // Store main message content scroll position
29
- const msgContent = messageContainer.querySelector('.msg-content');
30
- if (msgContent && msgContent.scrollHeight > msgContent.clientHeight) {
31
- positions.msgContent = {
32
- scrollTop: msgContent.scrollTop,
33
- scrollHeight: msgContent.scrollHeight,
34
- clientHeight: msgContent.clientHeight,
35
- isAtBottom: this.isAtBottom(msgContent)
36
- };
37
- }
38
-
39
- // Store message body scroll position (for terminal messages)
40
- const msgBody = messageContainer.querySelector('.message-body');
41
- if (msgBody && msgBody.scrollHeight > msgBody.clientHeight) {
42
- positions.msgBody = {
43
- scrollTop: msgBody.scrollTop,
44
- scrollHeight: msgBody.scrollHeight,
45
- clientHeight: msgBody.clientHeight,
46
- isAtBottom: this.isAtBottom(msgBody)
47
- };
48
- }
49
-
50
- // Store KVP scroll positions
51
- const kvpValues = messageContainer.querySelectorAll('.kvps-val');
52
- kvpValues.forEach((kvp, index) => {
53
- if (kvp.scrollHeight > kvp.clientHeight) {
54
- positions[`kvp_${index}`] = {
55
- scrollTop: kvp.scrollTop,
56
- scrollHeight: kvp.scrollHeight,
57
- clientHeight: kvp.clientHeight,
58
- isAtBottom: this.isAtBottom(kvp)
59
- };
60
- }
61
- });
62
-
63
- this.positions.set(messageId, positions);
23
+ // Disabled to prevent scroll position resets
24
+ return;
25
}
26
27
// Restore scroll positions for a message
28
restoreMessageScrollPositions(messageContainer) {
68
- if (!messageContainer) return;
69
-
70
- const messageId = messageContainer.id;
71
- const positions = this.positions.get(messageId);
72
- if (!positions) return;
73
-
74
- // Use a small delay to ensure DOM is fully updated
75
- setTimeout(() => {
76
- // Restore main message content scroll
77
- if (positions.msgContent) {
78
- const msgContent = messageContainer.querySelector('.msg-content');
79
- if (msgContent) {
80
- if (this.autoscrollDisabled || !positions.msgContent.isAtBottom) {
81
- // User had scrolled up, restore their position
82
- msgContent.scrollTop = positions.msgContent.scrollTop;
83
- } else {
84
- // User was at bottom, keep at bottom (autoscroll)
85
- msgContent.scrollTop = msgContent.scrollHeight;
86
- }
87
- }
88
- }
89
-
90
- // Restore message body scroll position (for terminal messages)
91
- if (positions.msgBody) {
92
- const msgBody = messageContainer.querySelector('.message-body');
93
- if (msgBody) {
94
- if (this.autoscrollDisabled || !positions.msgBody.isAtBottom) {
95
- // User had scrolled up, restore their position
96
- msgBody.scrollTop = positions.msgBody.scrollTop;
97
- } else {
98
- // User was at bottom, keep at bottom (autoscroll)
99
- msgBody.scrollTop = msgBody.scrollHeight;
100
- }
101
- }
102
- }
103
-
104
- // Restore KVP scroll positions
105
- const kvpValues = messageContainer.querySelectorAll('.kvps-val');
106
- kvpValues.forEach((kvp, index) => {
107
- const posKey = `kvp_${index}`;
108
- if (positions[posKey]) {
109
- if (this.autoscrollDisabled || !positions[posKey].isAtBottom) {
110
- // User had scrolled up, restore their position
111
- kvp.scrollTop = positions[posKey].scrollTop;
112
- } else {
113
- // User was at bottom, keep at bottom (autoscroll)
114
- kvp.scrollTop = kvp.scrollHeight;
115
- }
116
- }
117
- });
118
- }, 10); // Small delay to ensure DOM is ready
29
+ // Disabled to prevent scroll position resets
30
+ return;
31
}
32
33
// Check global scroll state and update autoscroll disabled flag
@@ -200,37 +112,26 @@ class ScrollPositionManager {
112
return true;
113
}
114
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);
115
+ // Enhanced method to re-enable autoscroll when user scrolls to bottom
116
+ reEnableAutoscrollIfAtBottom() {
117
+ // Check if main chat history is at bottom - this is the primary indicator
118
+ const chatHistory = document.getElementById("chat-history");
119
+ const isMainHistoryAtBottom = chatHistory && this.isAtBottom(chatHistory, 20);
120
217
- // Check if chat input is at bottom
218
- const chatInput = document.getElementById("chat-input");
219
- const isChatInputAtBottom = chatInput && this.isAtBottom(chatInput, 20);
121
+ // Check if chat input is at bottom
122
+ const chatInput = document.getElementById("chat-input");
123
+ const isChatInputAtBottom = chatInput && this.isAtBottom(chatInput, 20);
124
221
- // If main chat history is at bottom, re-enable autoscroll regardless of individual message positions
222
- if (isMainHistoryAtBottom && isChatInputAtBottom) {
223
- this.reEnableAutoscrollIfAtBottom();
224
- }
125
+ // If main chat history is at bottom, re-enable autoscroll regardless of individual message positions
126
+ // This allows users to scroll up in individual messages but still have autoscroll when they scroll down the main history
127
+ if (isMainHistoryAtBottom && isChatInputAtBottom) {
128
+ this.autoscrollDisabled = false;
129
+ // Scroll all elements to bottom when autoscroll is enabled
130
+ this.scrollAllToBottom();
131
+ // Update the main autoscroll state
132
+ if (window.updateAfterScroll) {
133
+ window.updateAfterScroll();
134
}
226
- }, 100);
227
- }
228
-
229
- // Stop continuous monitoring
230
- stopContinuousMonitoring() {
231
- if (this.monitoringInterval) {
232
- clearInterval(this.monitoringInterval);
233
- this.monitoringInterval = null;
135
}
136
}
137
@@ -286,8 +187,6 @@ class ScrollPositionManager {
187
chatHistory.addEventListener('scroll', () => {
188
if (!this.isAtBottom(chatHistory)) {
189
this.disableAutoscroll();
289
- // Start continuous monitoring when autoscroll is disabled
290
- this.startContinuousMonitoring();
190
} else {
191
this.reEnableAutoscrollIfAtBottom();
192
}
@@ -300,8 +199,6 @@ class ScrollPositionManager {
199
chatInput.addEventListener('scroll', () => {
200
if (!this.isAtBottom(chatInput)) {
201
this.disableAutoscroll();
303
- // Start continuous monitoring when autoscroll is disabled
304
- this.startContinuousMonitoring();
202
} else {
203
this.reEnableAutoscrollIfAtBottom();
204
}
@@ -309,31 +206,6 @@ class ScrollPositionManager {
206
}
207
}
208
312
- // Enhanced method to re-enable autoscroll when user scrolls to bottom
313
- reEnableAutoscrollIfAtBottom() {
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
331
- if (window.updateAfterScroll) {
332
- window.updateAfterScroll();
333
- }
334
- }
335
- }
336
-
209
// Method to scroll all scrollable elements to the bottom
210
scrollAllToBottom() {
211
// Scroll main chat history to bottom
@@ -377,7 +249,7 @@ class ScrollPositionManager {
249
disableAutoscroll() {
250
this.autoscrollDisabled = true;
251
// Start continuous monitoring when autoscroll is disabled
380
- this.startContinuousMonitoring();
252
+ // this.startContinuousMonitoring(); // Removed continuous monitoring
253
// Don't call window.toggleAutoScroll here to avoid circular dependency
254
// The main autoscroll state will be updated via scrollChanged function
255
}
@@ -386,7 +258,7 @@ class ScrollPositionManager {
258
enableAutoscroll() {
259
this.autoscrollDisabled = false;
260
// Stop continuous monitoring when autoscroll is enabled
389
- this.stopContinuousMonitoring();
261
+ // this.stopContinuousMonitoring(); // Removed continuous monitoring
262
// Automatically scroll to bottom when autoscroll is enabled
263
this.scrollAllToBottom();
264
}
@@ -406,9 +278,6 @@ export function setMessage(id, type, heading, content, temp, kvps = null) {
278
let isNewMessage = false;
279
280
if (messageContainer) {
409
- // Store current scroll positions before updating
410
- scrollManager.storeMessageScrollPositions(messageContainer);
411
-
281
// Don't clear innerHTML - we'll do incremental updates
282
// messageContainer.innerHTML = "";
283
} else {
@@ -418,7 +287,6 @@ export function setMessage(id, type, heading, content, temp, kvps = null) {
287
messageContainer = document.createElement("div");
288
messageContainer.id = `message-${id}`;
289
messageContainer.classList.add("message-container", `${sender}-container`);
421
- // if (temp) messageContainer.classList.add("message-temp");
290
}
291
292
const handler = getHandler(type);
@@ -428,50 +296,22 @@ export function setMessage(id, type, heading, content, temp, kvps = null) {
296
if (isNewMessage && !document.getElementById(`message-${id}`)) {
297
// message type visual grouping
298
const groupTypeMap = {
431
- user: "right",
432
- info: "mid",
433
- warning: "mid",
434
- error: "mid",
435
- rate_limit: "mid",
436
- util: "mid",
437
- hint: "mid",
438
- // anything else is "left"
439
- };
440
-
441
- //force new group on these types
442
- const groupStart = {
443
- agent: true,
444
- // anything else is false
299
+ user: "message-group-right",
300
+ ai: "message-group-mid",
301
+ tool: "message-group-mid",
302
+ default: "message-group-mid",
303
};
446
-
447
- const groupType = groupTypeMap[type] || "left";
448
-
449
- // here check if messageGroup is still in DOM, if not, then set it to null (context switch)
450
- if(messageGroup && !document.getElementById(messageGroup.id))
451
- messageGroup = null;
452
-
453
- if (
454
- !messageGroup || // no group yet exists
455
- groupStart[type] || // message type forces new group
456
- groupType != messageGroup.getAttribute("data-group-type") // message type changes group
457
- ) {
458
- messageGroup = document.createElement("div");
459
- messageGroup.id = `message-group-${id}`;
460
- messageGroup.classList.add(`message-group`, `message-group-${groupType}`);
461
- messageGroup.setAttribute("data-group-type", groupType);
462
- }
463
-
304
+ const groupType = groupTypeMap[type] || "message-group-mid";
305
+ messageGroup = document.createElement("div");
306
+ messageGroup.classList.add("message-group", groupType);
307
messageGroup.appendChild(messageContainer);
308
chatHistory.appendChild(messageGroup);
309
310
// Set up scroll listeners for new message
311
scrollManager.setupScrollListeners(messageContainer);
469
- } else {
470
- // For existing messages, restore scroll positions after DOM update
471
- setTimeout(() => {
472
- scrollManager.restoreMessageScrollPositions(messageContainer);
473
- }, 0);
312
}
313
+
314
+ return messageContainer;
315
}
316
317
function createCopyButton() {
@@ -1280,21 +1120,8 @@ function drawKvpsIncremental(container, kvps, latex) {
1120
addValue(value, tdiv);
1121
}
1122
1283
- // Restore scroll position or autoscroll
1284
- setTimeout(() => {
1285
- if (!scrollManager.autoscrollDisabled && isAtBottom) {
1286
- // User was at bottom, keep at bottom
1287
- tdiv.scrollTop = tdiv.scrollHeight;
1288
- } else if (!scrollManager.autoscrollDisabled) {
1289
- // User was at bottom, keep at bottom (autoscroll)
1290
- tdiv.scrollTop = tdiv.scrollHeight;
1291
- } else {
1292
- // Restore previous position only if user had scrolled up
1293
- if (currentScrollTop > 0) {
1294
- tdiv.scrollTop = currentScrollTop;
1295
- }
1296
- }
1297
- }, 10);
1123
+ // Don't restore scroll position to prevent resets
1124
+ // Let the natural scroll behavior work
1125
});
1126
1127
// Remove extra rows if we have fewer kvps now