diff --git a/packages/coding-agent/src/modes/controllers/event-controller.ts b/packages/coding-agent/src/modes/controllers/event-controller.ts index 20226e3c5..9e2c22087 100644 --- a/packages/coding-agent/src/modes/controllers/event-controller.ts +++ b/packages/coding-agent/src/modes/controllers/event-controller.ts @@ -819,6 +819,18 @@ export class EventController { } } async #handleAgentEnd(_event: Extract): Promise { + // A superseded agent_end: the agent is already streaming a fresh turn, so + // this event belongs to a turn that has already been replaced. The session + // dispatches to listeners fire-and-forget across an async extension-emit hop + // (#emitSessionEvent), so an interrupted turn's agent_end can land AFTER the + // resumed turn's agent_start — e.g. empty-Enter interrupt-and-flush of a + // queued steer (interruptAndFlushQueuedMessages) or any post-turn + // agent.continue(). Running the turn-end teardown now would stop the loader + // the live turn just created, leaving "Working…" gone while the agent keeps + // running. The live turn owns the loader and finalizes it at its own + // agent_end (isStreaming === false by then). Mirrors the collab guest's + // !isStreaming loader reconciler. + if (this.ctx.session.isStreaming) return; this.#agentTurnActive = false; this.#streamingReveal.stop(); this.#toolArgsReveal.flushAll();