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:
@ -226,7 +226,15 @@ describe("TauriAgentGateway invoke payloads", () => {
|
||||
});
|
||||
|
||||
it("cancelAgentChat invokes cancel_agent_chat without closing the session", async () => {
|
||||
await new TauriAgentGateway().cancelAgentChat("chat-session-1");
|
||||
const response = {
|
||||
sessionId: "chat-session-1",
|
||||
outcome: "cancelled" as const,
|
||||
};
|
||||
invoke.mockResolvedValueOnce(response);
|
||||
|
||||
await expect(
|
||||
new TauriAgentGateway().cancelAgentChat("chat-session-1"),
|
||||
).resolves.toEqual(response);
|
||||
|
||||
expect(invoke).toHaveBeenCalledWith("cancel_agent_chat", {
|
||||
sessionId: "chat-session-1",
|
||||
|
||||
@ -17,6 +17,7 @@ import { Channel, invoke } from "@tauri-apps/api/core";
|
||||
import type {
|
||||
Agent,
|
||||
AgentContextDocument,
|
||||
CancelAgentChatResult,
|
||||
EffortSelection,
|
||||
GatewayError,
|
||||
ExecuteSlashCommandResult,
|
||||
@ -364,8 +365,8 @@ export class TauriAgentGateway implements AgentGateway {
|
||||
await invoke("close_agent_session", { sessionId });
|
||||
}
|
||||
|
||||
async cancelAgentChat(sessionId: string): Promise<void> {
|
||||
await invoke("cancel_agent_chat", { sessionId });
|
||||
async cancelAgentChat(sessionId: string): Promise<CancelAgentChatResult> {
|
||||
return invoke<CancelAgentChatResult>("cancel_agent_chat", { sessionId });
|
||||
}
|
||||
|
||||
async reattach(
|
||||
|
||||
@ -308,11 +308,16 @@ describe("HttpAgentGateway WS round-trip (B6 frames)", () => {
|
||||
baseUrl: "https://host",
|
||||
fetchImpl: async (url, init) => {
|
||||
calls.push({ url, init });
|
||||
const command = JSON.parse((init as { body: string }).body).command;
|
||||
const response =
|
||||
command === "cancel_agent_chat"
|
||||
? { sessionId: "chat-1", outcome: "cancelled" }
|
||||
: undefined;
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => undefined,
|
||||
text: async () => "",
|
||||
json: async () => response,
|
||||
text: async () => response ? JSON.stringify(response) : "",
|
||||
};
|
||||
},
|
||||
}),
|
||||
@ -402,7 +407,10 @@ describe("HttpAgentGateway WS round-trip (B6 frames)", () => {
|
||||
});
|
||||
expect(chunks).toEqual([{ kind: "final", content: "Hello back" }]);
|
||||
|
||||
await gw.cancelAgentChat("chat-1");
|
||||
await expect(gw.cancelAgentChat("chat-1")).resolves.toEqual({
|
||||
sessionId: "chat-1",
|
||||
outcome: "cancelled",
|
||||
});
|
||||
await gw.closeAgentChat("chat-1");
|
||||
await gw.detachAgentChat("chat-1");
|
||||
|
||||
|
||||
@ -23,6 +23,7 @@ import type {
|
||||
Agent,
|
||||
AgentContextDocument,
|
||||
AppExitWorkGuardState,
|
||||
CancelAgentChatResult,
|
||||
DomainEvent,
|
||||
EffortSelection,
|
||||
ExecuteSlashCommandResult,
|
||||
@ -469,8 +470,8 @@ export class HttpAgentGateway implements AgentGateway {
|
||||
await this.http.invoke("close_agent_session", { sessionId });
|
||||
}
|
||||
|
||||
async cancelAgentChat(sessionId: string): Promise<void> {
|
||||
await this.http.invoke("cancel_agent_chat", { sessionId });
|
||||
async cancelAgentChat(sessionId: string): Promise<CancelAgentChatResult> {
|
||||
return this.http.invoke<CancelAgentChatResult>("cancel_agent_chat", { sessionId });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -9,6 +9,7 @@ import type {
|
||||
AgentDrift,
|
||||
AppExitWorkGuardState,
|
||||
AgentProfile,
|
||||
CancelAgentChatResult,
|
||||
DiagnosticWarning,
|
||||
DomainEvent,
|
||||
EffortSelection,
|
||||
@ -1133,13 +1134,14 @@ export class MockAgentGateway implements AgentGateway {
|
||||
}
|
||||
}
|
||||
|
||||
async cancelAgentChat(sessionId: string): Promise<void> {
|
||||
async cancelAgentChat(sessionId: string): Promise<CancelAgentChatResult> {
|
||||
if (!this.chatScrollback.has(sessionId)) {
|
||||
throw {
|
||||
code: "NOT_FOUND",
|
||||
message: `agent chat session ${sessionId} not found`,
|
||||
} as GatewayError;
|
||||
}
|
||||
return { sessionId, outcome: "cancelled" };
|
||||
}
|
||||
|
||||
async reattach(
|
||||
|
||||
@ -1937,6 +1937,17 @@ export interface ExecuteSlashCommandResult {
|
||||
effect: SlashCommandEffect;
|
||||
}
|
||||
|
||||
/** Idempotent outcome of cancelling the current turn of a structured chat. */
|
||||
export type CancelAgentChatOutcome =
|
||||
| "cancelled"
|
||||
| "alreadyCancelled"
|
||||
| "alreadyCompleted";
|
||||
|
||||
export interface CancelAgentChatResult {
|
||||
sessionId: string;
|
||||
outcome: CancelAgentChatOutcome;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Paired devices + pairing code (ticket #77)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@ -3,7 +3,7 @@ import { useState } from "react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { DIProvider } from "@/app/di";
|
||||
import type { AgentProfile, ReplyChunk } from "@/domain";
|
||||
import type { AgentProfile, CancelAgentChatResult, ReplyChunk } from "@/domain";
|
||||
import { MockSystemGateway, MockWorkStateGateway } from "@/adapters/mock";
|
||||
import { AnnouncementsProvider } from "@/features/announcements";
|
||||
import { PluginRuntimeProvider } from "@/features/plugins";
|
||||
@ -1562,7 +1562,23 @@ describe("CustomAgentChatView", () => {
|
||||
});
|
||||
|
||||
it("cancels only the current request and keeps the structured session usable", async () => {
|
||||
let resolveCancel: () => void = () => {};
|
||||
let resolveCancel: (result: CancelAgentChatResult) => void = () => {};
|
||||
let firstTurnChunk: (chunk: ReplyChunk) => void = () => {};
|
||||
let rejectFirstTurn: (reason: unknown) => void = () => {};
|
||||
const system = new MockSystemGateway();
|
||||
const workState = new MockWorkStateGateway();
|
||||
workState._setProjectWorkState("project-1", {
|
||||
agents: [
|
||||
{
|
||||
agentId: "agent-1",
|
||||
name: "Worker",
|
||||
profileId: "structured",
|
||||
busy: { state: "idle" },
|
||||
tickets: [],
|
||||
},
|
||||
],
|
||||
conversations: [],
|
||||
});
|
||||
const agent = {
|
||||
launchAgentChat: vi.fn(),
|
||||
reattachAgentChat: vi.fn(async (sessionId: string) => ({
|
||||
@ -1571,14 +1587,19 @@ describe("CustomAgentChatView", () => {
|
||||
})),
|
||||
sendAgentChat: vi
|
||||
.fn()
|
||||
.mockImplementationOnce(() => new Promise<void>(() => {}))
|
||||
.mockImplementationOnce((_sessionId: string, _prompt: string, onChunk) => {
|
||||
firstTurnChunk = onChunk;
|
||||
return new Promise<void>((_resolve, reject) => {
|
||||
rejectFirstTurn = reject;
|
||||
});
|
||||
})
|
||||
.mockImplementationOnce(async (_sessionId: string, _prompt: string, onChunk) => {
|
||||
onChunk({ kind: "final", content: "Next answer" });
|
||||
}),
|
||||
stopLiveAgent: vi.fn(),
|
||||
cancelAgentChat: vi.fn(
|
||||
() =>
|
||||
new Promise<void>((resolve) => {
|
||||
new Promise<CancelAgentChatResult>((resolve) => {
|
||||
resolveCancel = resolve;
|
||||
}),
|
||||
),
|
||||
@ -1590,7 +1611,8 @@ describe("CustomAgentChatView", () => {
|
||||
<DIProvider
|
||||
gateways={{
|
||||
agent,
|
||||
system: { pickFile: vi.fn(async () => null) },
|
||||
system,
|
||||
workState,
|
||||
} as unknown as Gateways}
|
||||
>
|
||||
<CustomAgentChatView
|
||||
@ -1630,6 +1652,32 @@ describe("CustomAgentChatView", () => {
|
||||
expect(hasProjectCliInteraction("project-1")).toBe(true);
|
||||
expect(screen.queryByRole("button", { name: "Cancel" })).toBeNull();
|
||||
|
||||
act(() => {
|
||||
firstTurnChunk({
|
||||
kind: "progress",
|
||||
progress: {
|
||||
source: "providerNative",
|
||||
kind: "turn",
|
||||
stage: "started",
|
||||
label: "Tour actif",
|
||||
nativeEvent: "item.started",
|
||||
},
|
||||
});
|
||||
});
|
||||
workState._setProjectWorkState("project-1", {
|
||||
agents: [
|
||||
{
|
||||
agentId: "agent-1",
|
||||
name: "Worker",
|
||||
profileId: "structured",
|
||||
busy: { state: "busy", ticket: "turn-1", sinceMs: 1 },
|
||||
tickets: [],
|
||||
},
|
||||
],
|
||||
conversations: [],
|
||||
});
|
||||
system.emit({ type: "agentBusyChanged", agentId: "agent-1", busy: true });
|
||||
|
||||
fireEvent.click(await screen.findByRole("button", { name: "Stop" }));
|
||||
|
||||
const stoppingLabel = await screen.findByText("Stopping...");
|
||||
@ -1639,8 +1687,29 @@ describe("CustomAgentChatView", () => {
|
||||
expect(agent.cancelAgentChat).toHaveBeenCalledWith("chat-session-1"),
|
||||
);
|
||||
expect(agent.cancelAgentChat).toHaveBeenCalledTimes(1);
|
||||
resolveCancel();
|
||||
resolveCancel({ sessionId: "chat-session-1", outcome: "cancelled" });
|
||||
await waitFor(() => expect(screen.getByText("Demande arrêtée.")).toBeTruthy());
|
||||
expect(screen.queryByRole("button", { name: "Stop" })).toBeNull();
|
||||
expect(screen.getByRole("button", { name: "Envoyer" })).toBeTruthy();
|
||||
act(() => {
|
||||
firstTurnChunk({
|
||||
kind: "progress",
|
||||
progress: {
|
||||
source: "providerNative",
|
||||
kind: "turn",
|
||||
stage: "completed",
|
||||
label: "Fin tardive à ignorer",
|
||||
nativeEvent: "item.completed",
|
||||
},
|
||||
});
|
||||
firstTurnChunk({ kind: "final", content: "Réponse tardive à ignorer" });
|
||||
rejectFirstTurn({ code: "PROCESS", message: "cancelled turn ended" });
|
||||
});
|
||||
await act(async () => Promise.resolve());
|
||||
expect(screen.queryByText("Fin tardive à ignorer")).toBeNull();
|
||||
expect(screen.queryByText("Réponse tardive à ignorer")).toBeNull();
|
||||
expect(screen.queryByText(/cancelled turn ended/)).toBeNull();
|
||||
expect(screen.getAllByText("Demande arrêtée.")).toHaveLength(1);
|
||||
expect(hasProjectCliInteraction("project-1")).toBe(false);
|
||||
expect(onSessionId).not.toHaveBeenCalledWith(null);
|
||||
expect(agent.stopLiveAgent).not.toHaveBeenCalled();
|
||||
@ -1663,6 +1732,74 @@ describe("CustomAgentChatView", () => {
|
||||
expect(await screen.findByText("Next answer")).toBeTruthy();
|
||||
});
|
||||
|
||||
it.each(["alreadyCancelled", "alreadyCompleted"] as const)(
|
||||
"treats backend outcome %s as one already-stopped terminal result",
|
||||
async (outcome) => {
|
||||
let reattachedChunk: (chunk: ReplyChunk) => void = () => {};
|
||||
const system = new MockSystemGateway();
|
||||
const workState = new MockWorkStateGateway();
|
||||
workState._setProjectWorkState("project-1", {
|
||||
agents: [
|
||||
{
|
||||
agentId: "agent-1",
|
||||
name: "Worker",
|
||||
profileId: "structured",
|
||||
busy: { state: "busy", ticket: "turn-1", sinceMs: 1 },
|
||||
tickets: [],
|
||||
},
|
||||
],
|
||||
conversations: [],
|
||||
});
|
||||
const agent = {
|
||||
launchAgentChat: vi.fn(),
|
||||
reattachAgentChat: vi.fn(
|
||||
async (sessionId: string, onChunk: (chunk: ReplyChunk) => void) => {
|
||||
reattachedChunk = onChunk;
|
||||
return { sessionId, scrollback: [] };
|
||||
},
|
||||
),
|
||||
sendAgentChat: vi.fn(async () => {}),
|
||||
cancelAgentChat: vi.fn(async () => ({
|
||||
sessionId: "chat-session-1",
|
||||
outcome,
|
||||
})),
|
||||
closeAgentChat: vi.fn(async () => {}),
|
||||
};
|
||||
|
||||
render(
|
||||
<DIProvider
|
||||
gateways={{ agent, system, workState } as unknown as Gateways}
|
||||
>
|
||||
<CustomAgentChatView
|
||||
projectId="project-1"
|
||||
agentId="agent-1"
|
||||
agentName="Worker"
|
||||
profile={profile}
|
||||
cwd="/repo"
|
||||
nodeId="node-1"
|
||||
sessionId="chat-session-1"
|
||||
conversationId="conversation-1"
|
||||
onSessionId={vi.fn()}
|
||||
onConversationId={vi.fn()}
|
||||
/>
|
||||
</DIProvider>,
|
||||
);
|
||||
|
||||
fireEvent.click(await screen.findByRole("button", { name: "Stop" }));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.getAllByText("Demande déjà arrêtée.")).toHaveLength(1),
|
||||
);
|
||||
expect(screen.queryByRole("button", { name: "Stop" })).toBeNull();
|
||||
expect(screen.getByTestId("custom-agent-chat-view")).toBeTruthy();
|
||||
expect(agent.closeAgentChat).not.toHaveBeenCalled();
|
||||
act(() => {
|
||||
reattachedChunk({ kind: "final", content: "Late reattached answer" });
|
||||
});
|
||||
expect(screen.queryByText("Late reattached answer")).toBeNull();
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps the composer locked from canonical work-state busy even without local stream activity", async () => {
|
||||
const system = new MockSystemGateway();
|
||||
const workState = new MockWorkStateGateway();
|
||||
@ -5625,6 +5762,9 @@ describe("CustomAgentChatView", () => {
|
||||
fireEvent.click(await screen.findByRole("button", { name: "Stop" }));
|
||||
|
||||
expect(await screen.findByText("Demande déjà arrêtée.")).toBeTruthy();
|
||||
expect(screen.getAllByText("Demande déjà arrêtée.")).toHaveLength(1);
|
||||
expect(screen.queryByRole("button", { name: "Stop" })).toBeNull();
|
||||
expect(screen.getByTestId("custom-agent-chat-view")).toBeTruthy();
|
||||
expect(onSessionId).not.toHaveBeenCalledWith(null);
|
||||
expect(agent.cancelAgentChat).toHaveBeenCalledWith("gone-session");
|
||||
expect(agent.stopLiveAgent).not.toHaveBeenCalled();
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,6 +13,7 @@ import type {
|
||||
AgentDrift,
|
||||
AgentProfile,
|
||||
AppExitWorkGuardState,
|
||||
CancelAgentChatResult,
|
||||
CustomProviderConfig,
|
||||
DomainEvent,
|
||||
EffortSelection,
|
||||
@ -398,7 +399,7 @@ export interface AgentGateway {
|
||||
/** Clears a persisted custom chat draft. */
|
||||
clearChatDraft?(scope: ChatDraftScope): Promise<void>;
|
||||
/** Interrupts only the current turn of a live structured session. */
|
||||
cancelAgentChat?(sessionId: string): Promise<void>;
|
||||
cancelAgentChat?(sessionId: string): Promise<CancelAgentChatResult>;
|
||||
/** Shuts a live structured session down. */
|
||||
closeAgentChat?(sessionId: string): Promise<void>;
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user