From 5066f90db2d61f8a9474e49d5f2f9d749516542d Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sat, 27 Jun 2026 15:56:42 -0700 Subject: [PATCH] FN-7137: stack queued chat messages Stack queued chat sends above the composer and flush them in order. - Persist pending chat sends as per-session FIFO arrays with legacy single-message restore fallback. - Render stacked queued previews above the direct and quick chat composer with per-item dismissal. - Flush one queued message after each completed or safely verified idle generation, preserving reconnect guards. - Extend chat hook and composer tests for multi-message queuing, restore, dismissal, and FIFO sends. - Document the queued stack behavior and add a published package changeset. Files changed: .changeset/fn-7137-stack-queued-chat-messages.md | 7 + docs/dashboard-guide.md | 4 +- packages/dashboard/app/components/ChatView.css | 15 +- packages/dashboard/app/components/ChatView.tsx | 38 ++-- .../__tests__/ChatView.autosize.test.tsx | 2 +- .../__tests__/ChatView.cli-mount.test.tsx | 2 +- .../__tests__/ChatView.core-interactions.test.tsx | 27 ++- .../__tests__/ChatView.default-model-icon.test.tsx | 2 +- .../components/__tests__/ChatView.draft.test.tsx | 2 +- .../__tests__/ChatView.hash-mention.test.tsx | 2 +- .../__tests__/ChatView.mobile-render.test.tsx | 2 +- .../components/__tests__/ChatView.rooms.test.tsx | 2 +- .../__tests__/ChatView.scroll-to-top.test.tsx | 2 +- .../__tests__/ChatView.swipe-back.test.tsx | 2 +- .../components/__tests__/ChatView.test-harness.tsx | 2 +- .../dashboard/app/hooks/__tests__/useChat.test.ts | 247 +++++++++++++++++---- .../app/hooks/chatPendingMessageStorage.ts | 38 +++- packages/dashboard/app/hooks/useChat.ts | 125 ++++++++--- 18 files changed, 390 insertions(+), 131 deletions(-) Fusion-Task-Id: FN-7137 Fusion-Task-Lineage: 65dd5ed7-a8db-447b-9a50-ce73ea7b597f Co-authored-by: Fusion (runfusion.ai) --- .../fn-7137-stack-queued-chat-messages.md | 7 + docs/dashboard-guide.md | 4 +- .../dashboard/app/components/ChatView.css | 15 +- .../dashboard/app/components/ChatView.tsx | 38 +-- .../__tests__/ChatView.autosize.test.tsx | 2 +- .../__tests__/ChatView.cli-mount.test.tsx | 2 +- .../ChatView.core-interactions.test.tsx | 27 +- .../ChatView.default-model-icon.test.tsx | 2 +- .../__tests__/ChatView.draft.test.tsx | 2 +- .../__tests__/ChatView.hash-mention.test.tsx | 2 +- .../__tests__/ChatView.mobile-render.test.tsx | 2 +- .../__tests__/ChatView.rooms.test.tsx | 2 +- .../__tests__/ChatView.scroll-to-top.test.tsx | 2 +- .../__tests__/ChatView.swipe-back.test.tsx | 2 +- .../__tests__/ChatView.test-harness.tsx | 2 +- .../app/hooks/__tests__/useChat.test.ts | 257 ++++++++++++++---- .../app/hooks/chatPendingMessageStorage.ts | 38 ++- packages/dashboard/app/hooks/useChat.ts | 131 ++++++--- 18 files changed, 398 insertions(+), 139 deletions(-) create mode 100644 .changeset/fn-7137-stack-queued-chat-messages.md diff --git a/.changeset/fn-7137-stack-queued-chat-messages.md b/.changeset/fn-7137-stack-queued-chat-messages.md new file mode 100644 index 0000000000..a18e8c3bb9 --- /dev/null +++ b/.changeset/fn-7137-stack-queued-chat-messages.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Stack multiple queued chat messages above the composer and send them in order. +category: feature +dev: Direct and Quick Chat queued sends now persist as FIFO arrays with legacy single-string restore fallback. diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index f8d00cd822..09c2fff052 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -347,7 +347,7 @@ Chat view provides project-scoped conversations with agents. - Full Chat and Quick Chat both consume the same streamed `/api/chat/sessions/:id/messages` response contract, and both now prefer the authoritative assistant `message` snapshot on `done` while still accumulating `text` chunks when present (so providers without incremental text streaming still render output immediately) - In-progress assistant responses now survive refresh/navigation while generation is still active: Chat restores the last durable in-flight text/thinking/tool state immediately, keeps the prior persisted conversation visible, then resumes streaming from the stored replay point; any new text, thinking, or tool-call updates append to that restored bubble instead of replacing it or starting from an empty "Working…" placeholder. - If a regular Chat stream drops with a hidden-tab/browser-suspension error (for example `Load failed`) while the server is still generating, Chat suppresses the false error banner, re-attaches to the in-progress stream using the durable replay state, and reconciles the final assistant reply when generation completes. -- If you queue a follow-up user message while the assistant is still streaming, Chat persists that queued text per session, shows the queued preview above the input box with a divider, and restores/sends it once the active response finishes if you leave and return. +- If you queue follow-up user messages while the assistant is still streaming, Chat persists them per session, stacks each queued preview above the input box with one shared divider, and restores/sends them one at a time in FIFO order once each active response finishes if you leave and return. - Chat message lists now track near-bottom scroll state: while you are reading older messages, live streaming/new replies do not force-scroll; a **Latest** jump control appears until you return to the tail. - On mobile direct-chat threads, entering a thread and restoring Chat after tab/page visibility returns re-anchors to the newest message (`scrollTop = scrollHeight`) so the view always opens at the live tail. - On mobile direct-chat threads, tapping the active title/identity in the thread header opens a lightweight conversation dropdown so you can switch to another direct session or start a New Chat without backing out to the sidebar list first; long conversation titles now stay readable in the dropdown via wrapped option text and taller touch-friendly rows. @@ -409,7 +409,7 @@ Quick Chat is an optional fast, project-scoped assistant surface for conversatio - Submitting the inline chooser uses explicit fresh-session creation and immediately persists/selects the new thread, then refreshes the session dropdown list - On first open for a project, Quick Chat restores the last opened non-archived session from per-project local storage; if that saved session is missing, it falls back to the most recently touched non-archived session by latest activity (`max(lastMessageAt, updatedAt)`), and only falls back to the first agent / configured default model when no prior session exists. - Closing and reopening Quick Chat keeps the active conversation warm in memory, so messages stay visible without a conversation reload or "Loading conversation…" flash. -- Queued follow-up messages entered while a Quick Chat response is still streaming now persist per session, so closing/reopening the panel restores the queued text and flushes it once the active response completes. +- Queued follow-up messages entered while a Quick Chat response is still streaming now persist per session, so closing/reopening the panel restores the queued stack and flushes the messages one at a time in FIFO order as active responses complete. - Resume lookups still use targeted session queries instead of loading the full active-session list first - Tool-call summaries in the floating quick-chat panel are intentionally condensed into a single-line header row (especially on small screens) so tool name + status stay scannable without multi-line wrapping - Question tool calls use the same shared response card as full Chat, with compact spacing in the floating panel and read-only answered history so Quick Chat can continue agent clarification loops without exposing raw tool JSON. diff --git a/packages/dashboard/app/components/ChatView.css b/packages/dashboard/app/components/ChatView.css index 38dca12c68..44c9c761ee 100644 --- a/packages/dashboard/app/components/ChatView.css +++ b/packages/dashboard/app/components/ChatView.css @@ -1950,8 +1950,17 @@ Tablet Chat View has enough message-pane width for assistant prose, markdown, to /* === Chat Pending Message === */ /* FNXC:ChatComposer 2026-06-27-00:00: -The single queued-message banner spans the composer above the input row, and the divider below it makes the queued state visually distinct without rendering an empty rule when no pending message exists. +Queued-message banners stack above the composer input with a capped scroll area, and the shared divider below them makes the queued state visually distinct without rendering an empty rule when the FIFO queue is empty. */ +.chat-pending-stack { + display: flex; + flex-direction: column; + gap: var(--space-xs); + width: 100%; + max-height: calc(var(--space-xl) * 6); + overflow-y: auto; +} + .chat-pending-message { display: flex; align-items: center; @@ -2015,6 +2024,10 @@ The single queued-message banner spans the composer above the input row, and the thread takes the full viewport. The thread already renders a back button (ChevronLeft) on mobile to flip back to the session list. */ @media (max-width: 768px) { + .chat-pending-stack { + max-height: calc(var(--space-xl) * 5); + } + .chat-pending-message { align-items: flex-start; } diff --git a/packages/dashboard/app/components/ChatView.tsx b/packages/dashboard/app/components/ChatView.tsx index 55b66b0a88..35a861e028 100644 --- a/packages/dashboard/app/components/ChatView.tsx +++ b/packages/dashboard/app/components/ChatView.tsx @@ -1024,7 +1024,7 @@ export function ChatView({ projectId, addToast, floating = false, onPopOut, onMa deleteSession, sendMessage, stopStreaming, - pendingMessage, + pendingMessages, clearPendingMessage, loadMoreMessages, hasMoreMessages, @@ -2731,9 +2731,9 @@ export function ChatView({ projectId, addToast, floating = false, onPopOut, onMa // the render-mode toggle still appears in a slim toolbar. const hideAssistantIdentity = activeSession?.agentId === FN_AGENT_ID; - const pendingPreview = pendingMessage.length > 50 - ? `${pendingMessage.slice(0, 50)}…` - : pendingMessage; + const getPendingPreview = (message: string) => message.length > 50 + ? `${message.slice(0, 50)}…` + : message; const toggleAllAsPlain = useCallback(() => { setShowAllAsPlain((value) => !value); @@ -3057,23 +3057,27 @@ export function ChatView({ projectId, addToast, floating = false, onPopOut, onMa ))} )} - {pendingMessage && ( + {pendingMessages.length > 0 && ( <> {/* FNXC:ChatComposer 2026-06-27-00:00: - The single-slot queued chat message must appear above the input box, separated by a divider, so users can notice the pending send without changing the one-pending-message queue model. + Queued direct-chat messages stack above the input in FIFO order with one shared divider, so multiple sends remain visible without changing the above-composer placement established by FN-7121. */} -
- {t("chat.queuedMessage", "Queued: {{preview}}", { preview: pendingPreview })} - +
+ {pendingMessages.map((pendingMessage, index) => ( +
+ {t("chat.queuedMessage", "Queued: {{preview}}", { preview: getPendingPreview(pendingMessage) })} + +
+ ))}