ui: message queue polishing, scrollbars unified
frdel committed
Jan 31, 2026 at 12:13 UTC
020822adfd443283808c23ca8d34f286685918da
8 files changed
+118
-42
webui/components/chat/attachments/inputPreview.html
+1
-1
@@ -6,7 +6,7 @@
6
<div x-data>
7
<template x-if="$store.chatAttachments">
8
9
- <div x-show="$store.chatAttachments.hasAttachments" class="preview-section">
9
+ <div x-show="$store.chatAttachments.hasAttachments" class="preview-section scrollbar">
10
<template x-for="(attachment, index) in $store.chatAttachments.attachments" :key="index">
11
<div class="attachment-item"
12
:class="{'image-type': attachment.type === 'image', 'file-type': attachment.type === 'file'}">
webui/components/chat/input/input-store.js
+1
-1
@@ -16,7 +16,7 @@ const model = {
16
const running = chatTopStore?.running;
17
18
if (hasQueue && !hasInput) return "send-all";
19
- if (running && hasInput) return "queue";
19
+ if ((running || hasQueue) && hasInput) return "queue";
20
return "send";
21
},
22
webui/components/chat/message-queue/message-queue-store.js
+58
-5
@@ -1,11 +1,31 @@
1
import { createStore } from "/js/AlpineStore.js";
2
import { store as navStore } from "/components/chat/navigation/chat-navigation-store.js";
3
import * as api from "/js/api.js";
4
+import { toastFrontendInfo, NotificationPriority } from "/components/notifications/notification-store.js";
5
+import { sleep } from "/js/sleep.js";
6
7
const model = {
8
items: [],
9
pendingItems: [], // Local pending items (uploading to queue)
10
11
+ _getQueueScrollerEl() {
12
+ return document.querySelector(".queue-preview .queue-items");
13
+ },
14
+
15
+ scrollQueueToBottom() {
16
+ const el = this._getQueueScrollerEl();
17
+ if (!el) return;
18
+
19
+ const scroll = () => {
20
+ el.scrollTop = el.scrollHeight;
21
+ };
22
+
23
+ requestAnimationFrame(() => {
24
+ scroll();
25
+ requestAnimationFrame(scroll);
26
+ });
27
+ },
28
+
29
get hasQueue() {
30
return this.items.length > 0 || this.pendingItems.length > 0;
31
},
@@ -34,6 +54,7 @@ const model = {
54
55
// Add to pending immediately for UI feedback
56
this.pendingItems = [...this.pendingItems, pendingItem];
57
+ this.scrollQueueToBottom();
58
59
try {
60
let filenames = [];
@@ -49,14 +70,16 @@ const model = {
70
}
71
}
72
const response = await api.callJsonApi("/message_queue_add", { context, text, attachments: filenames });
52
-
53
- // Remove from pending (poll will add the confirmed item)
54
- this.pendingItems = this.pendingItems.filter(p => p.id !== tempId);
73
+
74
+ const serverId = response?.item_id;
75
+ if (serverId) {
76
+ this.pendingItems = this.pendingItems.map((p) =>
77
+ p.id === tempId ? { ...p, serverId } : p,
78
+ );
79
+ }
80
return response?.ok || false;
81
} catch (e) {
82
console.error("Failed to queue message:", e);
58
- // Remove from pending on error
59
- this.pendingItems = this.pendingItems.filter(p => p.id !== tempId);
83
return false;
84
}
85
},
@@ -94,6 +117,24 @@ const model = {
117
async sendAll() {
118
const context = globalThis.getContext?.();
119
if (!context || !this.hasQueue) return;
120
+
121
+ // check for pending uploads and notify user
122
+ if (this.pendingItems.length > 0) {
123
+ await sleep(1000);
124
+ if (this.pendingItems.length > 0) {
125
+ toastFrontendInfo(
126
+ "There are pending uploads in the queue. You can wait for them to finish or remove them.",
127
+ "Pending uploads",
128
+ 3,
129
+ "pending-uploads",
130
+ NotificationPriority.NORMAL,
131
+ true,
132
+ );
133
+ return;
134
+ }
135
+ }
136
+
137
+ if (!this.hasQueue) return;
138
try {
139
navStore.scrollToBottom();
140
await api.callJsonApi("/message_queue_send", { context, send_all: true });
@@ -104,6 +145,18 @@ const model = {
145
146
updateFromPoll(queue) {
147
this.items = queue || [];
148
+
149
+ if (this.pendingItems.length > 0) {
150
+ const hasPendingWithServerId = this.pendingItems.some((p) => p.serverId);
151
+ if (hasPendingWithServerId) {
152
+ const serverIds = new Set(this.items.map((i) => i.id).filter(Boolean));
153
+ this.pendingItems = this.pendingItems.filter((p) => {
154
+ if (!p.serverId) return true;
155
+ return !serverIds.has(p.serverId);
156
+ });
157
+ }
158
+ }
159
+ // this.scrollQueueToBottom();
160
},
161
162
getAttachmentUrl(filename) {
webui/components/chat/message-queue/message-queue.html
+1
-1
@@ -233,7 +233,7 @@
233
}
234
235
.queue-item-pending .queue-item-actions {
236
- display: none;
236
+ visibility: hidden !important;
237
}
238
239
.queue-item-pending-icon {
webui/css/messages.css
-28
@@ -16,34 +16,6 @@
16
padding-bottom: 4rem !important;
17
-webkit-transition: all 0.3s ease;
18
transition: all 0.3s ease;
19
- scrollbar-width: thin;
20
- scrollbar-color: #555 transparent;
21
-}
22
-
23
-/* Scrollbar styling for Firefox */
24
-#chat-history::-webkit-scrollbar {
25
- width: 5px;
26
-}
27
-
28
-#chat-history::-webkit-scrollbar-track {
29
- box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.3);
30
- border-radius: 3px;
31
-}
32
-
33
-#chat-history::-webkit-scrollbar-thumb {
34
- border-radius: 3px;
35
- box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.3);
36
- background-color: #555;
37
- -webkit-transition: background-color var(--transition-speed) ease-in-out;
38
- transition: background-color var(--transition-speed) ease-in-out;
39
-}
40
-
41
-#chat-history::-webkit-scrollbar-thumb:hover {
42
- background-color: #666;
43
-}
44
-
45
-#chat-history::-webkit-scrollbar-thumb:active {
46
- background-color: #888;
19
}
20
21
/* Message Styles */
webui/index.css
+51
@@ -1561,4 +1561,55 @@ nav ul li a img {
1561
1562
.display-none{
1563
display: none !important;
1564
+}
1565
+
1566
+body * {
1567
+ scrollbar-width: thin;
1568
+ scrollbar-color: #555 transparent;
1569
+}
1570
+
1571
+body *::-webkit-scrollbar {
1572
+ width: 5px;
1573
+}
1574
+
1575
+body *::-webkit-scrollbar-track {
1576
+ box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.3);
1577
+ border-radius: 3px;
1578
+}
1579
+
1580
+body *::-webkit-scrollbar-thumb {
1581
+ border-radius: 3px;
1582
+ box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.3);
1583
+ background-color: #555;
1584
+ -webkit-transition: background-color var(--transition-speed) ease-in-out;
1585
+ transition: background-color var(--transition-speed) ease-in-out;
1586
+}
1587
+
1588
+body *::-webkit-scrollbar-thumb:hover {
1589
+ background-color: #666;
1590
+}
1591
+
1592
+body *::-webkit-scrollbar-thumb:active {
1593
+ background-color: #888;
1594
+}
1595
+
1596
+.light-mode * {
1597
+ scrollbar-color: #b7b7b7 transparent;
1598
+}
1599
+
1600
+.light-mode *::-webkit-scrollbar-track {
1601
+ box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.12);
1602
+}
1603
+
1604
+.light-mode *::-webkit-scrollbar-thumb {
1605
+ box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.12);
1606
+ background-color: #b7b7b7;
1607
+}
1608
+
1609
+.light-mode *::-webkit-scrollbar-thumb:hover {
1610
+ background-color: #9f9f9f;
1611
+}
1612
+
1613
+.light-mode *::-webkit-scrollbar-thumb:active {
1614
+ background-color: #8a8a8a;
1615
}
\ No newline at end of file
webui/index.js
+5
-4
@@ -54,13 +54,14 @@ export async function sendMessage() {
54
55
if (message || hasAttachments) {
56
// Check if agent is busy - queue instead of sending
57
- if (chatTopStore.running) {
58
- const success = await messageQueueStore.addToQueue(message, attachmentsWithUrls);
59
- if (success) {
57
+ if (chatTopStore.running || messageQueueStore.hasQueue) {
58
+ const success = messageQueueStore.addToQueue(message, attachmentsWithUrls);
59
+ // no await for the queue
60
+ // if (success) {
61
chatInputEl.value = "";
62
attachmentsStore.clearAttachments();
63
adjustTextareaHeight();
63
- }
64
+ // }
65
return;
66
}
67
webui/js/messages.js
+1
-2
@@ -336,8 +336,7 @@ function drawProcessStep({
336
stepDetail,
337
".process-step-detail-scroll",
338
"div",
339
- "process-step-detail-scroll",
340
- );
339
+ "process-step-detail-scroll" );
340
341
// set click handlers
342
setupProcessStepHandlers(step, stepHeader);