feat(FN-4422): restore last active quick chat session on reopen

Restores the latest active quick chat session when the QuickChatFAB is opened, with corresponding test coverage for the restored session behavior.

Fusion-Task-Id: FN-4422
This commit is contained in:
Fusion
2026-05-13 21:27:01 -07:00
committed by gsxdsm
parent 9b4a1ca04d
commit 3afc086995
3 changed files with 72 additions and 20 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": patch
---
Quick Chat now reliably restores the most recently active non-archived conversation by session ID when reopened, instead of occasionally landing on an older thread that shares the same target.

View File

@@ -939,6 +939,7 @@ export function QuickChatFAB({
const modelsInitSettledRef = useRef(false); const modelsInitSettledRef = useRef(false);
const prevSessionTargetRef = useRef(""); const prevSessionTargetRef = useRef("");
const hasAppliedInitialSessionRef = useRef(false); const hasAppliedInitialSessionRef = useRef(false);
const restoredFromExistingSessionRef = useRef(false);
const selectedAgentIdRef = useRef(selectedAgentId); const selectedAgentIdRef = useRef(selectedAgentId);
const selectedModelRef = useRef(selectedModel); const selectedModelRef = useRef(selectedModel);
const mentionCursorPosRef = useRef(0); const mentionCursorPosRef = useRef(0);
@@ -1234,11 +1235,19 @@ export function QuickChatFAB({
return; return;
} }
const latestSession = sessions[0]; const activeSessions = sessions.filter((session) => session.status !== "archived");
if (!latestSession) { const timestamp = (value?: string | null): number => {
return; if (!value) return 0;
} const parsed = Date.parse(value);
return Number.isFinite(parsed) ? parsed : 0;
};
const latestSession = [...activeSessions].sort((a, b) => {
const aLastTouched = Math.max(timestamp(a.lastMessageAt), timestamp(a.updatedAt));
const bLastTouched = Math.max(timestamp(b.lastMessageAt), timestamp(b.updatedAt));
return bLastTouched - aLastTouched;
})[0];
if (latestSession) {
if (latestSession.modelProvider && latestSession.modelId) { if (latestSession.modelProvider && latestSession.modelId) {
setChatMode("model"); setChatMode("model");
setSelectedModel(`${latestSession.modelProvider}/${latestSession.modelId}`); setSelectedModel(`${latestSession.modelProvider}/${latestSession.modelId}`);
@@ -1247,8 +1256,14 @@ export function QuickChatFAB({
setSelectedAgentId(latestSession.agentId); setSelectedAgentId(latestSession.agentId);
} }
restoredFromExistingSessionRef.current = true;
void selectSession(latestSession);
} else {
restoredFromExistingSessionRef.current = false;
}
hasAppliedInitialSessionRef.current = true; hasAppliedInitialSessionRef.current = true;
}, [isOpen, sessions, sessionsLoading]); }, [isOpen, selectSession, sessions, sessionsLoading]);
// Initialize/switch quick chat session whenever the selected target changes. // Initialize/switch quick chat session whenever the selected target changes.
// NOTE: activeSession and sessionsLoading are in the dependency array to // NOTE: activeSession and sessionsLoading are in the dependency array to
@@ -1287,6 +1302,12 @@ export function QuickChatFAB({
&& !activeSession && !activeSession
&& !sessionsLoading; && !sessionsLoading;
if (restoredFromExistingSessionRef.current) {
restoredFromExistingSessionRef.current = false;
prevSessionTargetRef.current = sessionTargetKey;
return;
}
if (sessionTargetKey === prevSessionTargetRef.current && !shouldRetrySessionInit) { if (sessionTargetKey === prevSessionTargetRef.current && !shouldRetrySessionInit) {
return; return;
} }

View File

@@ -164,9 +164,22 @@ describe("QuickChatFAB session-first UX", () => {
expect(screen.getByTestId("quick-chat-new-model-select")).toBeInTheDocument(); expect(screen.getByTestId("quick-chat-new-model-select")).toBeInTheDocument();
}); });
it("restores the newest agent session target on first open after reload", async () => { it("restores the most recently touched active session by id", async () => {
mockFetchChatSessions.mockResolvedValueOnce({ mockFetchChatSessions.mockResolvedValueOnce({
sessions: [agentTwoSession, modelSession, agentSession], sessions: [
{
...modelSession,
id: "older-updated",
updatedAt: "2026-05-13T10:00:00.000Z",
lastMessageAt: "2026-05-13T10:00:00.000Z",
},
{
...agentTwoSession,
id: "newer-last-message",
updatedAt: "2026-05-13T09:00:00.000Z",
lastMessageAt: "2026-05-13T11:00:00.000Z",
},
],
}); });
render(<QuickChatFAB addToast={vi.fn()} projectId="proj-1" />); render(<QuickChatFAB addToast={vi.fn()} projectId="proj-1" />);
@@ -174,25 +187,38 @@ describe("QuickChatFAB session-first UX", () => {
await waitFor(() => { await waitFor(() => {
expect(screen.getByTestId("quick-chat-input")).toHaveAttribute("placeholder", "Message Agent Two"); expect(screen.getByTestId("quick-chat-input")).toHaveAttribute("placeholder", "Message Agent Two");
expect(screen.getByTestId("quick-chat-session-dropdown")).toHaveValue("newer-last-message");
}); });
expect(mockFetchResumeChatSession).toHaveBeenCalledWith({ agentId: "agent-002" }, "proj-1"); expect(mockFetchResumeChatSession).not.toHaveBeenCalled();
}); });
it("restores the newest model session target instead of the configured default", async () => { it("skips archived newest sessions and restores the newest active session", async () => {
mockFetchChatSessions.mockResolvedValueOnce({ mockFetchChatSessions.mockResolvedValueOnce({
sessions: [modelSessionAnthropic, modelSession, agentSession], sessions: [
{
...modelSessionAnthropic,
id: "archived-newest",
status: "archived",
updatedAt: "2026-05-13T12:00:00.000Z",
lastMessageAt: "2026-05-13T12:00:00.000Z",
},
{
...agentTwoSession,
id: "active-latest",
updatedAt: "2026-05-13T11:00:00.000Z",
lastMessageAt: "2026-05-13T11:00:00.000Z",
},
],
}); });
render(<QuickChatFAB addToast={vi.fn()} projectId="proj-1" />); render(<QuickChatFAB addToast={vi.fn()} projectId="proj-1" />);
fireEvent.click(screen.getByTestId("quick-chat-fab")); fireEvent.click(screen.getByTestId("quick-chat-fab"));
await waitFor(() => { await waitFor(() => {
expect(screen.getByTestId("quick-chat-model-tag")).toHaveTextContent("Claude 3.7 Sonnet"); expect(screen.getByTestId("quick-chat-input")).toHaveAttribute("placeholder", "Message Agent Two");
expect(screen.getByTestId("quick-chat-session-dropdown")).toHaveValue("active-latest");
}); });
expect(mockFetchResumeChatSession).toHaveBeenCalledWith( expect(screen.getByRole("option", { name: /Claude thread/i })).toBeInTheDocument();
{ agentId: "__fn_agent__", modelProvider: "anthropic", modelId: "claude-3-7-sonnet" },
"proj-1",
);
}); });
it("falls back to the existing default target when there are no prior sessions", async () => { it("falls back to the existing default target when there are no prior sessions", async () => {