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:
5
.changeset/fn-4422-quick-chat-restore.md
Normal file
5
.changeset/fn-4422-quick-chat-restore.md
Normal 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.
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user