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 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,21 +1235,35 @@ 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.modelProvider && latestSession.modelId) {
|
if (latestSession) {
|
||||||
setChatMode("model");
|
if (latestSession.modelProvider && latestSession.modelId) {
|
||||||
setSelectedModel(`${latestSession.modelProvider}/${latestSession.modelId}`);
|
setChatMode("model");
|
||||||
|
setSelectedModel(`${latestSession.modelProvider}/${latestSession.modelId}`);
|
||||||
|
} else {
|
||||||
|
setChatMode("agent");
|
||||||
|
setSelectedAgentId(latestSession.agentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
restoredFromExistingSessionRef.current = true;
|
||||||
|
void selectSession(latestSession);
|
||||||
} else {
|
} else {
|
||||||
setChatMode("agent");
|
restoredFromExistingSessionRef.current = false;
|
||||||
setSelectedAgentId(latestSession.agentId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user