FN-7121: show queued chat previews above composer
Show queued Chat messages above the composer with a clear divider and covered placement behavior. - Move the queued-message indicator out of the textarea wrapper so it spans above the input row. - Add divider styling and responsive pending-message alignment for the composer. - Extend ChatView coverage for placement, dismissal, and divider removal. - Document the queued-message preview behavior and add a patch changeset. Files changed: .changeset/fn-7121-queued-message-above-input.md | 7 ++++ docs/dashboard-guide.md | 2 +- packages/dashboard/app/components/ChatView.css | 19 +++++++++-- packages/dashboard/app/components/ChatView.tsx | 35 ++++++++++++-------- .../__tests__/ChatView.core-interactions.test.tsx | 37 +++++++++++++++++++--- 5 files changed, 77 insertions(+), 23 deletions(-) Fusion-Task-Id: FN-7121 Fusion-Task-Lineage: b5cb0577-ca4e-459b-9b9e-5c60efd7bdfd Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-7121-queued-message-above-input.md
Normal file
7
.changeset/fn-7121-queued-message-above-input.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Show queued Chat messages above the input box with a divider.
|
||||
category: fix
|
||||
dev: Moves the existing single pending-message indicator out of the textarea wrapper and covers placement with ChatView tests.
|
||||
@@ -346,7 +346,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 now persists that queued text per session so leaving and returning to the view still restores and sends it once the active response finishes.
|
||||
- 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.
|
||||
- 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 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.
|
||||
|
||||
@@ -1917,21 +1917,30 @@ 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.
|
||||
*/
|
||||
.chat-pending-message {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
margin-top: var(--space-xs);
|
||||
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: 12px;
|
||||
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;
|
||||
@@ -1946,7 +1955,7 @@ Tablet Chat View has enough message-pane width for assistant prose, markdown, to
|
||||
padding: var(--space-xs);
|
||||
line-height: 1;
|
||||
flex-shrink: 0;
|
||||
font-size: 14px;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.chat-pending-message-dismiss:hover {
|
||||
@@ -1975,6 +1984,10 @@ Tablet Chat View has enough message-pane width for assistant prose, markdown, to
|
||||
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-message {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.chat-view__body {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@@ -3057,6 +3057,27 @@ export function ChatView({ projectId, addToast, floating = false, onPopOut, onMa
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{pendingMessage && (
|
||||
<>
|
||||
{/*
|
||||
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.
|
||||
*/}
|
||||
<div className="chat-pending-message" data-testid="chat-pending-indicator">
|
||||
<span>{t("chat.queuedMessage", "Queued: {{preview}}", { preview: pendingPreview })}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="chat-pending-message-dismiss"
|
||||
aria-label={t("chat.dismissQueuedMessage", "Dismiss queued message")}
|
||||
data-testid="chat-pending-dismiss"
|
||||
onClick={clearPendingMessage}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<div className="chat-pending-divider" aria-hidden="true" />
|
||||
</>
|
||||
)}
|
||||
<div className="chat-input-row">
|
||||
<button
|
||||
type="button"
|
||||
@@ -3128,20 +3149,6 @@ export function ChatView({ projectId, addToast, floating = false, onPopOut, onMa
|
||||
}}
|
||||
loading={fileMention.loading}
|
||||
/>
|
||||
{pendingMessage && (
|
||||
<div className="chat-pending-message" data-testid="chat-pending-indicator">
|
||||
<span>{t("chat.queuedMessage", "Queued: {{preview}}", { preview: pendingPreview })}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="chat-pending-message-dismiss"
|
||||
aria-label={t("chat.dismissQueuedMessage", "Dismiss queued message")}
|
||||
data-testid="chat-pending-dismiss"
|
||||
onClick={clearPendingMessage}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{isStreaming ? (
|
||||
<button
|
||||
|
||||
@@ -732,21 +732,48 @@ describe("ChatView core interactions", () => {
|
||||
expect(screen.getByTestId("chat-send-btn")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders pending message indicator and dismisses it", async () => {
|
||||
it("renders pending message indicator above the input row and dismisses it", async () => {
|
||||
const clearPendingMessage = vi.fn();
|
||||
const activeSession = activeSessionFixture;
|
||||
setupMockChat({
|
||||
activeSession: { id: "session-001", agentId: "agent-001", status: "active", title: "Test Chat", createdAt: "2026-04-08T00:00:00.000Z", updatedAt: "2026-04-08T00:00:00.000Z" },
|
||||
activeSession,
|
||||
messages: [],
|
||||
pendingMessage: "Queued while streaming",
|
||||
clearPendingMessage,
|
||||
});
|
||||
|
||||
await renderWithAct(<ChatView projectId="proj-123" addToast={vi.fn()} />);
|
||||
const { rerender } = await renderWithAct(<ChatView projectId="proj-123" addToast={vi.fn()} />);
|
||||
|
||||
expect(screen.getByTestId("chat-pending-indicator")).toHaveTextContent("Queued: Queued while streaming");
|
||||
const indicators = screen.getAllByTestId("chat-pending-indicator");
|
||||
expect(indicators).toHaveLength(1);
|
||||
const indicator = indicators[0];
|
||||
expect(indicator).toHaveTextContent("Queued: Queued while streaming");
|
||||
|
||||
const input = screen.getByTestId("chat-input");
|
||||
const inputArea = input.closest(".chat-input-area");
|
||||
const inputRow = input.closest(".chat-input-row");
|
||||
const inputWrapper = input.closest(".chat-input-wrapper");
|
||||
expect(inputArea).not.toBeNull();
|
||||
expect(inputRow).not.toBeNull();
|
||||
expect(inputWrapper).not.toBeNull();
|
||||
expect(inputArea).toContainElement(indicator);
|
||||
expect(inputWrapper).not.toContainElement(indicator);
|
||||
expect(indicator.compareDocumentPosition(inputRow!)).toBe(Node.DOCUMENT_POSITION_FOLLOWING);
|
||||
expect(inputArea!.querySelector(".chat-pending-divider")).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(screen.getByTestId("chat-pending-dismiss"));
|
||||
expect(clearPendingMessage).toHaveBeenCalledTimes(1);
|
||||
|
||||
setupMockChat({
|
||||
activeSession,
|
||||
messages: [],
|
||||
pendingMessage: "",
|
||||
clearPendingMessage,
|
||||
});
|
||||
rerender(<ChatView projectId="proj-123" addToast={vi.fn()} />);
|
||||
|
||||
expect(screen.queryByTestId("chat-pending-indicator")).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId("chat-input").closest(".chat-input-area")!.querySelector(".chat-pending-divider")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("textarea is enabled during streaming", async () => {
|
||||
|
||||
Reference in New Issue
Block a user