FN-5852: persist queued chat messages across navigation
Keep queued follow-up chat drafts across session switches, reloads, and view re-entry. - persist queued pending-message text per chat session in shared localStorage helpers - restore and flush queued follow-up messages in full Chat when returning to an active session - restore and flush queued follow-up messages in Quick Chat and cover the recovery path with tests - document queued-message persistence behavior in the dashboard guide Files changed: docs/dashboard-guide.md | 2 + .../dashboard/app/hooks/__tests__/useChat.test.ts | 119 +++++++++++++++++- .../app/hooks/__tests__/useQuickChat.test.ts | 135 +++++++++++++++++++++ .../app/hooks/chatPendingMessageStorage.ts | 48 ++++++++ packages/dashboard/app/hooks/useChat.ts | 38 ++++++ packages/dashboard/app/hooks/useQuickChat.ts | 35 ++++++ 6 files changed, 376 insertions(+), 1 deletion(-) Fusion-Task-Id: FN-5852 Fusion-Task-Lineage: 1cddfb19-7e02-40e5-b373-5d593dfcfe2a
This commit is contained in:
@@ -7,6 +7,7 @@ import { act, fireEvent, renderHook, waitFor } from "@testing-library/react";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { useChat } from "../useChat";
|
||||
import * as apiModule from "../../api";
|
||||
import { getChatPendingMessageKey } from "../chatPendingMessageStorage";
|
||||
import * as swrCacheModule from "../../utils/swrCache";
|
||||
import type { ChatSession, ChatMessage } from "@fusion/core";
|
||||
|
||||
@@ -1625,6 +1626,95 @@ describe("useChat", () => {
|
||||
expect(addToast).not.toHaveBeenCalledWith("Still waiting for previous response — message queued", "warning");
|
||||
});
|
||||
|
||||
it("persists queued message text to localStorage while streaming", async () => {
|
||||
const session = makeSession({ id: "session-001", agentId: "agent-001" });
|
||||
mockFetchChatSessions.mockResolvedValueOnce({ sessions: [session] });
|
||||
mockFetchChatMessages.mockResolvedValueOnce({ messages: [] });
|
||||
mockStreamChatResponse.mockReturnValue({ close: vi.fn(), isConnected: () => true });
|
||||
|
||||
const { result } = renderHook(() => useChat("proj-123"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.sessions).toHaveLength(1);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.selectSession("session-001");
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.activeSession?.id).toBe("session-001");
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.sendMessage("First");
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isStreaming).toBe(true);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.sendMessage("Queued follow-up");
|
||||
});
|
||||
|
||||
expect(localStorage.getItem(getChatPendingMessageKey("session-001"))).toBe("Queued follow-up");
|
||||
});
|
||||
|
||||
it("rehydrates queued message from localStorage after remount", async () => {
|
||||
const session = {
|
||||
...makeSession({ id: "session-001", agentId: "agent-001" }),
|
||||
isGenerating: true,
|
||||
inFlightGeneration: {
|
||||
streamingText: "partial",
|
||||
streamingThinking: "",
|
||||
toolCalls: [],
|
||||
},
|
||||
};
|
||||
mockFetchChatSessions.mockResolvedValue({ sessions: [session] });
|
||||
mockFetchChatMessages.mockResolvedValue({ messages: [] });
|
||||
mockAttachChatStream.mockReturnValue({ close: vi.fn(), isConnected: () => true });
|
||||
mockStreamChatResponse.mockReturnValue({ close: vi.fn(), isConnected: () => true });
|
||||
|
||||
const firstHook = renderHook(() => useChat("proj-123"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(firstHook.result.current.sessions).toHaveLength(1);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
firstHook.result.current.selectSession("session-001");
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(firstHook.result.current.isStreaming).toBe(true);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
firstHook.result.current.sendMessage("Queued follow-up");
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(localStorage.getItem(getChatPendingMessageKey("session-001"))).toBe("Queued follow-up");
|
||||
});
|
||||
|
||||
firstHook.unmount();
|
||||
|
||||
const secondHook = renderHook(() => useChat("proj-123"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(secondHook.result.current.sessions).toHaveLength(1);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
secondHook.result.current.selectSession("session-001");
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(secondHook.result.current.pendingMessage).toBe("Queued follow-up");
|
||||
});
|
||||
});
|
||||
|
||||
describe("queued message closure behavior", () => {
|
||||
it("queued message auto-sends after onDone with the active session and completes second stream", async () => {
|
||||
const session = makeSession({ id: "session-001", agentId: "agent-001" });
|
||||
@@ -1985,7 +2075,7 @@ describe("useChat", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("clearPendingMessage clears pending message", async () => {
|
||||
it("clearPendingMessage clears pending message and removes persisted queue entry", async () => {
|
||||
const session = makeSession({ id: "session-001", agentId: "agent-001" });
|
||||
mockFetchChatSessions.mockResolvedValueOnce({ sessions: [session] });
|
||||
mockFetchChatMessages.mockResolvedValueOnce({ messages: [] });
|
||||
@@ -2027,6 +2117,31 @@ describe("useChat", () => {
|
||||
});
|
||||
|
||||
expect(result.current.pendingMessage).toBe("");
|
||||
expect(localStorage.getItem(getChatPendingMessageKey("session-001"))).toBeNull();
|
||||
});
|
||||
|
||||
it("restored queued message auto-sends once after generation already completed", async () => {
|
||||
const session = makeSession({ id: "session-001", agentId: "agent-001" });
|
||||
mockFetchChatSessions.mockResolvedValueOnce({ sessions: [session] });
|
||||
mockFetchChatMessages.mockResolvedValue({ messages: [] });
|
||||
localStorage.setItem(getChatPendingMessageKey("session-001")!, "Queued follow-up");
|
||||
|
||||
const { result } = renderHook(() => useChat("proj-123"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.sessions).toHaveLength(1);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.selectSession("session-001");
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockStreamChatResponse).toHaveBeenCalledTimes(1);
|
||||
expect(mockStreamChatResponse.mock.calls[0]?.[1]).toBe("Queued follow-up");
|
||||
});
|
||||
|
||||
expect(localStorage.getItem(getChatPendingMessageKey("session-001"))).toBeNull();
|
||||
});
|
||||
|
||||
it("stopStreaming flushes pendingMessage", async () => {
|
||||
@@ -2068,6 +2183,8 @@ describe("useChat", () => {
|
||||
expect(mockStreamChatResponse.mock.calls[1]?.[1]).toBe("Queued follow-up");
|
||||
expect(result.current.pendingMessage).toBe("");
|
||||
});
|
||||
|
||||
expect(localStorage.getItem(getChatPendingMessageKey("session-001"))).toBeNull();
|
||||
});
|
||||
|
||||
it("loads more messages with pagination", async () => {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { act, fireEvent, renderHook, waitFor } from "@testing-library/react";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { ChatSession } from "@fusion/core";
|
||||
import * as apiModule from "../../api";
|
||||
import { getChatPendingMessageKey } from "../chatPendingMessageStorage";
|
||||
import { FN_AGENT_ID, useQuickChat } from "../useQuickChat";
|
||||
|
||||
vi.mock("../../api", () => ({
|
||||
@@ -49,6 +50,7 @@ const setDocumentVisibilityState = (state: DocumentVisibilityState) => {
|
||||
describe("useQuickChat", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
localStorage.clear();
|
||||
mockFetchResumeChatSession.mockResolvedValue({ session: null });
|
||||
mockFetchChatSessions.mockResolvedValue({ sessions: [] });
|
||||
mockCreateChatSession.mockResolvedValue({
|
||||
@@ -616,6 +618,8 @@ describe("useQuickChat", () => {
|
||||
expect(result.current.isStreaming).toBe(false);
|
||||
expect(result.current.activeSession?.id).toBe("session-fresh");
|
||||
});
|
||||
|
||||
expect(localStorage.getItem(getChatPendingMessageKey("session-existing"))).toBeNull();
|
||||
});
|
||||
|
||||
it("stopStreaming aborts stream and resets streaming state", async () => {
|
||||
@@ -687,6 +691,28 @@ describe("useQuickChat", () => {
|
||||
expect(mockStreamChatResponse.mock.calls[1]?.[1]).toBe("Queued follow-up");
|
||||
expect(result.current.pendingMessage).toBe("");
|
||||
});
|
||||
|
||||
expect(localStorage.getItem(getChatPendingMessageKey("session-existing"))).toBeNull();
|
||||
});
|
||||
|
||||
it("restored quick-chat queued message auto-sends once after generation already completed", async () => {
|
||||
const existingSession = makeSession({ id: "session-existing", agentId: "agent-001" });
|
||||
mockFetchResumeChatSession.mockResolvedValueOnce({ session: existingSession });
|
||||
mockFetchChatMessages.mockResolvedValue({ messages: [] });
|
||||
localStorage.setItem(getChatPendingMessageKey("session-existing")!, "Queued follow-up");
|
||||
|
||||
const { result } = renderHook(() => useQuickChat("proj-123"));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.switchSession("agent-001");
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockStreamChatResponse).toHaveBeenCalledTimes(1);
|
||||
expect(mockStreamChatResponse.mock.calls[0]?.[1]).toBe("Queued follow-up");
|
||||
});
|
||||
|
||||
expect(localStorage.getItem(getChatPendingMessageKey("session-existing"))).toBeNull();
|
||||
});
|
||||
|
||||
it("stopStreaming with no pendingMessage cancels stream without sending anything", async () => {
|
||||
@@ -715,6 +741,43 @@ describe("useQuickChat", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("clearPendingMessage removes persisted quick-chat queue entry", async () => {
|
||||
const existingSession = makeSession({ id: "session-existing", agentId: "agent-001" });
|
||||
|
||||
mockFetchResumeChatSession.mockResolvedValueOnce({ session: existingSession });
|
||||
mockFetchChatMessages.mockResolvedValue({ messages: [] });
|
||||
mockStreamChatResponse.mockReturnValue({ close: vi.fn(), isConnected: () => true });
|
||||
|
||||
const { result } = renderHook(() => useQuickChat("proj-123"));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.switchSession("agent-001");
|
||||
});
|
||||
|
||||
act(() => {
|
||||
void result.current.sendMessage("Hello");
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isStreaming).toBe(true);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
void result.current.sendMessage("Queued follow-up");
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.pendingMessage).toBe("Queued follow-up");
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.clearPendingMessage();
|
||||
});
|
||||
|
||||
expect(result.current.pendingMessage).toBe("");
|
||||
expect(localStorage.getItem(getChatPendingMessageKey("session-existing"))).toBeNull();
|
||||
});
|
||||
|
||||
it("sending during streaming queues message without warning toast", async () => {
|
||||
const existingSession = makeSession({ id: "session-existing", agentId: "agent-001" });
|
||||
const addToast = vi.fn();
|
||||
@@ -795,6 +858,78 @@ describe("useQuickChat", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("persists queued quick-chat message text to localStorage while streaming", async () => {
|
||||
const existingSession = makeSession({ id: "session-existing", agentId: "agent-001" });
|
||||
mockFetchResumeChatSession.mockResolvedValueOnce({ session: existingSession });
|
||||
mockFetchChatMessages.mockResolvedValue({ messages: [] });
|
||||
mockStreamChatResponse.mockReturnValue({ close: vi.fn(), isConnected: () => true });
|
||||
|
||||
const { result } = renderHook(() => useQuickChat("proj-123"));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.switchSession("agent-001");
|
||||
});
|
||||
|
||||
act(() => {
|
||||
void result.current.sendMessage("Hello");
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isStreaming).toBe(true);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
void result.current.sendMessage("Queued follow-up");
|
||||
});
|
||||
|
||||
expect(localStorage.getItem(getChatPendingMessageKey("session-existing"))).toBe("Queued follow-up");
|
||||
});
|
||||
|
||||
it("rehydrates queued quick-chat message from localStorage after remount", async () => {
|
||||
const existingSession = {
|
||||
...makeSession({ id: "session-existing", agentId: "agent-001" }),
|
||||
isGenerating: true,
|
||||
inFlightGeneration: {
|
||||
streamingText: "partial",
|
||||
streamingThinking: "",
|
||||
toolCalls: [],
|
||||
},
|
||||
};
|
||||
mockFetchResumeChatSession.mockResolvedValue({ session: existingSession });
|
||||
mockFetchChatMessages.mockResolvedValue({ messages: [] });
|
||||
mockAttachChatStream.mockReturnValue({ close: vi.fn(), isConnected: () => true });
|
||||
|
||||
const firstHook = renderHook(() => useQuickChat("proj-123"));
|
||||
|
||||
await act(async () => {
|
||||
await firstHook.result.current.switchSession("agent-001");
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(firstHook.result.current.isStreaming).toBe(true);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
void firstHook.result.current.sendMessage("Queued follow-up");
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(localStorage.getItem(getChatPendingMessageKey("session-existing"))).toBe("Queued follow-up");
|
||||
});
|
||||
|
||||
firstHook.unmount();
|
||||
|
||||
const secondHook = renderHook(() => useQuickChat("proj-123"));
|
||||
|
||||
await act(async () => {
|
||||
await secondHook.result.current.switchSession("agent-001");
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(secondHook.result.current.pendingMessage).toBe("Queued follow-up");
|
||||
});
|
||||
});
|
||||
|
||||
describe("message queue behavior", () => {
|
||||
it("queued message is auto-sent after streaming onDone", async () => {
|
||||
const existingSession = makeSession({ id: "session-existing", agentId: "agent-001" });
|
||||
|
||||
48
packages/dashboard/app/hooks/chatPendingMessageStorage.ts
Normal file
48
packages/dashboard/app/hooks/chatPendingMessageStorage.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
const CHAT_PENDING_MESSAGE_STORAGE_PREFIX = "fusion:chat-pending:";
|
||||
|
||||
export function getChatPendingMessageKey(sessionId: string | null | undefined): string | null {
|
||||
if (!sessionId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return `${CHAT_PENDING_MESSAGE_STORAGE_PREFIX}${sessionId}`;
|
||||
}
|
||||
|
||||
export function getPersistedPendingChatMessage(sessionId: string | null | undefined): string {
|
||||
const key = getChatPendingMessageKey(sessionId);
|
||||
if (!key || typeof window === "undefined") {
|
||||
return "";
|
||||
}
|
||||
|
||||
try {
|
||||
return localStorage.getItem(key) ?? "";
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
export function setPersistedPendingChatMessage(sessionId: string | null | undefined, content: string): void {
|
||||
const key = getChatPendingMessageKey(sessionId);
|
||||
if (!key || typeof window === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
localStorage.setItem(key, content);
|
||||
} catch {
|
||||
// Ignore localStorage failures so chat queuing still works in-memory.
|
||||
}
|
||||
}
|
||||
|
||||
export function removePersistedPendingChatMessage(sessionId: string | null | undefined): void {
|
||||
const key = getChatPendingMessageKey(sessionId);
|
||||
if (!key || typeof window === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
localStorage.removeItem(key);
|
||||
} catch {
|
||||
// Ignore localStorage failures so cleanup paths do not throw.
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,11 @@ export interface ChatSessionInfo {
|
||||
export type { ChatMessageInfo, FailureInfo, FallbackInfo, ToolCallInfo } from "./chatTypes";
|
||||
import type { ChatMessageInfo, FailureInfo, FallbackInfo, ToolCallInfo } from "./chatTypes";
|
||||
import { createChatStreamHandlers } from "./createChatStreamHandlers";
|
||||
import {
|
||||
getPersistedPendingChatMessage,
|
||||
removePersistedPendingChatMessage,
|
||||
setPersistedPendingChatMessage,
|
||||
} from "./chatPendingMessageStorage";
|
||||
import { isLikelyTabSuspensionError, useTabVisibilitySuspension } from "./visibilitySuspension";
|
||||
import { clearCache, readCache, SWR_CACHE_KEYS, SWR_TASKS_MAX_AGE_MS, writeCache } from "../utils/swrCache";
|
||||
import { useAgentsMapCache } from "./useAgentsMapCache";
|
||||
@@ -482,6 +487,7 @@ export function useChat(
|
||||
const resetTransientComposerState = useCallback(() => {
|
||||
cancelStreamingFlushesRef.current?.();
|
||||
cancelStreamingFlushesRef.current = null;
|
||||
removePersistedPendingChatMessage(activeSessionRef.current?.id);
|
||||
pendingMessageRef.current = "";
|
||||
setPendingMessage("");
|
||||
setStreamingText("");
|
||||
@@ -491,6 +497,7 @@ export function useChat(
|
||||
}, []);
|
||||
|
||||
const clearPendingMessage = useCallback(() => {
|
||||
removePersistedPendingChatMessage(activeSessionRef.current?.id);
|
||||
pendingMessageRef.current = "";
|
||||
setPendingMessage("");
|
||||
}, []);
|
||||
@@ -501,6 +508,7 @@ export function useChat(
|
||||
return;
|
||||
}
|
||||
|
||||
removePersistedPendingChatMessage(activeSessionRef.current?.id);
|
||||
pendingMessageRef.current = "";
|
||||
setPendingMessage("");
|
||||
sendMessageRef.current(queuedMessage);
|
||||
@@ -596,6 +604,9 @@ export function useChat(
|
||||
if (id && currentActiveSessionId === id && !sessionOverride) {
|
||||
return;
|
||||
}
|
||||
if (currentActiveSessionId && currentActiveSessionId !== id) {
|
||||
removePersistedPendingChatMessage(currentActiveSessionId);
|
||||
}
|
||||
|
||||
// Close any existing stream
|
||||
if (streamRef.current) {
|
||||
@@ -663,6 +674,32 @@ export function useChat(
|
||||
// This is needed to avoid circular dependencies in useEffect
|
||||
selectSessionRef.current = selectSession;
|
||||
|
||||
useEffect(() => {
|
||||
const sessionId = activeSession?.id;
|
||||
if (!sessionId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const restoredPendingMessage = getPersistedPendingChatMessage(sessionId);
|
||||
if (!restoredPendingMessage) {
|
||||
return;
|
||||
}
|
||||
|
||||
pendingMessageRef.current = restoredPendingMessage;
|
||||
setPendingMessage(restoredPendingMessage);
|
||||
|
||||
queueMicrotask(() => {
|
||||
if (
|
||||
activeSessionRef.current?.id === sessionId &&
|
||||
pendingMessageRef.current.trim().length > 0 &&
|
||||
!isStreamingRef.current &&
|
||||
!streamRef.current
|
||||
) {
|
||||
flushPendingMessage();
|
||||
}
|
||||
});
|
||||
}, [activeSession?.id, flushPendingMessage]);
|
||||
|
||||
// Create a new session
|
||||
const createSession = useCallback(
|
||||
async (input: { agentId: string; title?: string; modelProvider?: string; modelId?: string }) => {
|
||||
@@ -829,6 +866,7 @@ export function useChat(
|
||||
if (isStreamingRef.current) {
|
||||
pendingMessageRef.current = content;
|
||||
setPendingMessage(content);
|
||||
setPersistedPendingChatMessage(activeSession.id, content);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,11 @@ export const FN_AGENT_ID = "__fn_agent__";
|
||||
export type { ChatMessageInfo, FallbackInfo, ToolCallInfo } from "./chatTypes";
|
||||
import type { ChatMessageInfo, FallbackInfo, ToolCallInfo } from "./chatTypes";
|
||||
import { createChatStreamHandlers } from "./createChatStreamHandlers";
|
||||
import {
|
||||
getPersistedPendingChatMessage,
|
||||
removePersistedPendingChatMessage,
|
||||
setPersistedPendingChatMessage,
|
||||
} from "./chatPendingMessageStorage";
|
||||
import { isLikelyTabSuspensionError, useTabVisibilitySuspension } from "./visibilitySuspension";
|
||||
|
||||
interface ModelSelection {
|
||||
@@ -276,6 +281,7 @@ export function useQuickChat(
|
||||
);
|
||||
|
||||
const clearPendingMessage = useCallback(() => {
|
||||
removePersistedPendingChatMessage(activeSessionRef.current?.id);
|
||||
pendingMessageRef.current = "";
|
||||
setPendingMessage("");
|
||||
}, []);
|
||||
@@ -286,6 +292,7 @@ export function useQuickChat(
|
||||
return;
|
||||
}
|
||||
|
||||
removePersistedPendingChatMessage(activeSessionRef.current?.id);
|
||||
pendingMessageRef.current = "";
|
||||
setPendingMessage("");
|
||||
const queuedCompletion = queuedPreSessionCompletionRef.current;
|
||||
@@ -514,6 +521,7 @@ export function useQuickChat(
|
||||
const resetTransientComposerState = useCallback(() => {
|
||||
cancelStreamingFlushesRef.current?.();
|
||||
cancelStreamingFlushesRef.current = null;
|
||||
removePersistedPendingChatMessage(activeSessionRef.current?.id);
|
||||
pendingMessageRef.current = "";
|
||||
setPendingMessage("");
|
||||
queuedPreSessionCompletionRef.current?.resolve();
|
||||
@@ -608,6 +616,32 @@ export function useQuickChat(
|
||||
}
|
||||
}, [attachIfGenerating, projectId, resetTransientComposerState]);
|
||||
|
||||
useEffect(() => {
|
||||
const sessionId = activeSession?.id;
|
||||
if (!sessionId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const restoredPendingMessage = getPersistedPendingChatMessage(sessionId);
|
||||
if (!restoredPendingMessage) {
|
||||
return;
|
||||
}
|
||||
|
||||
pendingMessageRef.current = restoredPendingMessage;
|
||||
setPendingMessage(restoredPendingMessage);
|
||||
|
||||
queueMicrotask(() => {
|
||||
if (
|
||||
activeSessionRef.current?.id === sessionId &&
|
||||
pendingMessageRef.current.trim().length > 0 &&
|
||||
!isStreamingRef.current &&
|
||||
!streamRef.current
|
||||
) {
|
||||
void flushPendingMessage();
|
||||
}
|
||||
});
|
||||
}, [activeSession?.id, flushPendingMessage]);
|
||||
|
||||
const startModelChat = useCallback(
|
||||
async (modelProvider: string, modelId: string) => {
|
||||
await switchSession(FN_AGENT_ID, modelProvider, modelId);
|
||||
@@ -765,6 +799,7 @@ export function useQuickChat(
|
||||
|
||||
pendingMessageRef.current = content;
|
||||
setPendingMessage(content);
|
||||
setPersistedPendingChatMessage(activeSession.id, content);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user