From 84d77b608c4a217b3624bc2bfb0d493f3ff9f4e3 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Mon, 27 Jul 2026 22:42:21 -0500 Subject: [PATCH 1/2] feat(desktop): double-Enter sends the queued turn, and the row says Enter Empty Enter while busy was a hard no-op, so a queued turn could only be sent early via the panel's send arrow or Cmd+Shift+K. With prompts queued, a second Enter now promotes the head and interrupts, mirroring the idle empty-Enter drain. Nothing queued keeps the old no-op. The panel's send-now row action switches from a bare up-arrow to the return glyph, so the row states the keybind the double-send uses. --- apps/desktop/src/app/chat/composer/index.tsx | 19 ++++++++++++++----- .../src/app/chat/composer/queue-panel.tsx | 4 ++-- apps/desktop/src/lib/icons.ts | 2 ++ 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/apps/desktop/src/app/chat/composer/index.tsx b/apps/desktop/src/app/chat/composer/index.tsx index 18aa1b97801a..21df163ef8fc 100644 --- a/apps/desktop/src/app/chat/composer/index.tsx +++ b/apps/desktop/src/app/chat/composer/index.tsx @@ -724,12 +724,21 @@ 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. - // 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. + // Empty Enter while busy. With prompts queued this is the double-send: + // the first Enter put the words in the queue, a second sends them now + // (promote + interrupt + drain on settle), mirroring the idle empty-Enter + // drain above. With nothing queued it stays a no-op — interrupting is + // explicit (Stop/Esc), never a stray Enter after sending. 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. if (busy && !hasLivePayload) { + const head = queuedPrompts.find(entry => entry.id !== queueEdit?.entryId) + + if (head) { + sendQueuedNow(head.id) + } + return } diff --git a/apps/desktop/src/app/chat/composer/queue-panel.tsx b/apps/desktop/src/app/chat/composer/queue-panel.tsx index 6bb04ec23c4e..591eeb10ebbe 100644 --- a/apps/desktop/src/app/chat/composer/queue-panel.tsx +++ b/apps/desktop/src/app/chat/composer/queue-panel.tsx @@ -4,7 +4,7 @@ import { Button } from '@/components/ui/button' import { Codicon } from '@/components/ui/codicon' import { Tip } from '@/components/ui/tooltip' import { type Translations, useI18n } from '@/i18n' -import { ArrowUp, iconSize, Pencil, Trash2 } from '@/lib/icons' +import { CornerDownLeft, iconSize, Pencil, Trash2 } from '@/lib/icons' import { cn } from '@/lib/utils' import type { QueuedPromptEntry } from '@/store/composer-queue' @@ -103,7 +103,7 @@ export function QueuePanel({ type="button" variant="ghost" > - + diff --git a/apps/desktop/src/lib/icons.ts b/apps/desktop/src/lib/icons.ts index 1fda821b796e..35e6a92237dc 100644 --- a/apps/desktop/src/lib/icons.ts +++ b/apps/desktop/src/lib/icons.ts @@ -33,6 +33,7 @@ import { IconCommand as Command, IconCopy as Copy, IconCopy as CopyIcon, + IconCornerDownLeft as CornerDownLeft, IconCpu as Cpu, IconCreditCard as CreditCard, IconDownload as Download, @@ -155,6 +156,7 @@ export { Command, Copy, CopyIcon, + CornerDownLeft, Cpu, CreditCard, Download, From ac8310bcc29606a321f032676b2a49a0eff92e1d Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Mon, 27 Jul 2026 22:43:42 -0500 Subject: [PATCH 2/2] test(desktop): cover the busy empty-Enter double-send --- .../composer/enter-submit-dom-race.test.tsx | 51 +++++++++++++++++-- 1 file changed, 48 insertions(+), 3 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 ff01bf6fd377..5e914dc194b3 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 @@ -29,7 +29,8 @@ function Harness({ onSubmit, onQueue, onCancel, - onDrain + onDrain, + onSendNow }: { busy?: boolean disabled?: boolean @@ -38,6 +39,7 @@ function Harness({ onQueue: (text: string) => void onCancel: () => void onDrain: () => void + onSendNow?: (id: string) => void }) { const editorRef = useRef(null) const draftRef = useRef('') @@ -103,6 +105,12 @@ function Harness({ } if (busy && !hasLivePayload) { + const head = queued[0] + + if (head) { + onSendNow?.(head) + } + return } @@ -167,13 +175,21 @@ describe('composer Enter submit — live DOM vs stale composer state (#39630)', expect(onCancel).not.toHaveBeenCalled() }) - it('treats an empty Enter while busy as a no-op (never an accidental Stop)', async () => { + it('treats an empty Enter while busy with nothing queued as a no-op (never an accidental Stop)', async () => { const onCancel = vi.fn() const onSubmit = vi.fn() const onQueue = vi.fn() + const onSendNow = vi.fn() const { getByTestId } = render( - + ) const editor = getByTestId('editor') @@ -186,6 +202,35 @@ describe('composer Enter submit — live DOM vs stale composer state (#39630)', expect(onCancel).not.toHaveBeenCalled() expect(onSubmit).not.toHaveBeenCalled() expect(onQueue).not.toHaveBeenCalled() + expect(onSendNow).not.toHaveBeenCalled() + }) + + it('double-send: an empty Enter while busy with a queued turn sends that turn now', async () => { + const onCancel = vi.fn() + const onSendNow = vi.fn() + + const { getByTestId } = render( + + ) + + const editor = getByTestId('editor') + + await act(async () => { + editor.textContent = '' + fireEvent.keyDown(editor, { key: 'Enter' }) + }) + + // Head of the queue, and NOT a bare cancel — send-now promotes + interrupts. + expect(onSendNow).toHaveBeenCalledWith('queued-1') + expect(onCancel).not.toHaveBeenCalled() }) it('drains the next queued prompt on Enter when idle with a truly empty editor', async () => {