Adjust message queue behavior

keyboardstaff committed Jan 31, 2026 at 00:23 UTC 34e0997e73e945562cd4c6f846dd898af25a68de
6 files changed +129 -52
webui/components/chat/input/chat-bar-input.html
+13 -21
@@ -35,18 +35,22 @@
35 <!-- Send button -->
36 <button class="chat-button" id="send-button" aria-label="Send message"
37 @click="$store.chatInput.sendMessage()"
38 - :class="{ 'send-queue': $store.messageQueue?.hasQueue }"
39 - :title="$store.messageQueue?.hasQueue ? 'Send queued messages' : 'Send message'">
40 - <!-- Normal send icon -->
41 - <template x-if="!$store.messageQueue?.hasQueue">
38 + :class="$store.chatInput.sendButtonClass"
39 + :title="$store.chatInput.sendButtonTitle">
40 + <!-- Send all queued: double arrow -->
41 + <template x-if="$store.chatInput.sendButtonIcon === 'send-all'">
42 + <span class="material-symbols-outlined">keyboard_double_arrow_right</span>
43 + </template>
44 + <!-- Queue message: schedule send -->
45 + <template x-if="$store.chatInput.sendButtonIcon === 'queue'">
46 + <span class="material-symbols-outlined">schedule_send</span>
47 + </template>
48 + <!-- Normal send -->
49 + <template x-if="$store.chatInput.sendButtonIcon === 'send'">
50 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
51 <path d="M25 20 L75 50 L25 80" fill="none" stroke="currentColor" stroke-width="15"></path>
52 </svg>
53 </template>
46 - <!-- Spinning loader when queue has messages -->
47 - <template x-if="$store.messageQueue?.hasQueue">
48 - <span class="send-queue-spinner"></span>
49 - </template>
54 </button>
55
56 <!-- Microphone button -->
@@ -123,19 +127,7 @@
127 #send-button.send-queue:hover { background-color: #d35400; }
128 #send-button:active { -webkit-transform: scale(1); transform: scale(1); transform-origin: center; background-color: #2b309c; }
129 .chat-button svg { width: 1.5rem; height: 1.5rem; }
126 -
127 - /* Queue spinner in send button */
128 - .send-queue-spinner {
129 - width: 18px;
130 - height: 18px;
131 - border: 3px solid rgba(255,255,255,0.3);
132 - border-top-color: white;
133 - border-radius: 50%;
134 - animation: send-queue-spin 0.8s linear infinite;
135 - }
136 - @keyframes send-queue-spin {
137 - to { transform: rotate(360deg); }
138 - }
130 + #send-button .material-symbols-outlined { font-size: 1.5rem; }
131
132 /* Microphone button */
133 .chat-button.mic-inactive svg { /* Add specific styles if needed */ }
webui/components/chat/input/input-store.js
+31
@@ -1,10 +1,41 @@
1 import { createStore } from "/js/AlpineStore.js";
2 import * as shortcuts from "/js/shortcuts.js";
3 import { store as fileBrowserStore } from "/components/modals/file-browser/file-browser-store.js";
4 +import { store as messageQueueStore } from "/components/chat/message-queue/message-queue-store.js";
5 +import { store as chatTopStore } from "/components/chat/top-section/chat-top-store.js";
6 +import { store as attachmentsStore } from "/components/chat/attachments/attachmentsStore.js";
7
8 const model = {
9 paused: false,
10
11 + // Computed: send button icon type
12 + get sendButtonIcon() {
13 + const input = document.getElementById("chat-input");
14 + const hasInput = input?.value?.trim() || attachmentsStore?.attachments?.length > 0;
15 + const hasQueue = messageQueueStore?.hasQueue;
16 + const running = chatTopStore?.running;
17 +
18 + if (hasQueue && !hasInput) return "send-all";
19 + if (running && hasInput) return "queue";
20 + return "send";
21 + },
22 +
23 + // Computed: send button CSS class
24 + get sendButtonClass() {
25 + const icon = this.sendButtonIcon;
26 + if (icon === "send-all") return "send-queue";
27 + if (icon === "queue") return "send-queue";
28 + return "";
29 + },
30 +
31 + // Computed: send button title
32 + get sendButtonTitle() {
33 + const icon = this.sendButtonIcon;
34 + if (icon === "send-all") return "Send all queued messages";
35 + if (icon === "queue") return "Add to queue";
36 + return "Send message";
37 + },
38 +
39 init() {
40 console.log("Input store initialized");
41 // Event listeners are now handled via Alpine directives in the component
webui/components/chat/message-queue/message-queue-store.js
+25 -2
@@ -3,19 +3,37 @@ import * as api from "/js/api.js";
3
4 const model = {
5 items: [],
6 + pendingItems: [], // Local pending items (uploading to queue)
7
8 get hasQueue() {
8 - return this.items.length > 0;
9 + return this.items.length > 0 || this.pendingItems.length > 0;
10 },
11
12 get count() {
12 - return this.items.length;
13 + return this.items.length + this.pendingItems.length;
14 + },
15 +
16 + // Combined items for display: confirmed first, then pending at the end
17 + get allItems() {
18 + return [...this.items, ...this.pendingItems];
19 },
20
21 async addToQueue(text, attachments = []) {
22 const context = globalThis.getContext?.();
23 if (!context) return false;
24
25 + // Generate a temporary ID for pending item
26 + const tempId = `pending-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
27 + const pendingItem = {
28 + id: tempId,
29 + text: text || "(attachment only)",
30 + attachments: attachments.map(a => a.name || a.file?.name || "file"),
31 + pending: true,
32 + };
33 +
34 + // Add to pending immediately for UI feedback
35 + this.pendingItems = [...this.pendingItems, pendingItem];
36 +
37 try {
38 let filenames = [];
39 if (attachments.length > 0) {
@@ -30,9 +48,14 @@ const model = {
48 }
49 }
50 const response = await api.callJsonApi("/message_queue_add", { context, text, attachments: filenames });
51 +
52 + // Remove from pending (poll will add the confirmed item)
53 + this.pendingItems = this.pendingItems.filter(p => p.id !== tempId);
54 return response?.ok || false;
55 } catch (e) {
56 console.error("Failed to queue message:", e);
57 + // Remove from pending on error
58 + this.pendingItems = this.pendingItems.filter(p => p.id !== tempId);
59 return false;
60 }
61 },
webui/components/chat/message-queue/message-queue.html
+56 -26
@@ -10,18 +10,27 @@
10 <div class="queue-preview">
11 <div class="queue-header">
12 <span class="queue-title">
13 - <span class="queue-spinner"></span>
13 Queued Messages (<span x-text="$store.messageQueue.count"></span>)
14 </span>
16 - <button class="queue-clear-btn" @click="$store.messageQueue.clearQueue()" title="Clear all">
17 - <span class="material-symbols-outlined">delete_sweep</span>
18 - </button>
15 + <div class="queue-header-actions">
16 + <button class="queue-header-btn" @click="$store.messageQueue.sendAll()" title="Send all">
17 + <span class="material-symbols-outlined">keyboard_double_arrow_right</span>
18 + </button>
19 + <button class="queue-header-btn delete" @click="$store.messageQueue.clearQueue()" title="Clear all">
20 + <span class="material-symbols-outlined">delete_sweep</span>
21 + </button>
22 + </div>
23 </div>
24 <div class="queue-items">
21 - <template x-for="(item, index) in $store.messageQueue.items" :key="item.id">
22 - <div class="queue-item">
25 + <template x-for="(item, index) in $store.messageQueue.allItems" :key="item.id">
26 + <div class="queue-item" :class="{ 'queue-item-pending': item.pending }">
27 <div class="queue-item-content">
24 - <span class="queue-item-seq" x-text="index + 1"></span>
28 + <template x-if="item.pending">
29 + <span class="queue-item-pending-icon"></span>
30 + </template>
31 + <template x-if="!item.pending">
32 + <span class="queue-item-seq" x-text="index + 1"></span>
33 + </template>
34 <span class="queue-item-text" x-text="item.text || '(attachment only)'"></span>
35 <template x-if="item.attachments?.length > 0">
36 <div class="queue-attachments">
@@ -67,27 +76,16 @@
76 }
77
78 .queue-title {
70 - display: flex;
71 - align-items: center;
72 - gap: var(--spacing-xs);
79 font-size: var(--font-size-xs);
80 color: var(--color-text-secondary);
81 }
82
77 - .queue-spinner {
78 - width: 12px;
79 - height: 12px;
80 - border: 2px solid var(--color-border);
81 - border-top-color: var(--color-primary);
82 - border-radius: 50%;
83 - animation: queue-spin 1s linear infinite;
84 - }
85 -
86 - @keyframes queue-spin {
87 - to { transform: rotate(360deg); }
83 + .queue-header-actions {
84 + display: flex;
85 + gap: 2px;
86 }
87
90 - .queue-clear-btn {
88 + .queue-header-btn {
89 background: transparent;
90 border: none;
91 color: var(--color-text-secondary);
@@ -100,17 +98,17 @@
98 transition: all 0.1s ease-in-out;
99 }
100
103 - .queue-clear-btn:hover {
101 + .queue-header-btn:hover {
102 opacity: 1;
103 transform: scale(1.1);
104 }
105
108 - .queue-clear-btn:active {
106 + .queue-header-btn:active {
107 opacity: 0.5;
108 transform: scale(0.95);
109 }
110
113 - .queue-clear-btn .material-symbols-outlined {
111 + .queue-header-btn .material-symbols-outlined {
112 font-size: var(--font-size-large);
113 }
114
@@ -194,14 +192,25 @@
192 display: flex;
193 gap: 2px;
194 flex-shrink: 0;
195 + }
196 +
197 + .device-pointer .queue-item-actions {
198 opacity: 0;
199 visibility: hidden;
200 + pointer-events: none;
201 transition: opacity 0.15s;
202 }
203
202 - .queue-item:hover .queue-item-actions {
204 + .device-pointer .queue-item:hover .queue-item-actions {
205 opacity: 1;
206 visibility: visible;
207 + pointer-events: auto;
208 + }
209 +
210 + .device-touch .queue-item-actions {
211 + opacity: 1;
212 + visibility: visible;
213 + pointer-events: auto;
214 }
215
216 .queue-action-btn {
@@ -218,6 +227,27 @@
227 .queue-action-btn .material-symbols-outlined {
228 font-size: var(--font-size-smaller);
229 }
230 +
231 + .queue-item-pending {
232 + opacity: 0.6;
233 + }
234 +
235 + .queue-item-pending .queue-item-actions {
236 + display: none;
237 + }
238 +
239 + .queue-item-pending-icon {
240 + width: 1rem;
241 + height: 1rem;
242 + border: 2px solid var(--color-border);
243 + border-top-color: var(--color-primary);
244 + border-radius: 50%;
245 + animation: pending-spin 0.8s linear infinite;
246 + }
247 +
248 + @keyframes pending-spin {
249 + to { transform: rotate(360deg); }
250 + }
251 </style>
252 </body>
253 </html>
webui/components/chat/top-section/chat-top-store.js
+2 -1
@@ -3,7 +3,8 @@ import { createStore } from "/js/AlpineStore.js";
3 // define the model object holding data and functions
4 const model = {
5 connected: false,
6 - progressActive: false, // true when agent is working
6 + progressActive: false, // true when progress bar is active
7 + running: false, // true when agent is running (from context.is_running())
8 };
9
10 // convert it to alpine store
webui/index.js
+2 -2
@@ -54,7 +54,7 @@ export async function sendMessage() {
54
55 if (message || hasAttachments) {
56 // Check if agent is busy - queue instead of sending
57 - if (chatTopStore.progressActive) {
57 + if (chatTopStore.running) {
58 const success = await messageQueueStore.addToQueue(message, attachmentsWithUrls);
59 if (success) {
60 chatInputEl.value = "";
@@ -338,7 +338,7 @@ export async function poll() {
338 updateProgress(response.log_progress, response.log_progress_active);
339
340 // Update agent busy state for queue logic
341 - chatTopStore.progressActive = response.log_progress_active;
341 + chatTopStore.running = response.running;
342
343 // Update message queue from poll
344 messageQueueStore.updateFromPoll(response.message_queue);