Queue concurrent prompts instead of dropping or conflicting
Chat store: when sendMessage() is called while streaming, push to a per-conversation queue and return. When the stream ends the next queued message fires automatically via setTimeout(0) to keep the call stack clean. clearQueue() and queuedCount exposed for UI consumption. Queue is cleaned up on conversation delete. ChatView/WorkspaceView: remove the streaming guard from the local sendMessage() functions and the :disabled on the textarea so users can type and submit freely while streaming. Input placeholder changes to "Type to queue next message…" during streaming. A small "⏳ N queued ×" chip appears below the streaming bubble showing queue depth with a cancel button. DashboardChatInput: disable input, attach button, and send button during streaming. The dashboard creates a fresh conversation per message so in-conversation queuing doesn't apply — locking the input is the correct UX here. Placeholder updates to "Generating response…" during streaming. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -114,7 +114,8 @@ const streamingRendered = computed(() => {
|
||||
|
||||
const inputPlaceholder = computed(() => {
|
||||
if (!store.chatReady) return "Chat unavailable";
|
||||
return "Type a message... (Enter to send, Shift+Enter for new line)";
|
||||
if (store.streaming) return "Type to queue next message… (Enter to queue)";
|
||||
return "Type a message… (Enter to send, Shift+Enter for new line)";
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
@@ -283,7 +284,7 @@ async function removeConversation(id: number) {
|
||||
|
||||
async function sendMessage() {
|
||||
const content = messageInput.value.trim();
|
||||
if (!content || store.streaming) return;
|
||||
if (!content) return;
|
||||
|
||||
// Auto-create conversation if none selected
|
||||
if (!store.currentConversation) {
|
||||
@@ -631,6 +632,13 @@ onUnmounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Queued message indicator -->
|
||||
<div v-if="store.queuedCount" class="queued-indicator">
|
||||
<span class="queued-icon">⏳</span>
|
||||
{{ store.queuedCount }} message{{ store.queuedCount > 1 ? 's' : '' }} queued
|
||||
<button class="queued-cancel" @click="store.clearQueue()" aria-label="Cancel queued messages">×</button>
|
||||
</div>
|
||||
|
||||
<p
|
||||
v-if="!store.currentConversation.messages.length && !store.streaming"
|
||||
class="empty-msg"
|
||||
@@ -775,7 +783,7 @@ onUnmounted(() => {
|
||||
@keydown="onInputKeydown"
|
||||
@input="autoResize"
|
||||
:placeholder="inputPlaceholder"
|
||||
:disabled="store.streaming || !store.chatReady"
|
||||
:disabled="!store.chatReady"
|
||||
rows="1"
|
||||
></textarea>
|
||||
<button
|
||||
@@ -1601,4 +1609,30 @@ details[open] .thinking-summary::before {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.queued-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
font-size: 0.8rem;
|
||||
color: var(--color-text-muted);
|
||||
padding: 0.3rem 1rem;
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
.queued-icon {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
.queued-cancel {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 1rem;
|
||||
line-height: 1;
|
||||
padding: 0 0.2rem;
|
||||
margin-left: 0.1rem;
|
||||
}
|
||||
.queued-cancel:hover {
|
||||
color: var(--color-text);
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user