diff --git a/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts b/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts index 2cde405ad..c2c43d9fa 100644 --- a/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts +++ b/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts @@ -1143,6 +1143,7 @@ describe("ProviderCommandReactor", () => { await waitFor(() => harness.interruptTurn.mock.calls.length === 1); expect(harness.interruptTurn.mock.calls[0]?.[0]).toEqual({ threadId: "thread-1", + turnId: "turn-1", }); }); diff --git a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts index a6f92c74c..dead00300 100644 --- a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts +++ b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts @@ -715,7 +715,10 @@ const make = Effect.gen(function* () { } // Orchestration turn ids are not provider turn ids, so interrupt by session. - yield* providerService.interruptTurn({ threadId: event.payload.threadId }); + yield* providerService.interruptTurn({ + threadId: event.payload.threadId, + ...(event.payload.turnId !== undefined ? { turnId: event.payload.turnId } : {}), + }); }); const processApprovalResponseRequested = Effect.fnUntraced(function* ( diff --git a/apps/web/src/components/ChatView.browser.tsx b/apps/web/src/components/ChatView.browser.tsx index f02665a5c..e6147f1f3 100644 --- a/apps/web/src/components/ChatView.browser.tsx +++ b/apps/web/src/components/ChatView.browser.tsx @@ -1598,6 +1598,42 @@ describe("ChatView timeline estimator parity (full app)", () => { } }); + it("includes the active turn id when stopping a running turn", async () => { + wsRequests.length = 0; + + const mounted = await mountChatView({ + viewport: DEFAULT_VIEWPORT, + snapshot: createSnapshotForTargetUser({ + targetMessageId: "msg-user-stop-button-turn-id" as MessageId, + targetText: "stop button turn id target", + sessionStatus: "running", + activeTurnId: "turn-stop-button-turn-id" as TurnId, + }), + }); + + try { + const stopButton = await waitForElement( + () => document.querySelector('button[aria-label="Stop generation"]'), + "Unable to find stop generation button.", + ); + + stopButton.click(); + + await vi.waitFor( + () => + wsRequests.some( + (request) => + request._tag === ORCHESTRATION_WS_METHODS.dispatchCommand && + request.type === "thread.turn.interrupt" && + request.turnId === "turn-stop-button-turn-id", + ), + { timeout: 8_000, interval: 16 }, + ); + } finally { + await mounted.cleanup(); + } + }); + it("keeps the new thread selected after clicking the new-thread button", async () => { const mounted = await mountChatView({ viewport: DEFAULT_VIEWPORT, diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 72dbcf66e..6d1ee65cc 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -3867,6 +3867,10 @@ export default function ChatView({ type: "thread.turn.interrupt", commandId: newCommandId(), threadId: activeThread.id, + ...(activeThread.session?.activeTurnId !== undefined && + activeThread.session?.activeTurnId !== null + ? { turnId: activeThread.session.activeTurnId } + : {}), createdAt: new Date().toISOString(), }); };