fix(domain,application,infrastructure,backend,app-tauri,frontend): Stop demande agent préserve la conversation ouverte — arrêt sans fermeture/clôture du chat, états cohérents après cancel sur les 3 adapters de session, cellule et workstate synchronisés — #343 (QA verte)

This commit is contained in:
2026-09-13 10:03:36 +02:00
parent a1e8a6466f
commit c37638ff24
26 changed files with 1210 additions and 232 deletions

View File

@ -1009,6 +1009,7 @@ export function CustomAgentChatView({
>(null);
const [busy, setBusy] = useState(false);
const [stopping, setStopping] = useState(false);
const [stopTerminalOverride, setStopTerminalOverride] = useState(false);
const [error, setError] = useState<ChatErrorState | null>(null);
const [projectIcon, setProjectIcon] = useState<ProjectIconRef | null>(null);
const scrollRef = useRef<HTMLDivElement | null>(null);
@ -1032,6 +1033,11 @@ export function CustomAgentChatView({
const draftSaveSeqRef = useRef(0);
const interactionSeqRef = useRef(0);
const activeInteractionIdRef = useRef<string | null>(null);
const cancelledInteractionIdsRef = useRef(new Set<string>());
const ignoreUnscopedChunksAfterStopRef = useRef(false);
const stopInFlightRef = useRef(false);
const stopTerminalOverrideRef = useRef(false);
const requesterRefreshSeqRef = useRef(0);
const draftHydratedRef = useRef(false);
const draftEditedBeforeHydrationRef = useRef(false);
const lastPersistedDraftRef = useRef("");
@ -1185,18 +1191,24 @@ export function CustomAgentChatView({
busy ||
stopping ||
Boolean(restart && restart.outcome !== "failed") ||
requesterDelegation.currentAgentBusy ||
delegationActive;
(!stopTerminalOverride &&
(requesterDelegation.currentAgentBusy || delegationActive));
const delegationFallbackText = requesterDelegation.delegationTargetName
? `${agentName} attend ${requesterDelegation.delegationTargetName}`
: tr("Délégation en cours.", "Delegation in progress.");
const refreshRequesterDelegation = useCallback(async () => {
const refreshSeq = ++requesterRefreshSeqRef.current;
if (!workState) {
if (refreshSeq !== requesterRefreshSeqRef.current) return;
setRequesterDelegation({
currentAgentBusy: false,
delegationTargetName: null,
});
if (stopTerminalOverrideRef.current) {
stopTerminalOverrideRef.current = false;
setStopTerminalOverride(false);
}
return;
}
try {
@ -1214,11 +1226,17 @@ export function CustomAgentChatView({
ticket.source.agentId === agentId,
),
);
if (refreshSeq !== requesterRefreshSeqRef.current) return;
setRequesterDelegation({
currentAgentBusy,
delegationTargetName: target ? workAgentDisplayName(target) : null,
});
if (stopTerminalOverrideRef.current && !currentAgentBusy && !target) {
stopTerminalOverrideRef.current = false;
setStopTerminalOverride(false);
}
} catch {
if (refreshSeq !== requesterRefreshSeqRef.current) return;
setRequesterDelegation({
currentAgentBusy: false,
delegationTargetName: null,
@ -1446,7 +1464,20 @@ export function CustomAgentChatView({
}, []);
const receive = useCallback(
(chunk: ReplyChunk) => {
(chunk: ReplyChunk, sourceInteractionId?: string) => {
if (
sourceInteractionId &&
cancelledInteractionIdsRef.current.has(sourceInteractionId)
) {
return;
}
if (!sourceInteractionId && ignoreUnscopedChunksAfterStopRef.current) {
if (chunk.kind === "userPrompt") {
ignoreUnscopedChunksAfterStopRef.current = false;
} else {
return;
}
}
setTranscriptUnavailableReason(null);
scrollbackChunkCountRef.current += 1;
const activeInteractionId = activeInteractionIdRef.current;
@ -2284,6 +2315,9 @@ export function CustomAgentChatView({
const attachmentLabels = outgoingAttachments.map((item) => item.label);
const displayText = text || "Pièce jointe";
const interactionId = `${agentId}:${++interactionSeqRef.current}`;
ignoreUnscopedChunksAfterStopRef.current = false;
stopTerminalOverrideRef.current = false;
setStopTerminalOverride(false);
activeInteractionIdRef.current = interactionId;
markProjectCliInteractionStarted(projectId, interactionId);
const finishInteraction = () => {
@ -2312,8 +2346,10 @@ export function CustomAgentChatView({
}));
const sendTurn = (sid: string) =>
attachmentInputs.length > 0
? agent.sendAgentChat!(sid, text, receive, { attachments: attachmentInputs })
: agent.sendAgentChat!(sid, text, receive);
? agent.sendAgentChat!(sid, text, (chunk) => receive(chunk, interactionId), {
attachments: attachmentInputs,
})
: agent.sendAgentChat!(sid, text, (chunk) => receive(chunk, interactionId));
try {
const sid =
currentSession ??
@ -2322,8 +2358,10 @@ export function CustomAgentChatView({
retryAttachNotFound: true,
}));
await sendTurn(sid);
if (cancelledInteractionIdsRef.current.has(interactionId)) return;
finishInteraction();
} catch (e) {
if (cancelledInteractionIdsRef.current.has(interactionId)) return;
if (isNotFound(e)) {
try {
setCurrentSession(null);
@ -2361,11 +2399,31 @@ export function CustomAgentChatView({
async function stopCurrentRequest() {
const sid = sessionRef.current;
if (!sid || !agent.cancelAgentChat || stopping) return;
if (
!sid ||
!agent.cancelAgentChat ||
stopping ||
stopInFlightRef.current ||
stopTerminalOverrideRef.current
) {
return;
}
stopInFlightRef.current = true;
ignoreUnscopedChunksAfterStopRef.current = true;
const interactionBeingCancelled = activeInteractionIdRef.current;
if (interactionBeingCancelled) {
cancelledInteractionIdsRef.current.add(interactionBeingCancelled);
}
setStopping(true);
setError(null);
try {
await agent.cancelAgentChat(sid);
const settleStoppedRequest = (alreadyStopped: boolean) => {
if (stopTerminalOverrideRef.current) return;
stopTerminalOverrideRef.current = true;
setStopTerminalOverride(true);
setRequesterDelegation({
currentAgentBusy: false,
delegationTargetName: null,
});
if (activeInteractionIdRef.current) {
markProjectCliInteractionStopped(projectId, activeInteractionIdRef.current);
activeInteractionIdRef.current = null;
@ -2380,32 +2438,28 @@ export function CustomAgentChatView({
...completed.turns,
{
role: "tool",
label: tr("Demande arrêtée.", "Request stopped."),
label: alreadyStopped
? tr("Demande déjà arrêtée.", "Request already stopped.")
: tr("Demande arrêtée.", "Request stopped."),
receivedAtMs: null,
},
],
};
});
void refreshRequesterDelegation();
};
try {
const result = await agent.cancelAgentChat(sid);
settleStoppedRequest(result.outcome !== "cancelled");
} catch (e) {
if (isNotFound(e)) {
if (activeInteractionIdRef.current) {
markProjectCliInteractionStopped(projectId, activeInteractionIdRef.current);
activeInteractionIdRef.current = null;
}
setBusy(false);
setOpening(false);
updateChatViewState((prev) => {
const completed = completeChatViewProgress(prev);
return {
...completed,
turns: [
...completed.turns,
{ role: "tool", label: tr("Demande déjà arrêtée.", "Request already stopped."), receivedAtMs: null },
],
};
});
settleStoppedRequest(true);
return;
}
if (interactionBeingCancelled) {
cancelledInteractionIdsRef.current.delete(interactionBeingCancelled);
}
ignoreUnscopedChunksAfterStopRef.current = false;
setError({ message: describe(e), openFailure: false });
updateChatViewState((prev) => ({
...prev,
@ -2419,6 +2473,7 @@ export function CustomAgentChatView({
],
}));
} finally {
stopInFlightRef.current = false;
setStopping(false);
}
}