diff --git a/.ideai/tickets/149/carnet.md b/.ideai/tickets/149/carnet.md index 6cf3d41..eacaa8a 100644 --- a/.ideai/tickets/149/carnet.md +++ b/.ideai/tickets/149/carnet.md @@ -1,6 +1,6 @@ --- issueRef: "#149" -version: 1 +version: 2 updatedBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"} -updatedAt: 1785930073255 +updatedAt: 1785930103912 --- diff --git a/.ideai/tickets/149/issue.md b/.ideai/tickets/149/issue.md index b403d88..74bf771 100644 --- a/.ideai/tickets/149/issue.md +++ b/.ideai/tickets/149/issue.md @@ -2,7 +2,7 @@ id: "1bd74960-361f-4083-acff-4c0b55cd920f" number: 149 title: "CLI custom: fallback silencieux vers Plain/TUI avant chargement du catalogue agent/profil" -status: "open" +status: "inProgress" priority: "high" sprint: null links: [{"target":"#148","kind":"relatesTo"},{"target":"#147","kind":"relatesTo"}] @@ -11,7 +11,7 @@ attachments: [] createdBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"} updatedBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"} createdAt: 1785930073255 -updatedAt: 1785930073255 -version: 1 +updatedAt: 1785930103912 +version: 2 --- Bug report utilisateur du 2026-08-05: la CLI custom ne se lance plus; à l'ouverture elle se ferme immédiatement et la cellule revient sur Plain/TUI. Diagnostic Architecture: bug distinct de #148. Dans `frontend/src/features/layout/LayoutGrid.tsx`, `cellMode` restauré à `custom` depuis le storage est forcé trop tôt vers `tui` par l'effet garde-fou `if (!customCliAvailable && cellMode !== "tui") setCellMode("tui")`, alors que `agents`/`profiles` sont encore vides pendant leur chargement asynchrone initial. Résultat: la préférence `custom` est écrasée silencieusement avant même que la vue puisse se monter. Objectif: retarder ce fallback jusqu'à la fin du premier chargement du catalogue, couvrir par test de non-régression, valider QA, puis rebuild AppImage Linux. \ No newline at end of file diff --git a/.ideai/tickets/index.json b/.ideai/tickets/index.json index 48165b1..2626158 100644 --- a/.ideai/tickets/index.json +++ b/.ideai/tickets/index.json @@ -1950,7 +1950,7 @@ "issueRef": "#149", "path": "149", "title": "CLI custom: fallback silencieux vers Plain/TUI avant chargement du catalogue agent/profil", - "status": "open", + "status": "inProgress", "priority": "high", "sprint": null, "assignedAgentIds": [ @@ -1960,7 +1960,7 @@ "kind": "agent", "agent_id": "a6c6ea12-bfc6-4bdc-8031-324102dfa34d" }, - "updatedAt": 1785930073255 + "updatedAt": 1785930103912 } ] } \ No newline at end of file diff --git a/frontend/src/features/layout/LayoutGrid.chat.test.tsx b/frontend/src/features/layout/LayoutGrid.chat.test.tsx index 6d8cd01..0948baa 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,112 @@ 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"); + }); + + it("falls back to native TUI once the catalog confirms the pinned profile is incompatible", async () => { + const layout = new MockLayoutGateway(); + const agent = new MockAgentGateway(); + const profileGateway = new MockProfileGateway(); + const terminal = new MockTerminalGateway(); + const system = new MockSystemGateway(); + await profileGateway.configureProfiles([ptyProfile]); + const created = await agent.createAgent("p1", { + name: "Worker", + profileId: ptyProfile.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"); + + agentsLoaded.resolve(); + profilesLoaded.resolve(); + + await waitFor(() => + expect(window.localStorage.getItem(`idea.agent-cell-mode.p1.${leafId}`)).toBe("tui"), + ); + expect(screen.getByTestId("terminal-view")).toBeTruthy(); + expect(screen.queryByTestId("custom-agent-chat-view")).toBeNull(); + }); }); 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