fix(frontend): expose idea_ask_agents et renforce le fit terminal au remount projet

Panneau Permissions: idea_ask_agents est câblé côté backend MCP mais absent
du groupe « Délégation agents » du panneau — ajout de l'outil et de son
label, catalogue mock MCP aligné en conséquence.

Terminaux: ProjectsView remonte LayoutGrid via une key au changement de
projet, donc refitEpoch n'aide pas ; le montage de TerminalView ne
maintenait pas assez longtemps le fit tant que la nouvelle cellule ne
s'était pas stabilisée. Le chemin de montage garde désormais une traîne de
refits plus longue (MOUNT_SETTLE_REFIT_FRAMES) avant de laisser la main au
ResizeObserver.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-28 22:51:48 +02:00
parent b4f4c8eb71
commit f0ba559885
5 changed files with 73 additions and 3 deletions

View File

@ -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",

View File

@ -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();

View File

@ -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<string, string> = {
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",

View File

@ -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 (
<DIProvider gateways={{ terminal: new MockTerminalGateway() } as unknown as Gateways}>
<TerminalView
key={projectId}
cwd={`/cwd/${projectId}`}
open={open}
/>
</DIProvider>
);
}
const { rerender } = render(<ProjectTerminal projectId="p1" />);
await waitFor(() => expect(open).toHaveBeenCalledTimes(1));
setTerminalBoxSize(400, 200);
await waitFor(() => expect(firstHandle.resize).toHaveBeenCalled());
fitSpy.mockClear();
rerender(<ProjectTerminal projectId="p2" />);
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" }));

View File

@ -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);