From b44d8496bc8230d443373b39501dd385af761890 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Thu, 12 Mar 2026 18:08:45 -0400 Subject: [PATCH] fix: queue drain survives navigation away from a streaming conversation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously the queue drain guard (currentConversation.id === convId) meant that if the user navigated to ChatView while a workspace stream was running, the workspace's queued messages were silently abandoned — they sat in localStorage indefinitely with no code path to resume them. - Extract _tryDrainQueue(convId) with the same guard logic - Call it at stream-end (replaces the inline block) - Call it in fetchConversation after _loadQueue, so returning to a conversation with orphaned queue messages drains them automatically - Order is now preserved: messages drain in the order they were queued, even across navigation events Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/stores/chat.ts | 45 +++++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/frontend/src/stores/chat.ts b/frontend/src/stores/chat.ts index 7d2e83f..76c1f23 100644 --- a/frontend/src/stores/chat.ts +++ b/frontend/src/stores/chat.ts @@ -109,6 +109,28 @@ export const useChatStore = defineStore("chat", () => { } } + // Drain the next queued message for a conversation, if conditions are met. + // Called both at stream-end and after fetchConversation, so orphaned queue + // messages (e.g. from a navigation away mid-stream) are picked up on return. + function _tryDrainQueue(convId: number) { + const queue = convQueues.value[convId]; + if (!queue?.length) return; + if (isStreamingConv(convId)) return; // stream-end will drain naturally + if (currentConversation.value?.id !== convId) return; // not our conversation + const next = queue.shift()!; + _saveQueue(convId); + setTimeout(() => sendMessage( + next.content, + next.contextNoteId, + next.includeNoteIds, + next.think, + next.contextNoteTitle, + next.excludeNoteIds, + next.ragProjectId, + next.workspaceProjectId, + ), 0); + } + function clearQueue() { const id = currentConversation.value?.id; if (id) { @@ -158,6 +180,9 @@ export const useChatStore = defineStore("chat", () => { `/api/chat/conversations/${id}` ); _loadQueue(id); + // Drain any messages that were queued but never sent because the user + // navigated away before the previous stream finished. + _tryDrainQueue(id); } catch (e) { useToastStore().show("Failed to load conversation", "error"); throw e; @@ -431,23 +456,9 @@ export const useChatStore = defineStore("chat", () => { s.pendingTool = null; } - // Process next queued message, if any. - // Use setTimeout so this frame resolves before the next send begins. - const queue = convQueues.value[convId]; - if (queue?.length && currentConversation.value?.id === convId) { - const next = queue.shift()!; - _saveQueue(convId); - setTimeout(() => sendMessage( - next.content, - next.contextNoteId, - next.includeNoteIds, - next.think, - next.contextNoteTitle, - next.excludeNoteIds, - next.ragProjectId, - next.workspaceProjectId, - ), 0); - } + // Process next queued message if this is still the active conversation. + // If the user has navigated away, _tryDrainQueue will fire on fetchConversation. + _tryDrainQueue(convId); } async function reconnectIfGenerating(convId: number): Promise {