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 2ed7ea2f86
commit 0553f87e7d
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 prevSessionTargetRef = useRef("");
const hasAppliedInitialSessionRef = useRef(false);
const restoredFromExistingSessionRef = useRef(false);
const selectedAgentIdRef = useRef(selectedAgentId);
const selectedModelRef = useRef(selectedModel);
const mentionCursorPosRef = useRef(0);
@@ -1234,21 +1235,35 @@ export function QuickChatFAB({
return;
}
const latestSession = sessions[0];
if (!latestSession) {
return;
}
const activeSessions = sessions.filter((session) => session.status !== "archived");
const timestamp = (value?: string | null): number => {
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.modelProvider && latestSession.modelId) {
setChatMode("model");
setSelectedModel(`${latestSession.modelProvider}/${latestSession.modelId}`);
if (latestSession) {
if (latestSession.modelProvider && latestSession.modelId) {
setChatMode("model");
setSelectedModel(`${latestSession.modelProvider}/${latestSession.modelId}`);
} else {
setChatMode("agent");
setSelectedAgentId(latestSession.agentId);
}
restoredFromExistingSessionRef.current = true;
void selectSession(latestSession);
} else {
setChatMode("agent");
setSelectedAgentId(latestSession.agentId);
restoredFromExistingSessionRef.current = false;
}
hasAppliedInitialSessionRef.current = true;
}, [isOpen, sessions, sessionsLoading]);
}, [isOpen, selectSession, sessions, sessionsLoading]);
// Initialize/switch quick chat session whenever the selected target changes.
// NOTE: activeSession and sessionsLoading are in the dependency array to
@@ -1287,6 +1302,12 @@ export function QuickChatFAB({
&& !activeSession
&& !sessionsLoading;
if (restoredFromExistingSessionRef.current) {
restoredFromExistingSessionRef.current = false;
prevSessionTargetRef.current = sessionTargetKey;
return;
}
if (sessionTargetKey === prevSessionTargetRef.current && !shouldRetrySessionInit) {
return;
}

View File

@@ -164,9 +164,22 @@ describe("QuickChatFAB session-first UX", () => {
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({
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" />);
@@ -174,25 +187,38 @@ describe("QuickChatFAB session-first UX", () => {
await waitFor(() => {
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({
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" />);
fireEvent.click(screen.getByTestId("quick-chat-fab"));
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(
{ agentId: "__fn_agent__", modelProvider: "anthropic", modelId: "claude-3-7-sonnet" },
"proj-1",
);
expect(screen.getByRole("option", { name: /Claude thread/i })).toBeInTheDocument();
});
it("falls back to the existing default target when there are no prior sessions", async () => {