diff --git a/frontend/src/features/layout/LayoutGrid.chat.test.tsx b/frontend/src/features/layout/LayoutGrid.chat.test.tsx index 6d8cd01..8424779 100644 --- a/frontend/src/features/layout/LayoutGrid.chat.test.tsx +++ b/frontend/src/features/layout/LayoutGrid.chat.test.tsx @@ -77,6 +77,14 @@ const ptyProfile: AgentProfile = { cwdTemplate: "{projectRoot}", }; +function deferred() { + let resolve!: (value: T | PromiseLike) => void; + const promise = new Promise((res) => { + resolve = res; + }); + return { promise, resolve }; +} + beforeEach(() => { window.localStorage.clear(); }); @@ -191,4 +199,58 @@ describe("LayoutGrid custom agent CLI (#147)", () => { expect(screen.getByTestId("custom-agent-chat-view")).toBeTruthy(), ); }); + + it("keeps a restored custom CLI mode while the agent/profile catalog is still loading (#149)", async () => { + const layout = new MockLayoutGateway(); + const agent = new MockAgentGateway(); + const profileGateway = new MockProfileGateway(); + const terminal = new MockTerminalGateway(); + const system = new MockSystemGateway(); + await profileGateway.configureProfiles([structuredProfile]); + const created = await agent.createAgent("p1", { + name: "Worker", + profileId: structuredProfile.id, + }); + const tree = await layout.loadLayout("p1"); + const leafId = leaves(tree)[0].id; + await layout.mutateLayout("p1", { + type: "setCellAgent", + target: leafId, + agent: created.id, + }); + window.localStorage.setItem(`idea.agent-cell-mode.p1.${leafId}`, "custom"); + + const agentsLoaded = deferred(); + const profilesLoaded = deferred(); + const originalListAgents = agent.listAgents.bind(agent); + const originalListProfiles = profileGateway.listProfiles.bind(profileGateway); + vi.spyOn(agent, "listAgents").mockImplementation(async (projectId) => { + await agentsLoaded.promise; + return originalListAgents(projectId); + }); + vi.spyOn(profileGateway, "listProfiles").mockImplementation(async () => { + await profilesLoaded.promise; + return originalListProfiles(); + }); + + renderGrid({ + layout, + agent, + profile: profileGateway, + terminal, + system, + } as unknown as Gateways); + + await waitFor(() => expect(screen.getByTestId("layout-leaf")).toBeTruthy()); + expect(window.localStorage.getItem(`idea.agent-cell-mode.p1.${leafId}`)).toBe("custom"); + expect(screen.queryByTestId("custom-agent-chat-view")).toBeNull(); + + agentsLoaded.resolve(); + profilesLoaded.resolve(); + + await waitFor(() => + expect(screen.getByTestId("custom-agent-chat-view")).toBeTruthy(), + ); + expect(window.localStorage.getItem(`idea.agent-cell-mode.p1.${leafId}`)).toBe("custom"); + }); }); diff --git a/frontend/src/features/layout/LayoutGrid.tsx b/frontend/src/features/layout/LayoutGrid.tsx index f59b6c8..098de80 100644 --- a/frontend/src/features/layout/LayoutGrid.tsx +++ b/frontend/src/features/layout/LayoutGrid.tsx @@ -362,25 +362,48 @@ function LeafView({ // Load the project's agents for the dropdown. const [agents, setAgents] = useState([]); + const [agentsLoaded, setAgentsLoaded] = useState(false); useEffect(() => { - if (!agentGateway) return; + setAgentsLoaded(false); + if (!agentGateway) { + setAgentsLoaded(true); + return; + } let cancelled = false; agentGateway.listAgents(projectId).then((list) => { - if (!cancelled) setAgents(list); - }).catch(() => {/* ignore — dropdown stays empty */}); + if (!cancelled) { + setAgents(list); + setAgentsLoaded(true); + } + }).catch(() => { + if (!cancelled) setAgentsLoaded(true); + /* ignore — dropdown stays empty */ + }); return () => { cancelled = true; }; }, [agentGateway, projectId]); const [profiles, setProfiles] = useState([]); + const [profilesLoaded, setProfilesLoaded] = useState(false); useEffect(() => { + setProfilesLoaded(false); + if (!profileGateway) { + setProfilesLoaded(true); + return; + } let cancelled = false; profileGateway - ?.listProfiles() + .listProfiles() .then((list) => { - if (!cancelled) setProfiles(list); + if (!cancelled) { + setProfiles(list); + setProfilesLoaded(true); + } }) .catch(() => { - if (!cancelled) setProfiles([]); + if (!cancelled) { + setProfiles([]); + setProfilesLoaded(true); + } }); return () => { cancelled = true; @@ -486,8 +509,9 @@ function LeafView({ agentGateway?.closeAgentChat, ); useEffect(() => { + if (!agentsLoaded || !profilesLoaded) return; if (!customCliAvailable && cellMode !== "tui") setCellMode("tui"); - }, [cellMode, customCliAvailable]); + }, [agentsLoaded, cellMode, customCliAvailable, profilesLoaded, setCellMode]); const modelServerStatus = statusForAgent(pinnedAgent); const modelServerOverlay = modelServerOverlayText(modelServerStatus); // F2 — download progress (bar/%/bytes/source) when the status carries it; null