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);