From 21b3baa62dd62c93a87de3f676c76a60f67f7808 Mon Sep 17 00:00:00 2001 From: Fusion Agent Date: Wed, 19 Aug 2026 06:27:16 +0000 Subject: [PATCH] FN-029: unify direct and planner chat queues Standardize queued-message controls across Direct Chat and planner conversations while preserving force-send ordering during cancellation reconciliation. - Add shared pending-message queue UI with edit, reorder, delete, and force-send actions. - Coordinate durable cancellation reconciliation before dispatching selected queued messages. - Disable the Direct composer and related controls until force-send reconciliation completes. - Add localized queue copy, documentation, styling, and regression coverage. Files changed: docs/dashboard-guide.md | 8 + packages/dashboard/app/components/ChatView.css | 63 ------ packages/dashboard/app/components/ChatView.tsx | 55 +++-- packages/dashboard/app/components/PendingChatMessageQueue.css | 94 +++++++++ packages/dashboard/app/components/PendingChatMessageQueue.tsx | 200 ++++++++++++++++++ packages/dashboard/app/components/TaskPlannerChatTab.css | 84 -------- packages/dashboard/app/components/TaskPlannerChatTab.tsx | 113 ++--------- packages/dashboard/app/components/__tests__/ChatView.core-interactions.test.tsx | 24 ++- packages/dashboard/app/components/__tests__/PendingChatMessageQueue.test.tsx | 76 +++++++ packages/dashboard/app/hooks/__tests__/useChat.test.ts | 39 +++ packages/dashboard/app/hooks/useChat.ts | 224 ++++++++++++++++----- packages/i18n/locales/en/app.json | 25 +-- packages/i18n/locales/es/app.json | 25 +-- packages/i18n/locales/fr/app.json | 25 +-- packages/i18n/locales/ko/app.json | 25 +-- packages/i18n/locales/pt-BR/app.json | 25 +-- packages/i18n/locales/zh-CN/app.json | 25 +-- packages/i18n/locales/zh-TW/app.json | 25 +-- packages/i18n/src/resources.d.ts | 25 +-- 19 files changed, 746 insertions(+), 434 deletions(-) Fusion-Task-Id: FN-029 Fusion-Task-Lineage: 3330d0e7-ba77-4392-98f7-69425070cfd1 Co-authored-by: Fusion --- docs/dashboard-guide.md | 8 + .../dashboard/app/components/ChatView.css | 63 ----- .../dashboard/app/components/ChatView.tsx | 55 ++--- .../components/PendingChatMessageQueue.css | 94 ++++++++ .../components/PendingChatMessageQueue.tsx | 200 ++++++++++++++++ .../app/components/TaskPlannerChatTab.css | 84 ------- .../app/components/TaskPlannerChatTab.tsx | 113 ++------- .../ChatView.core-interactions.test.tsx | 24 +- .../PendingChatMessageQueue.test.tsx | 76 ++++++ .../app/hooks/__tests__/useChat.test.ts | 39 +++ packages/dashboard/app/hooks/useChat.ts | 224 ++++++++++++++---- packages/i18n/locales/en/app.json | 25 +- packages/i18n/locales/es/app.json | 25 +- packages/i18n/locales/fr/app.json | 25 +- packages/i18n/locales/ko/app.json | 25 +- packages/i18n/locales/pt-BR/app.json | 25 +- packages/i18n/locales/zh-CN/app.json | 25 +- packages/i18n/locales/zh-TW/app.json | 25 +- packages/i18n/src/resources.d.ts | 25 +- 19 files changed, 746 insertions(+), 434 deletions(-) create mode 100644 packages/dashboard/app/components/PendingChatMessageQueue.css create mode 100644 packages/dashboard/app/components/PendingChatMessageQueue.tsx create mode 100644 packages/dashboard/app/components/__tests__/PendingChatMessageQueue.test.tsx diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index 74f8c20442..66bbccabf8 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -246,6 +246,14 @@ The server accepts the replacement only after it has discarded the target and ev Planner Chat refreshes task detail after an accepted replacement. Steering comments and refinement tasks created by discarded planner turns are durable side effects and are not rolled back; when applicable, Planner Chat shows the existing informational notice. The edit behavior is shared across desktop, popup/dock/host, mobile/touch, and task-detail surfaces. +## Pending messages in Direct and Planner Chat + +Direct Chat and task-detail Planner Chat share one browser-local, text-only pending queue per chat session. While a model reply is active, send additional text to add it to the queue; the queue survives reloads under the session-scoped `fusion:chat-pending:` storage key and does not cross projects, tasks, or sessions. + +Both model-loop surfaces expose the same queue controls: edit an entry, move it earlier or later, delete it, or **Force send** a selected entry. Duplicate text is selected by its position in the list, not by its content. A blank edit is rejected without deleting the queued entry, and queue controls remain named and touch-reachable on narrow screens. + +Normal completion and **Stop** release only the FIFO front after cancellation and authoritative history reconciliation. **Force send** first fences and cancels the active stream, waits for cancellation plus reconciliation, then dispatches only the selected entry; failures keep the entry in its original queue position. Attachments are never queued. Activity task chat, Chat Rooms, and CLI-backed chat intentionally keep their separate interaction and transport contracts and do not inherit these model-loop queue controls. + ## Automations diff --git a/packages/dashboard/app/components/ChatView.css b/packages/dashboard/app/components/ChatView.css index 62733374e2..dadde626f0 100644 --- a/packages/dashboard/app/components/ChatView.css +++ b/packages/dashboard/app/components/ChatView.css @@ -2377,61 +2377,6 @@ Thinking-section text uses the defined muted text token across all themes. The m box-shadow: var(--focus-ring-strong); } -/* === Chat Pending Message === */ -/* -FNXC:ChatComposer 2026-06-27-00:00: -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; - gap: var(--space-sm); - width: 100%; - padding: var(--space-xs) var(--space-sm); - border-radius: var(--radius-sm); - background: color-mix(in srgb, var(--todo) 10%, transparent); - color: var(--text-muted); - font-size: 0.75rem; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; -} - -.chat-pending-divider { - width: 100%; - border-top: 1px solid var(--border); -} - -.chat-pending-message span { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.chat-pending-message-dismiss { - background: none; - border: none; - color: var(--text-dim); - cursor: pointer; - padding: var(--space-xs); - line-height: 1; - flex-shrink: 0; - font-size: 0.875rem; -} - -.chat-pending-message-dismiss:hover { - color: var(--text-muted); -} - /* Sidebar footer contains the primary chat-scope action on desktop and mobile. */ .chat-sidebar-footer { display: block; @@ -2454,14 +2399,6 @@ Queued-message banners stack above the composer input with a capped scroll area, 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; - } - .chat-view__body { flex-direction: column; } diff --git a/packages/dashboard/app/components/ChatView.tsx b/packages/dashboard/app/components/ChatView.tsx index 5b6aadbb45..3c4105f5b5 100644 --- a/packages/dashboard/app/components/ChatView.tsx +++ b/packages/dashboard/app/components/ChatView.tsx @@ -34,6 +34,7 @@ import { type Agent, type ChatTag, type Settings } from "@fusion/core"; import { CustomModelDropdown } from "./CustomModelDropdown"; import { MicButton } from "./MicButton"; import { ChatThinkingLevelControl } from "./ChatThinkingLevelControl"; +import { PendingChatMessageQueue } from "./PendingChatMessageQueue"; import { AgentMentionPopup } from "./AgentMentionPopup"; import { AgentAvatar } from "./AgentAvatar"; import { ProviderIcon } from "./ProviderIcon"; @@ -652,7 +653,11 @@ export function ChatView({ projectId, addToast, floating = false, compactLayout editMessageAndResend, stopStreaming, pendingMessages, + pendingQueueAction, clearPendingMessage, + updatePendingMessage, + movePendingMessage, + forceSendPendingMessage, loadMoreMessages, hasMoreMessages, searchQuery, @@ -2770,10 +2775,6 @@ export function ChatView({ projectId, addToast, floating = false, compactLayout // assistant bubble is noise. Hide the per-message identity row entirely. const hideAssistantIdentity = activeSession?.agentId === FN_AGENT_ID; - const getPendingPreview = (message: string) => message.length > 50 - ? `${message.slice(0, 50)}…` - : message; - useEffect(() => { if (!mobileSessionMenuOpen) { return; @@ -3086,31 +3087,15 @@ export function ChatView({ projectId, addToast, floating = false, compactLayout ))} )} - {pendingMessages.length > 0 && ( - <> - {/* - FNXC:ChatComposer 2026-06-27-00:00: - 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. - */} -
- {pendingMessages.map((pendingMessage, index) => ( -
- {t("chat.queuedMessage", "Queued: {{preview}}", { preview: getPendingPreview(pendingMessage) })} - -
- ))} -
-