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).