mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-29 18:46:59 +00:00
Merge pull request #73101 from NousResearch/bb/queue-double-send
feat(desktop): double-Enter sends the queued turn
This commit is contained in:
commit
56aacbdb01
4 changed files with 66 additions and 10 deletions
|
|
@ -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<HTMLDivElement>(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(
|
||||
<Harness busy onCancel={onCancel} onDrain={vi.fn()} onQueue={onQueue} onSubmit={onSubmit} />
|
||||
<Harness
|
||||
busy
|
||||
onCancel={onCancel}
|
||||
onDrain={vi.fn()}
|
||||
onQueue={onQueue}
|
||||
onSendNow={onSendNow}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
)
|
||||
|
||||
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(
|
||||
<Harness
|
||||
busy
|
||||
onCancel={onCancel}
|
||||
onDrain={vi.fn()}
|
||||
onQueue={vi.fn()}
|
||||
onSendNow={onSendNow}
|
||||
onSubmit={vi.fn()}
|
||||
queued={['queued-1', 'queued-2']}
|
||||
/>
|
||||
)
|
||||
|
||||
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 () => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
>
|
||||
<ArrowUp className={iconSize.xs} />
|
||||
<CornerDownLeft className={iconSize.xs} />
|
||||
</Button>
|
||||
</Tip>
|
||||
<Tip label={c.queueDelete}>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue