diff --git a/frontend/src/adapters/mock/index.ts b/frontend/src/adapters/mock/index.ts index 6702c38..8de814d 100644 --- a/frontend/src/adapters/mock/index.ts +++ b/frontend/src/adapters/mock/index.ts @@ -2462,6 +2462,7 @@ export class MockPermissionGateway implements PermissionGateway { ], writeActionTools: [ "idea_ask_agent", + "idea_ask_agents", "idea_run_in_background", "idea_launch_agent", "idea_stop_agent", diff --git a/frontend/src/features/permissions/McpToolPermissionsPanel.test.tsx b/frontend/src/features/permissions/McpToolPermissionsPanel.test.tsx index 9b0e3d5..33cd6d6 100644 --- a/frontend/src/features/permissions/McpToolPermissionsPanel.test.tsx +++ b/frontend/src/features/permissions/McpToolPermissionsPanel.test.tsx @@ -108,6 +108,19 @@ describe("PermissionsPanel — Tools MCP IdeA tab", () => { expect(screen.getByRole("button", { name: "Enregistrer" })).toHaveProperty("disabled", true); }); + it("shows multi-agent delegation in the Délégation agents MCP group", async () => { + await renderPanel(); + await openMcpToolsTab(); + + const delegationGroup = screen.getByRole("button", { + name: /Délégation agents/, + }); + expect(delegationGroup).toBeTruthy(); + expect(screen.getByRole("checkbox", { + name: /Solliciter plusieurs agents, idea_ask_agents$/, + })).toBeTruthy(); + }); + it("creating an override prefills the draft with the current effective allowlist", async () => { await renderPanel(); await openMcpToolsTab(); diff --git a/frontend/src/features/permissions/mcpToolGroups.ts b/frontend/src/features/permissions/mcpToolGroups.ts index 82c3606..603e031 100644 --- a/frontend/src/features/permissions/mcpToolGroups.ts +++ b/frontend/src/features/permissions/mcpToolGroups.ts @@ -39,7 +39,12 @@ export const MCP_TOOL_GROUPS: McpToolGroupDef[] = [ { id: "delegation", label: "Délégation agents", - tools: ["idea_ask_agent", "idea_launch_agent", "idea_stop_agent"], + tools: [ + "idea_ask_agent", + "idea_ask_agents", + "idea_launch_agent", + "idea_stop_agent", + ], }, { id: "contextMemory", @@ -83,6 +88,7 @@ export const MCP_TOOL_LABELS: Record = { idea_ticket_read_carnet: "Lire le carnet d'un ticket", idea_sprint_list: "Lister les sprints", idea_ask_agent: "Déléguer à un agent", + idea_ask_agents: "Solliciter plusieurs agents", idea_launch_agent: "Lancer un agent", idea_stop_agent: "Arrêter un agent", idea_update_context: "Modifier le contexte d'un agent", diff --git a/frontend/src/features/terminals/TerminalView.test.tsx b/frontend/src/features/terminals/TerminalView.test.tsx index 5ba23e4..6e60038 100644 --- a/frontend/src/features/terminals/TerminalView.test.tsx +++ b/frontend/src/features/terminals/TerminalView.test.tsx @@ -455,6 +455,52 @@ describe("TerminalView — visible launch-failure surface (ticket #14 F3)", () = fitSpy.mockRestore(); }); + it("keeps fitting across a keyed project remount until the new cell settles", async () => { + // ProjectsView keys LayoutGrid by project. A project switch therefore + // unmounts/remounts TerminalView, so the mount path itself must keep a + // short tail of fits alive while the new cell geometry settles. + const fitSpy = vi.spyOn(FitAddon.prototype, "fit"); + const firstHandle = makeHandle({ sessionId: "project-1" }); + const secondHandle = makeHandle({ sessionId: "project-2" }); + const open = vi + .fn() + .mockResolvedValueOnce(firstHandle) + .mockResolvedValueOnce(secondHandle); + + function ProjectTerminal({ projectId }: { projectId: string }) { + return ( + + + + ); + } + + const { rerender } = render(); + await waitFor(() => expect(open).toHaveBeenCalledTimes(1)); + setTerminalBoxSize(400, 200); + await waitFor(() => expect(firstHandle.resize).toHaveBeenCalled()); + + fitSpy.mockClear(); + rerender(); + await waitFor(() => expect(open).toHaveBeenCalledTimes(2)); + setTerminalBoxSize(520, 260); + + for (let i = 0; i < 8; i += 1) { + await new Promise((resolve) => requestAnimationFrame(resolve)); + } + + expect(fitSpy.mock.calls.length).toBeGreaterThan(5); + await waitFor(() => expect(secondHandle.resize).toHaveBeenCalled()); + expect(firstHandle.close).not.toHaveBeenCalled(); + expect(secondHandle.close).not.toHaveBeenCalled(); + + fitSpy.mockRestore(); + }); + it("does not refit when refitSignal is left undefined (no-op for callers that don't pass it)", async () => { const fitSpy = vi.spyOn(FitAddon.prototype, "fit"); const open = vi.fn(async () => makeHandle({ sessionId: "no-signal-1" })); diff --git a/frontend/src/features/terminals/TerminalView.tsx b/frontend/src/features/terminals/TerminalView.tsx index 015067f..6a2e8b9 100644 --- a/frontend/src/features/terminals/TerminalView.tsx +++ b/frontend/src/features/terminals/TerminalView.tsx @@ -368,7 +368,11 @@ export function TerminalView({ // manual resize. This stays bounded and preserves the rows/cols-changed // guard before touching the PTY. const SETTLE_REFIT_FRAMES = 4; - const MAX_ZERO_SIZE_RETRIES = 8; + // Project switches remount LayoutGrid/TerminalView by key. The new cell can + // report a non-zero but still intermediate box for more than the ordinary + // transition tail, so the mount path keeps fitting a little longer. + const MOUNT_SETTLE_REFIT_FRAMES = 12; + const MAX_ZERO_SIZE_RETRIES = MOUNT_SETTLE_REFIT_FRAMES; let zeroSizeRetries = 0; const refit = () => { rafId = 0; @@ -418,7 +422,7 @@ export function TerminalView({ }; const ro = new ResizeObserver(() => scheduleRefit()); ro.observe(container); - scheduleSettledRefit(); + scheduleRefit(MOUNT_SETTLE_REFIT_FRAMES); window.addEventListener("resize", scheduleSettledRefit); window.addEventListener("focus", scheduleSettledRefit); window.addEventListener("pageshow", scheduleSettledRefit);