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

@ -534,6 +534,7 @@ export function CustomAgentChatView({
useState<ProfileCommandDialogState | null>(null);
const [opening, setOpening] = useState(false);
const [busy, setBusy] = useState(false);
const [stopping, setStopping] = useState(false);
const [error, setError] = useState<string | null>(null);
const scrollRef = useRef<HTMLDivElement | null>(null);
const composerRef = useRef<HTMLTextAreaElement | null>(null);
@ -1209,23 +1210,46 @@ export function CustomAgentChatView({
}
}
async function cancel() {
async function stopAgent() {
const sid = sessionRef.current;
if (!sid || !agent.cancelAgentChat) return;
setBusy(false);
setOpening(false);
if (!sid || !agent.stopLiveAgent || stopping) return;
setStopping(true);
setError(null);
try {
await agent.cancelAgentChat(sid);
setTurns((prev) => [...prev, { role: "tool", label: "Tour interrompu." }]);
const stopped = await agent.stopLiveAgent(projectId, agentId);
setBusy(false);
setOpening(false);
setCurrentSession(null);
publishSessionId(null);
setTurns((prev) => [
...completeRunningProgress(prev),
{
role: "tool",
label:
stopped.kind === "structured"
? "Agent arrêté. Cascade d'arrêt demandée pour les tâches et appels MCP liés."
: "Agent arrêté.",
},
]);
} catch (e) {
if (isNotFound(e)) {
setBusy(false);
setOpening(false);
setCurrentSession(null);
publishSessionId(null);
setTurns((prev) => [...prev, { role: "tool", label: "Session déjà fermée." }]);
setTurns((prev) => [
...completeRunningProgress(prev),
{ role: "tool", label: "Session déjà arrêtée." },
]);
return;
}
setError(describe(e));
setTurns((prev) => [
...prev,
{ role: "error", text: `Arrêt de l'agent échoué: ${describe(e)}` },
]);
} finally {
setStopping(false);
}
}
@ -1511,10 +1535,15 @@ export function CustomAgentChatView({
size="sm"
variant={busy ? "danger" : undefined}
className="shrink-0 whitespace-nowrap"
disabled={busy ? !supported || !currentSession : !canSend}
onClick={() => (busy ? void cancel() : void send())}
disabled={
busy
? !supported || !currentSession || !agent.stopLiveAgent || stopping
: !canSend
}
loading={stopping}
onClick={() => (busy ? void stopAgent() : void send())}
>
{busy ? "Stop" : "Envoyer"}
{stopping ? "Arrêt en cours..." : busy ? "Stop" : "Envoyer"}
</Button>
</div>
</div>