feat(FN-3062): add /clear command to Chat and Quick Chat, fix session banne

The merge adds a `/clear` command to both the Chat view and Quick Chat that clears transient chat state on session resets, with tests for all new hooks and components. It also persists session banner dismissals via a new `useSessionBannerPref` hook, adds a hide-banner setting, and preserves planning

Fusion-Task-Id: FN-3062
This commit is contained in:
Fusion
2026-05-01 13:17:09 -07:00
committed by gsxdsm
parent 0e0754f790
commit c729527fcc
9 changed files with 294 additions and 31 deletions

View File

@@ -674,6 +674,54 @@ describe("useChat", () => {
});
});
it("selectSession clears pending queued message state", async () => {
const sessionA = makeSession({ id: "session-001", agentId: "agent-001" });
const sessionB = makeSession({ id: "session-002", agentId: "agent-002" });
mockFetchChatSessions.mockResolvedValueOnce({ sessions: [sessionA, sessionB] });
mockFetchChatMessages.mockResolvedValue({ messages: [] });
mockStreamChatResponse.mockReturnValue({ close: vi.fn(), isConnected: () => true });
const { result } = renderHook(() => useChat("proj-123"));
await waitFor(() => {
expect(result.current.sessions).toHaveLength(2);
});
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");
});
await waitFor(() => {
expect(result.current.pendingMessage).toBe("Queued follow-up");
});
act(() => {
result.current.selectSession("session-002");
});
await waitFor(() => {
expect(result.current.pendingMessage).toBe("");
expect(result.current.isStreaming).toBe(false);
});
});
it("clearPendingMessage clears pending message", async () => {
const session = makeSession({ id: "session-001", agentId: "agent-001" });
mockFetchChatSessions.mockResolvedValueOnce({ sessions: [session] });

View File

@@ -357,6 +357,47 @@ describe("useQuickChat", () => {
});
});
it("startFreshSession clears queued pending message state", async () => {
const existingSession = makeSession({ id: "session-existing", agentId: "agent-001" });
const freshSession = makeSession({ id: "session-fresh", agentId: "agent-001" });
mockFetchResumeChatSession.mockResolvedValueOnce({ session: existingSession });
mockCreateChatSession.mockResolvedValueOnce({ session: freshSession });
mockFetchChatMessages.mockResolvedValue({ messages: [] });
const { result } = renderHook(() => useQuickChat("proj-123"));
await act(async () => {
await result.current.switchSession("agent-001");
});
act(() => {
result.current.sendMessage("Hello");
});
await waitFor(() => {
expect(result.current.isStreaming).toBe(true);
});
act(() => {
result.current.sendMessage("Queued follow-up");
});
await waitFor(() => {
expect(result.current.pendingMessage).toBe("Queued follow-up");
});
await act(async () => {
await result.current.startFreshSession();
});
await waitFor(() => {
expect(result.current.pendingMessage).toBe("");
expect(result.current.isStreaming).toBe(false);
expect(result.current.activeSession?.id).toBe("session-fresh");
});
});
it("stopStreaming aborts stream and resets streaming state", async () => {
const existingSession = makeSession({ id: "session-existing", agentId: "agent-001" });
const closeFn = vi.fn();