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