From 51bda204555d80c06dae94d84812b9ad690d25ce Mon Sep 17 00:00:00 2001 From: Blomios Date: Wed, 5 Aug 2026 13:45:41 +0200 Subject: [PATCH 1/3] fix(chat): preserve custom CLI preference during catalog load (#149) Co-Authored-By: Claude Opus 4.8 --- .../features/layout/LayoutGrid.chat.test.tsx | 62 +++++++++++++++++++ frontend/src/features/layout/LayoutGrid.tsx | 38 +++++++++--- 2 files changed, 93 insertions(+), 7 deletions(-) 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 From dedc3b5134f0e3c28388cc687dd9d8a36d7df06a Mon Sep 17 00:00:00 2001 From: Blomios Date: Wed, 5 Aug 2026 13:48:27 +0200 Subject: [PATCH 2/3] =?UTF-8?q?test(chat):=20couvre=20le=20fallback=20TUI?= =?UTF-8?q?=20si=20le=20catalogue=20invalide=20le=20profil=20pinn=C3=A9=20?= =?UTF-8?q?(#149)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- .../features/layout/LayoutGrid.chat.test.tsx | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/frontend/src/features/layout/LayoutGrid.chat.test.tsx b/frontend/src/features/layout/LayoutGrid.chat.test.tsx index 8424779..0948baa 100644 --- a/frontend/src/features/layout/LayoutGrid.chat.test.tsx +++ b/frontend/src/features/layout/LayoutGrid.chat.test.tsx @@ -253,4 +253,58 @@ describe("LayoutGrid custom agent CLI (#147)", () => { ); 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(); + }); }); From 0103a8acf6876d4d8ce245473cf8ec477f3bdab4 Mon Sep 17 00:00:00 2001 From: Blomios Date: Wed, 5 Aug 2026 13:48:30 +0200 Subject: [PATCH 3/3] =?UTF-8?q?chore(tickets):=20cl=C3=B4ture=20#149=20?= =?UTF-8?q?=E2=80=94=20QA=20verte=20sur=20feature/ticket149-custom-cli-sil?= =?UTF-8?q?ent-fallback?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- .ideai/tickets/149/carnet.md | 4 ++-- .ideai/tickets/149/issue.md | 6 +++--- .ideai/tickets/index.json | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) 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