From c95517af312f3d921a327d1cebc48b55af3519c9 Mon Sep 17 00:00:00 2001 From: Fusion Date: Wed, 6 May 2026 19:26:14 -0700 Subject: [PATCH] feat(FN-3608): update dashboard chat documentation Updated the dashboard chat documentation in the user guide with a minor addition. Fusion-Task-Id: FN-3608 --- .changeset/chat-stream-stuck-state.md | 10 ++++++++++ packages/dashboard/app/api/legacy.ts | 11 +++++++++++ packages/dashboard/app/hooks/useChat.ts | 1 + packages/dashboard/app/hooks/useQuickChat.ts | 1 + 4 files changed, 23 insertions(+) create mode 100644 .changeset/chat-stream-stuck-state.md diff --git a/.changeset/chat-stream-stuck-state.md b/.changeset/chat-stream-stuck-state.md new file mode 100644 index 000000000..2482b277c --- /dev/null +++ b/.changeset/chat-stream-stuck-state.md @@ -0,0 +1,10 @@ +--- +"@runfusion/fusion": patch +--- + +Fix chat sending silently failing on flaky networks (especially mobile). +The SSE reader in the dashboard client now treats a closed stream without +a terminal `done`/`error` event as an error so streaming state unwinds +instead of getting stuck. The `useChat` and `useQuickChat` hooks also now +show a toast when a message is queued behind an in-flight response, so +the previous stuck state is observable rather than silent. diff --git a/packages/dashboard/app/api/legacy.ts b/packages/dashboard/app/api/legacy.ts index 6411ca500..c3ee2e826 100644 --- a/packages/dashboard/app/api/legacy.ts +++ b/packages/dashboard/app/api/legacy.ts @@ -8058,6 +8058,7 @@ export function streamChatResponse( const abortController = new AbortController(); let closedByUser = false; + let terminated = false; const dispatchEvent = (eventName: string, rawData: string): void => { if (!eventName) { @@ -8101,6 +8102,7 @@ export function streamChatResponse( } break; case "done": + terminated = true; try { const parsed = JSON.parse(rawData) as { messageId?: unknown; message?: unknown }; handlers.onDone?.({ @@ -8112,6 +8114,7 @@ export function streamChatResponse( } break; case "error": + terminated = true; try { const parsed = JSON.parse(rawData); handlers.onError?.(parsed.message || parsed); @@ -8216,6 +8219,14 @@ export function streamChatResponse( processLines(decoder.decode(value, { stream: true })); } + + // Server closed the stream without emitting a terminal `done` or `error` + // SSE event (common on flaky mobile networks, proxy idle-kill, or + // backgrounded tabs). Surface as an error so the client unwinds + // streaming state instead of getting stuck with isStreaming=true. + if (!terminated && !closedByUser) { + handlers.onError?.("Connection closed unexpectedly"); + } } catch (err: unknown) { if (err instanceof DOMException && err.name === "AbortError") { if (!closedByUser) { diff --git a/packages/dashboard/app/hooks/useChat.ts b/packages/dashboard/app/hooks/useChat.ts index 07a9e185d..fa8f6dc04 100644 --- a/packages/dashboard/app/hooks/useChat.ts +++ b/packages/dashboard/app/hooks/useChat.ts @@ -492,6 +492,7 @@ export function useChat( if (isStreaming) { pendingMessageRef.current = content; setPendingMessage(content); + addToast?.("Still waiting for previous response — message queued", "warning"); return; } diff --git a/packages/dashboard/app/hooks/useQuickChat.ts b/packages/dashboard/app/hooks/useQuickChat.ts index 372702592..7fba14039 100644 --- a/packages/dashboard/app/hooks/useQuickChat.ts +++ b/packages/dashboard/app/hooks/useQuickChat.ts @@ -557,6 +557,7 @@ export function useQuickChat( pendingMessageRef.current = content; setPendingMessage(content); + addToast?.("Still waiting for previous response — message queued", "warning"); return Promise.resolve(); }