fix(chat): le bouton Stop de la CLI custom arrête l'agent en cascade (#187)

Le Stop du CustomAgentChatView n'annulait que le tour courant sans libérer la
session ni toucher aux tâches enfants ou appels MCP en cours. Il appelle
désormais stop_live_agent sur l'agent vivant, qui:

- ajoute le port StopAgentExecutionCascade (best-effort, idempotent), implémenté
  par OrchestratorService pour annuler les idea_run_in_background rattachés au
  rendez-vous actif (via BackgroundTaskRunner.cancel);
- cascade l'arrêt aux wait-dépendances (stop_turn_watch + preempt) pour
  interrompre les appels MCP et les attentes inter-agents;
- libère et supprime la session structurée.

UI: nouvel état "Arrêt en cours..." (bouton disabled + loading) pendant l'arrêt,
libération du sessionId, label expliquant la cascade demandée. La Tauri command
cancel_agent_chat est alignée sur stop_live_agent + stop_turn_watch agent et
dépendances.

Tests: SpyBackgroundRunner (cascade n'annule que les tâches rattachées),
workstate_actions cascade best-effort, CustomAgentChatView stop+cascade feedback
et fallback NOT_FOUND.
This commit is contained in:
2026-08-11 18:14:47 +02:00
parent 2413aa7473
commit 22ce2f62fd
9 changed files with 293 additions and 46 deletions

View File

@ -101,7 +101,12 @@ describe("CustomAgentChatView", () => {
expect(screen.getByText("Fichier: clipboard.png")).toBeTruthy();
});
it("cancels only the current turn and keeps the structured session alive", async () => {
it("stops the live agent with cascade feedback and releases the structured session", async () => {
let resolveStop: (value: {
agentId: string;
sessionId: string;
kind: "structured";
}) => void = () => {};
const agent = {
launchAgentChat: vi.fn(),
reattachAgentChat: vi.fn(async (sessionId: string) => ({
@ -109,6 +114,14 @@ describe("CustomAgentChatView", () => {
scrollback: [],
})),
sendAgentChat: vi.fn(() => new Promise<void>(() => {})),
stopLiveAgent: vi.fn(
() =>
new Promise<{ agentId: string; sessionId: string; kind: "structured" }>(
(resolve) => {
resolveStop = resolve;
},
),
),
cancelAgentChat: vi.fn(async () => {}),
closeAgentChat: vi.fn(async () => {}),
};
@ -159,24 +172,27 @@ describe("CustomAgentChatView", () => {
fireEvent.click(await screen.findByRole("button", { name: "Stop" }));
const stoppingLabel = await screen.findByText("Arrêt en cours...");
expect(stoppingLabel.closest("button")?.hasAttribute("disabled")).toBe(true);
await waitFor(() =>
expect(agent.cancelAgentChat).toHaveBeenCalledWith("chat-session-1"),
expect(agent.stopLiveAgent).toHaveBeenCalledWith("project-1", "agent-1"),
);
expect(agent.closeAgentChat).not.toHaveBeenCalled();
expect(onSessionId).not.toHaveBeenCalledWith(null);
expect(screen.getByText("Tour interrompu.")).toBeTruthy();
fireEvent.change(screen.getByLabelText(/message CLI custom/), {
target: { value: "second turn" },
resolveStop({
agentId: "agent-1",
sessionId: "chat-session-1",
kind: "structured",
});
fireEvent.click(screen.getByRole("button", { name: "Envoyer" }));
await waitFor(() => expect(agent.sendAgentChat).toHaveBeenCalledTimes(2));
expect(agent.sendAgentChat).toHaveBeenLastCalledWith(
"chat-session-1",
"second turn",
expect.any(Function),
);
await waitFor(() => expect(onSessionId).toHaveBeenCalledWith(null));
expect(agent.cancelAgentChat).not.toHaveBeenCalled();
expect(agent.closeAgentChat).not.toHaveBeenCalled();
expect(
screen.getByText(
"Agent arrêté. Cascade d'arrêt demandée pour les tâches et appels MCP liés.",
),
).toBeTruthy();
expect(
screen.getByRole("button", { name: "Envoyer" }).hasAttribute("disabled"),
).toBe(true);
});
it("falls back to a fresh launch when reattach reports NOT_FOUND", async () => {
@ -2569,10 +2585,11 @@ describe("CustomAgentChatView", () => {
scrollback: [],
})),
sendAgentChat: vi.fn(() => new Promise<void>(() => {})),
cancelAgentChat: vi.fn().mockRejectedValueOnce({
stopLiveAgent: vi.fn().mockRejectedValueOnce({
code: "NOT_FOUND",
message: "not found: structured session gone-session",
message: "agent agent-1 has no live session",
}),
cancelAgentChat: vi.fn(async () => {}),
closeAgentChat: vi.fn(async () => {}),
};
const onSessionId = vi.fn();
@ -2614,8 +2631,9 @@ describe("CustomAgentChatView", () => {
fireEvent.click(await screen.findByRole("button", { name: "Stop" }));
expect(await screen.findByText("Session déjà fermée.")).toBeTruthy();
expect(await screen.findByText("Session déjà arrêtée.")).toBeTruthy();
expect(onSessionId).toHaveBeenCalledWith(null);
expect(agent.cancelAgentChat).not.toHaveBeenCalled();
expect(agent.closeAgentChat).not.toHaveBeenCalled();
expect(screen.queryByText(/not found: structured session/)).toBeNull();
});