diff --git a/.changeset/fix-agent-picker-stale-cache.md b/.changeset/fix-agent-picker-stale-cache.md new file mode 100644 index 000000000..5ada36653 --- /dev/null +++ b/.changeset/fix-agent-picker-stale-cache.md @@ -0,0 +1,5 @@ +--- +"@gsxdsm/fusion": patch +--- + +Fix agent picker showing stale agents when switching between projects in QuickEntryBox and InlineCreateCard. The picker now clears cached agents when projectId changes, ensuring fresh agent data is always fetched for the current project context. diff --git a/packages/dashboard/app/components/InlineCreateCard.tsx b/packages/dashboard/app/components/InlineCreateCard.tsx index e1eb1bc05..74dbb4ded 100644 --- a/packages/dashboard/app/components/InlineCreateCard.tsx +++ b/packages/dashboard/app/components/InlineCreateCard.tsx @@ -119,6 +119,12 @@ export function InlineCreateCard({ } }, [description, projectId]); + // Clear agents cache when projectId changes to prevent stale agents from leaking across projects + useEffect(() => { + setAgents([]); + setSelectedAgentId(null); + }, [projectId]); + const loadModels = useCallback(async () => { if (availableModels) { setLoadedModels(availableModels); diff --git a/packages/dashboard/app/components/QuickEntryBox.tsx b/packages/dashboard/app/components/QuickEntryBox.tsx index 2fb0c57ea..a4c905935 100644 --- a/packages/dashboard/app/components/QuickEntryBox.tsx +++ b/packages/dashboard/app/components/QuickEntryBox.tsx @@ -230,6 +230,12 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels, } }, [description, projectId]); + // Clear agents cache when projectId changes to prevent stale agents from leaking across projects + useEffect(() => { + setAgents([]); + setSelectedAgentId(null); + }, [projectId]); + // Clean up legacy disclosure persistence key from previous versions useEffect(() => { if (typeof window !== "undefined") { diff --git a/packages/dashboard/app/components/__tests__/InlineCreateCard.test.tsx b/packages/dashboard/app/components/__tests__/InlineCreateCard.test.tsx index 5bd183af4..268ec6e85 100644 --- a/packages/dashboard/app/components/__tests__/InlineCreateCard.test.tsx +++ b/packages/dashboard/app/components/__tests__/InlineCreateCard.test.tsx @@ -1082,6 +1082,106 @@ describe("InlineCreateCard button visibility when collapsed", () => { expect(payload.assignedAgentId).toBeUndefined(); }); }); + + it("fetches fresh agents when projectId changes to prevent stale cache leakage", async () => { + const project1Agents = [ + { + id: "agent-001", + name: "Project One Agent", + role: "executor", + state: "active", + metadata: {}, + createdAt: "2026-01-01T00:00:00.000Z", + updatedAt: "2026-01-01T00:00:00.000Z", + }, + ] as any; + + const project2Agents = [ + { + id: "agent-002", + name: "Project Two Agent", + role: "executor", + state: "active", + metadata: {}, + createdAt: "2026-01-01T00:00:00.000Z", + updatedAt: "2026-01-01T00:00:00.000Z", + }, + ] as any; + + // First render with project 1 + vi.mocked(fetchAgents).mockResolvedValueOnce(project1Agents); + const { rerender } = renderCard([], { projectId: "proj-1" }); + expandCard(); + + // Open agent picker - should show project 1 agent + fireEvent.click(screen.getByTestId("inline-create-agent-button")); + await waitFor(() => { + expect(screen.getByText("Project One Agent")).toBeInTheDocument(); + }); + + // Close picker and switch to project 2 + fireEvent.click(screen.getByTestId("inline-create-agent-button")); + + // Rerender with different projectId - agents should be cleared and re-fetched + vi.mocked(fetchAgents).mockResolvedValueOnce(project2Agents); + rerender(); + + // Open picker again for project 2 + expandCard(); + fireEvent.click(screen.getByTestId("inline-create-agent-button")); + + await waitFor(() => { + // Should show project 2's agent, not the stale project 1 agent + expect(screen.getByText("Project Two Agent")).toBeInTheDocument(); + }); + + // Verify project 1's agent is NOT shown + expect(screen.queryByText("Project One Agent")).not.toBeInTheDocument(); + }); + + it("clears selected agent when projectId changes", async () => { + const agents = [ + { + id: "agent-001", + name: "Test Agent", + role: "executor", + state: "active", + metadata: {}, + createdAt: "2026-01-01T00:00:00.000Z", + updatedAt: "2026-01-01T00:00:00.000Z", + }, + ] as any; + + vi.mocked(fetchAgents).mockResolvedValue(agents); + const { rerender } = renderCard([], { projectId: "proj-1" }); + expandCard(); + + // Select an agent + fireEvent.click(screen.getByTestId("inline-create-agent-button")); + await waitFor(() => expect(screen.getByText("Test Agent")).toBeInTheDocument()); + fireEvent.click(screen.getByText("Test Agent")); + + // Switch to different project + rerender(); + + // Selected agent should be cleared, picker should show "Agent" without name + const agentButton = screen.getByTestId("inline-create-agent-button"); + expect(agentButton.textContent).toBe(" Agent"); + }); }); describe("description fullscreen expansion", () => { diff --git a/packages/dashboard/app/components/__tests__/QuickEntryBox.test.tsx b/packages/dashboard/app/components/__tests__/QuickEntryBox.test.tsx index 68cd0ccbd..fb19c0946 100644 --- a/packages/dashboard/app/components/__tests__/QuickEntryBox.test.tsx +++ b/packages/dashboard/app/components/__tests__/QuickEntryBox.test.tsx @@ -2335,6 +2335,104 @@ describe("QuickEntryBox", () => { expect(payload.assignedAgentId).toBeUndefined(); }); }); + + it("fetches fresh agents when projectId changes to prevent stale cache leakage", async () => { + const project1Agents = [ + { + id: "agent-001", + name: "Project One Agent", + role: "executor", + state: "active", + metadata: {}, + createdAt: "2026-01-01T00:00:00.000Z", + updatedAt: "2026-01-01T00:00:00.000Z", + }, + ] as any; + + const project2Agents = [ + { + id: "agent-002", + name: "Project Two Agent", + role: "executor", + state: "active", + metadata: {}, + createdAt: "2026-01-01T00:00:00.000Z", + updatedAt: "2026-01-01T00:00:00.000Z", + }, + ] as any; + + // First render with project 1 + vi.mocked(fetchAgents).mockResolvedValueOnce(project1Agents); + const { rerender } = renderQuickEntryBox({ projectId: "proj-1" }); + expandQuickEntry(); + + // Open agent picker - should show project 1 agent + fireEvent.click(screen.getByTestId("quick-entry-agent-button")); + await waitFor(() => { + expect(screen.getByText("Project One Agent")).toBeInTheDocument(); + }); + + // Close picker and switch to project 2 + fireEvent.click(screen.getByTestId("quick-entry-agent-button")); + + // Rerender with different projectId - agents should be cleared and re-fetched + vi.mocked(fetchAgents).mockResolvedValueOnce(project2Agents); + rerender(); + + // Open picker again for project 2 + expandQuickEntry(); + fireEvent.click(screen.getByTestId("quick-entry-agent-button")); + + await waitFor(() => { + // Should show project 2's agent, not the stale project 1 agent + expect(screen.getByText("Project Two Agent")).toBeInTheDocument(); + }); + + // Verify project 1's agent is NOT shown + expect(screen.queryByText("Project One Agent")).not.toBeInTheDocument(); + }); + + it("clears selected agent when projectId changes", async () => { + const agents = [ + { + id: "agent-001", + name: "Test Agent", + role: "executor", + state: "active", + metadata: {}, + createdAt: "2026-01-01T00:00:00.000Z", + updatedAt: "2026-01-01T00:00:00.000Z", + }, + ] as any; + + vi.mocked(fetchAgents).mockResolvedValue(agents); + const { rerender } = renderQuickEntryBox({ projectId: "proj-1" }); + expandQuickEntry(); + + // Select an agent + fireEvent.click(screen.getByTestId("quick-entry-agent-button")); + await waitFor(() => expect(screen.getByText("Test Agent")).toBeInTheDocument()); + fireEvent.click(screen.getByText("Test Agent")); + + // Switch to different project + rerender(); + + // Selected agent should be cleared, picker should show "Agent" without name + const agentButton = screen.getByTestId("quick-entry-agent-button"); + expect(agentButton.textContent).toBe(" Agent"); + }); }); describe("description fullscreen expansion", () => {