From 6df056cb604a41c683de4e7d98a43c43be58b3a9 Mon Sep 17 00:00:00 2001 From: Guilhem Lemouel Date: Wed, 16 Sep 2026 13:20:39 +0200 Subject: [PATCH] fix(chat): stop a send whose panel went away while it uploaded MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A send waits on its attachments before it has a run to be stopped, and the panel can be replaced while it waits — the chat is keyed on its flow and workspace, and changing either builds a new one. The upload resolved regardless and the send carried on, starting a run against a manager nobody was reading, with the conversation and arguments of the chat that had gone. The panel tells the host it is gone, and a send that finds it gone stops where Stop would have stopped it. Co-Authored-By: Claude Opus 5 (1M context) --- .../conversations/FlowChatInterface.svelte | 4 ++++ .../conversations/flowChatViewHost.svelte.ts | 19 ++++++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/frontend/src/lib/components/flows/conversations/FlowChatInterface.svelte b/frontend/src/lib/components/flows/conversations/FlowChatInterface.svelte index fe3cd4fcda..7a35e6ea1f 100644 --- a/frontend/src/lib/components/flows/conversations/FlowChatInterface.svelte +++ b/frontend/src/lib/components/flows/conversations/FlowChatInterface.svelte @@ -184,6 +184,10 @@ }) setChatViewHost(chatHost) + // The panel is replaced rather than re-pointed when its flow or workspace changes, so a + // send still waiting on its uploads has to be told this one is gone before it carries on. + $effect(() => () => chatHost.dispose()) + // What the Configure-inputs modal asks for: every flow input the composer does not // edit itself. Below the host, because whether the paperclip is offered is its answer. const modalSchema = $derived.by(() => { diff --git a/frontend/src/lib/components/flows/conversations/flowChatViewHost.svelte.ts b/frontend/src/lib/components/flows/conversations/flowChatViewHost.svelte.ts index 9476f9edcf..012d88592a 100644 --- a/frontend/src/lib/components/flows/conversations/flowChatViewHost.svelte.ts +++ b/frontend/src/lib/components/flows/conversations/flowChatViewHost.svelte.ts @@ -329,6 +329,21 @@ export class FlowChatViewHost implements ChatViewHost { */ #abortedSends = new Set() + /** The panel this belongs to is gone. */ + #gone = false + + /** + * The panel went away, so nothing this host started may still act. + * + * A send waits on its uploads before it has a run, and the panel can be replaced while it + * waits — the chat is keyed on its flow and workspace, and changing either builds a new + * one. The upload resolves regardless, and without this the send would carry on into a + * manager nobody is reading, starting a run against the flow that replaced this one. + */ + dispose() { + this.#gone = true + } + /** * Give a spent draft back after a send that did not run. The composer took it before * calling, so something has to. @@ -455,7 +470,9 @@ export class FlowChatViewHost implements ChatViewHost { } // Stop pressed while the upload ran has no job to cancel yet, so it is honoured // here — the run has not started, and starting it now would execute a message the - // reader already took back. + // reader already took back. The panel going away is the same: nothing is left to + // run the turn for, and the composer it would be handed back to is gone too. + if (this.#gone) return false if (this.#abortedSends.delete(conversationId)) { this.#restoreToComposer({ ...options, conversationId }) return false