feat(FN-4350): complete Step 3 — mirror reconnect logic in useQuickChat

Fusion-Task-Id: FN-4350
Fusion-Task-Lineage: 11fff2c2-1cd1-4562-9b01-032a15217479
This commit is contained in:
Fusion
2026-05-13 12:16:30 -07:00
committed by gsxdsm
parent 5a2af0d918
commit c3e87e5eec
2 changed files with 185 additions and 14 deletions

View File

@@ -269,7 +269,11 @@ export function useQuickChat(
[projectId],
);
const attachIfGenerating = useCallback((sessionId: string, inFlightGeneration?: ChatInFlightGenerationState | null) => {
const attachIfGenerating = useCallback((
sessionId: string,
inFlightGeneration?: ChatInFlightGenerationState | null,
options?: { silent?: boolean },
) => {
if (streamRef.current || !sessionId) {
return true;
}
@@ -289,7 +293,7 @@ export function useQuickChat(
setStreamingThinking,
setStreamingToolCalls,
cancelStreamingFlushesRef,
addToast,
addToast: options?.silent ? undefined : addToast,
onFallbackSession: (data, fallbackSessionId) => {
const nextModel = parseModelDescriptor(data.fallbackModel);
setSessions((prev) => prev.map((session) =>
@@ -316,7 +320,9 @@ export function useQuickChat(
isStreamingRef.current = false;
streamRef.current = null;
const errorMessage = typeof data === "string" && data.trim() ? data : "Failed to get response";
addToast?.(errorMessage, "error");
if (!options?.silent) {
addToast?.(errorMessage, "error");
}
void fetchChatMessages(sessionId, { limit: 50 }, projectId).then((resp) => {
setMessages(resp.messages.map(mapChatMessageToInfo));
}).catch(() => {});
@@ -613,6 +619,43 @@ export function useQuickChat(
const sendMessageRef = useRef<(content: string, attachments?: File[]) => Promise<void>>(() => Promise.resolve());
const visibilitySuspension = useTabVisibilitySuspension();
const reconnectSessionSilently = useCallback(async (sessionId: string) => {
try {
await refreshSessions();
const refreshed = await fetchChatSession(sessionId, projectId);
const refreshedSession = refreshed.session;
if (activeSessionRef.current?.id === sessionId) {
setActiveSession((prev) => {
if (!prev || prev.id !== sessionId) {
return prev;
}
return {
...prev,
...refreshedSession,
};
});
}
if (refreshedSession.isGenerating) {
setStreamingText("");
setStreamingThinking("");
setStreamingToolCalls([]);
setIsStreaming(true);
isStreamingRef.current = true;
attachIfGenerating(sessionId, refreshedSession.inFlightGeneration, { silent: true });
} else {
setStreamingText("");
setStreamingThinking("");
setStreamingToolCalls([]);
setIsStreaming(false);
isStreamingRef.current = false;
await reloadMessages();
}
} catch {
// Intentionally silent reconnect path.
}
}, [attachIfGenerating, projectId, refreshSessions, reloadMessages]);
/**
* Send a message using SSE streaming.
* @param content message text content
@@ -721,12 +764,18 @@ export function useQuickChat(
console.error("[useQuickChat] Stream error:", data);
const errorMessage = typeof data === "string" && data.trim() ? data : "Failed to get response";
const shouldSuppressSuspensionError = typeof data === "string"
&& isLikelyTabSuspensionError(data)
&& (visibilitySuspension.isHiddenNow() || visibilitySuspension.wasRecentlyHidden(5000));
const shouldSuppressSuspensionError = isLikelyTabSuspensionError(errorMessage);
if (shouldSuppressSuspensionError) {
console.info("[useQuickChat] Suppressed tab-suspension stream error:", data);
if (activeSession?.id) {
setStreamingText("");
setStreamingThinking("");
setStreamingToolCalls([]);
setIsStreaming(true);
isStreamingRef.current = true;
void reconnectSessionSilently(activeSession.id);
}
sendCompletionRef.current?.resolve();
} else {
addToast?.(errorMessage, "error");
@@ -743,7 +792,9 @@ export function useQuickChat(
}
}
void reloadMessages();
if (!shouldSuppressSuspensionError) {
void reloadMessages();
}
},
});
@@ -755,11 +806,54 @@ export function useQuickChat(
void completionPromise.catch(() => {});
return completionPromise;
},
[activeSession, projectId, addToast, reloadMessages, visibilitySuspension],
[activeSession, projectId, addToast, reloadMessages, reconnectSessionSilently],
);
sendMessageRef.current = sendMessage;
useEffect(() => {
const unsubscribe = visibilitySuspension.onBecameVisible(() => {
if (skipNextSessionInitRef.current) {
return;
}
const currentSession = activeSessionRef.current;
if (!currentSession || streamRef.current) {
return;
}
void fetchChatSession(currentSession.id, projectId)
.then((data) => {
if (streamRef.current || activeSessionRef.current?.id !== currentSession.id) {
return;
}
if (data.session.isGenerating) {
setStreamingText("");
setStreamingThinking("");
setStreamingToolCalls([]);
setIsStreaming(true);
isStreamingRef.current = true;
attachIfGenerating(currentSession.id, data.session.inFlightGeneration, { silent: true });
return;
}
if (isStreamingRef.current) {
setStreamingText("");
setStreamingThinking("");
setStreamingToolCalls([]);
setIsStreaming(false);
isStreamingRef.current = false;
void reloadMessages();
}
})
.catch(() => {
// Intentionally silent for visibility reconnect path.
});
});
return unsubscribe;
}, [attachIfGenerating, projectId, reloadMessages, visibilitySuspension]);
// Cleanup on unmount
useEffect(() => {
return () => {