Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ function Harness({
if (busy) {
if (payloadPresent) {
onQueue(text)
onCancel()
} else {
onCancel()
}
Expand Down Expand Up @@ -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()
Expand All @@ -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 () => {
Expand Down
13 changes: 11 additions & 2 deletions apps/desktop/src/app/chat/composer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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).
Expand Down