From b0b335cf671cb577736d8fc260c8d6d7639bd669 Mon Sep 17 00:00:00 2001 From: etodanik Date: Tue, 3 Nov 2026 16:33:44 +0300 Subject: [PATCH] fix(web): stop thinking when the coordinator replies End the live reply on connector.reply so a missed message-end frame cannot leave the thinking indicator active. Add one regression test covering completion, text preservation, and thinking restarting for the next generation. --- apps/web/src/pages/workspace/live-run.ts | 7 +++- .../src/pages/workspace/transcript.test.tsx | 34 +++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/apps/web/src/pages/workspace/live-run.ts b/apps/web/src/pages/workspace/live-run.ts index ae0cc6b3..49c8cd69 100644 --- a/apps/web/src/pages/workspace/live-run.ts +++ b/apps/web/src/pages/workspace/live-run.ts @@ -111,7 +111,12 @@ export function foldLiveRun( reload, }; } - if (event.type === "message.run.ended" || event.type === "reactor.done") { + // A connector reply is terminal for this message even if the following end frame is missed. + if ( + event.type === "connector.reply" || + event.type === "message.run.ended" || + event.type === "reactor.done" + ) { return { run: { ...run, ended: true, running: false, endedAt: Date.now() }, reload, diff --git a/apps/web/src/pages/workspace/transcript.test.tsx b/apps/web/src/pages/workspace/transcript.test.tsx index d62c89d2..be6d498b 100644 --- a/apps/web/src/pages/workspace/transcript.test.tsx +++ b/apps/web/src/pages/workspace/transcript.test.tsx @@ -7,6 +7,7 @@ import { awaitingCommit, emptyLiveRun, foldLiveRun, + pendingLiveTurn, waitingOnPerson, } from "./live-run.ts"; import { RoomThread } from "./transcript.tsx"; @@ -164,6 +165,39 @@ describe("conversation transcript", () => { ); }); + test("a connector reply stops thinking without an end frame and keeps text until it is committed", () => { + const started = foldLiveRun( + emptyLiveRun, + { type: "inference.start" }, + null, + ).run; + const generated = foldLiveRun( + started, + { + type: "inference.text.delta", + data: { partial: { text: "The constraints are ready." } }, + }, + null, + ).run; + const done = foldLiveRun(generated, { type: "inference.done" }, null).run; + expect(done.running).toBe(true); + const replied = foldLiveRun(done, { type: "connector.reply" }, null); + expect(replied.reload).toBe(true); + expect(replied.run.running).toBe(false); + expect(replied.run.ended).toBe(true); + expect(replied.run.endedAt).not.toBeNull(); + expect(pendingLiveTurn(replied.run, [])?.text).toBe( + "The constraints are ready.", + ); + const committed = [ + { role: "assistant" as const, text: "The constraints are ready.", at: 1 }, + ]; + expect(pendingLiveTurn(replied.run, committed)).toBeNull(); + const next = foldLiveRun(replied.run, { type: "inference.start" }, 1).run; + expect(next.running).toBe(true); + expect(next.ended).toBe(false); + }); + test("an inference call that finishes with tool work settles even while the message run stays open on a park", () => { const started = foldLiveRun( emptyLiveRun,