From d59c19f2a59ad4d455c9cc6572a4581bc8f95a86 Mon Sep 17 00:00:00 2001 From: Raymond Lowe Date: Wed, 24 Jun 2026 12:02:33 +0800 Subject: [PATCH] fix(desktop): interrupt running turn when user sends a message while busy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The desktop composer queued messages while the agent was busy but never fired an interrupt — so the agent kept running for minutes, ignoring the user's correction. The queue counter grew ('4 queued', '5 queued'...) and only ONE message drained after the turn finally ended, leaving the rest stranded. Now when the user presses Enter with a payload while busy, the composer both queues the message AND fires onCancel() to interrupt the running turn — mirroring the CLI's busy_input_mode: interrupt behaviour and the existing sendQueuedNow busy-path. The auto-drain then sends the queued message as soon as the agent reports busy=false. Files changed: - apps/desktop/src/app/chat/composer/index.tsx - apps/desktop/src/app/chat/composer/enter-submit-dom-race.test.tsx --- .../chat/composer/enter-submit-dom-race.test.tsx | 5 +++-- apps/desktop/src/app/chat/composer/index.tsx | 13 +++++++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/apps/desktop/src/app/chat/composer/enter-submit-dom-race.test.tsx b/apps/desktop/src/app/chat/composer/enter-submit-dom-race.test.tsx index 921ec485ae37..368d66832d70 100644 --- a/apps/desktop/src/app/chat/composer/enter-submit-dom-race.test.tsx +++ b/apps/desktop/src/app/chat/composer/enter-submit-dom-race.test.tsx @@ -73,6 +73,7 @@ function Harness({ if (busy) { if (payloadPresent) { onQueue(text) + onCancel() } else { onCancel() } @@ -142,7 +143,7 @@ describe('composer Enter submit — live DOM vs stale composer state (#39630)', expect(onSubmit).toHaveBeenCalledWith('hello world') }) - it('queues a fast-typed message while busy instead of draining the queue or cancelling', async () => { + it('queues a fast-typed message while busy and interrupts the running turn', async () => { const onQueue = vi.fn() const onDrain = vi.fn() const onCancel = vi.fn() @@ -158,7 +159,7 @@ describe('composer Enter submit — live DOM vs stale composer state (#39630)', expect(onQueue).toHaveBeenCalledWith('urgent follow-up') expect(onDrain).not.toHaveBeenCalled() - expect(onCancel).not.toHaveBeenCalled() + expect(onCancel).toHaveBeenCalledTimes(1) }) it('treats an empty Enter while busy as a no-op (never an accidental Stop)', async () => { diff --git a/apps/desktop/src/app/chat/composer/index.tsx b/apps/desktop/src/app/chat/composer/index.tsx index 4010f2f783e2..2b7d9dce6d08 100644 --- a/apps/desktop/src/app/chat/composer/index.tsx +++ b/apps/desktop/src/app/chat/composer/index.tsx @@ -1144,8 +1144,8 @@ export function ChatBar({ return } - // Empty Enter while busy is a no-op — interrupting is explicit (Stop/Esc), - // never a stray Enter after sending. With a payload, submitDraft queues it. + // Empty Enter while busy is a no-op — interrupting requires a payload + // or Esc/Stop. With a payload, submitDraft queues it and interrupts. // Gate on the live DOM payload (not the render-lagged composer state) so a // message typed fast / via IME while busy still reaches submitDraft() and // gets queued instead of being mistaken for an empty Enter. @@ -1721,7 +1721,16 @@ export function ChatBar({ clearDraft() dispatchSubmit(text) } else if (payloadPresent) { + // Queue the draft, then interrupt the running turn so the queued + // message drains immediately — instead of waiting for the current + // (potentially long) turn to finish while the user's correction + // sits ignored. Mirrors sendQueuedNow's busy-path: queue → onCancel() + // → auto-drain on busy→false. The CLI achieves the same via + // busy_input_mode: interrupt; the desktop app has no such knob, so + // we wire it directly here. queueCurrentDraft() + triggerHaptic('cancel') + void Promise.resolve(onCancel()) } else { // Stop button (the only way to reach here while busy with an empty // composer — empty Enter is short-circuited in the keydown handler).