mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-28 06:51:16 +08:00
feat(tui): delete queued message while editing with ctrl-x / cancel with esc
Today there's no way to remove a queued message — ↑ loads it for edit, ctrl-K dispatches the head, but a draft you no longer want stays put forever. ctrl-C just clears the composer and exits edit mode without touching the queue. Two new bindings, both gated on queueEditIdx !== null so they're inert when the user isn't pointing at a queue item: - ctrl-X — delete the queue item being edited, clear composer, exit edit mode. "cut" matches the mental model and doesn't collide with any existing binding. - esc — cancel the edit (composer clears, item stays in queue). Mirrors ctrl-C's existing behavior so muscle memory has two paths. Header line now reads `queued (3) · editing 2 · ⌃X delete · esc cancel` when in edit mode, so the affordance is discoverable without /help. The /help hotkey table also gets a Ctrl+X entry. ctrl-C is intentionally unchanged: it should never destroy queued content. Cancel is non-destructive (esc / ctrl-C); only ctrl-X removes the item.
This commit is contained in:
28
ui-tui/src/__tests__/useQueue.test.ts
Normal file
28
ui-tui/src/__tests__/useQueue.test.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { removeAt } from '../hooks/useQueue.js'
|
||||
|
||||
describe('removeAt', () => {
|
||||
it('removes the item at the given index in place', () => {
|
||||
const arr = ['a', 'b', 'c']
|
||||
|
||||
removeAt(arr, 1)
|
||||
expect(arr).toEqual(['a', 'c'])
|
||||
})
|
||||
|
||||
it('is a no-op when the index is out of bounds', () => {
|
||||
const arr = ['a', 'b']
|
||||
|
||||
removeAt(arr, -1)
|
||||
removeAt(arr, 5)
|
||||
expect(arr).toEqual(['a', 'b'])
|
||||
})
|
||||
|
||||
it('returns the same reference (mutates in place)', () => {
|
||||
const arr = ['x']
|
||||
const same = removeAt(arr, 0)
|
||||
|
||||
expect(same).toBe(arr)
|
||||
expect(arr).toEqual([])
|
||||
})
|
||||
})
|
||||
@@ -125,6 +125,7 @@ export interface ComposerActions {
|
||||
handleTextPaste: (event: PasteEvent) => MaybePromise<ComposerPasteResult | null>
|
||||
openEditor: () => Promise<void>
|
||||
pushHistory: (text: string) => void
|
||||
removeQueue: (index: number) => void
|
||||
replaceQueue: (index: number, text: string) => void
|
||||
setCompIdx: StateSetter<number>
|
||||
setHistoryIdx: StateSetter<null | number>
|
||||
|
||||
@@ -110,8 +110,18 @@ export function useComposerState({
|
||||
const isBlocked = useStore($isBlocked)
|
||||
const { querier } = useStdin() as { querier: Parameters<typeof readOsc52Clipboard>[0] }
|
||||
|
||||
const { queueRef, queueEditRef, queuedDisplay, queueEditIdx, enqueue, dequeue, replaceQ, setQueueEdit, syncQueue } =
|
||||
useQueue()
|
||||
const {
|
||||
queueRef,
|
||||
queueEditRef,
|
||||
queuedDisplay,
|
||||
queueEditIdx,
|
||||
enqueue,
|
||||
dequeue,
|
||||
removeQ,
|
||||
replaceQ,
|
||||
setQueueEdit,
|
||||
syncQueue
|
||||
} = useQueue()
|
||||
|
||||
const { historyRef, historyIdx, setHistoryIdx, historyDraftRef, pushHistory } = useInputHistory()
|
||||
const { completions, compIdx, setCompIdx, compReplace } = useCompletion(input, isBlocked, gw)
|
||||
@@ -294,6 +304,7 @@ export function useComposerState({
|
||||
handleTextPaste,
|
||||
openEditor,
|
||||
pushHistory,
|
||||
removeQueue: removeQ,
|
||||
replaceQueue: replaceQ,
|
||||
setCompIdx,
|
||||
setHistoryIdx,
|
||||
@@ -310,6 +321,7 @@ export function useComposerState({
|
||||
handleTextPaste,
|
||||
openEditor,
|
||||
pushHistory,
|
||||
removeQ,
|
||||
replaceQ,
|
||||
setCompIdx,
|
||||
setHistoryIdx,
|
||||
|
||||
@@ -311,6 +311,10 @@ export function useInputHandlers(ctx: InputHandlerContext): InputHandlerResult {
|
||||
return clearSelection()
|
||||
}
|
||||
|
||||
if (key.escape && cState.queueEditIdx !== null) {
|
||||
return cActions.clearIn()
|
||||
}
|
||||
|
||||
if (key.upArrow && !cState.inputBuf.length) {
|
||||
const inputSel = getInputSelection()
|
||||
const cursor = inputSel && inputSel.start === inputSel.end ? inputSel.start : null
|
||||
@@ -357,6 +361,11 @@ export function useInputHandlers(ctx: InputHandlerContext): InputHandlerResult {
|
||||
}
|
||||
}
|
||||
|
||||
if (isCtrl(key, ch, 'x') && cState.queueEditIdx !== null) {
|
||||
cActions.removeQueue(cState.queueEditIdx)
|
||||
return cActions.clearIn()
|
||||
}
|
||||
|
||||
if (key.ctrl && ch.toLowerCase() === 'c') {
|
||||
if (live.busy && live.sid) {
|
||||
return turnController.interruptTurn({
|
||||
|
||||
@@ -24,7 +24,8 @@ export function QueuedMessages({ cols, queueEditIdx, queued, t }: QueuedMessages
|
||||
return (
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
<Text color={t.color.dim} dimColor>
|
||||
queued ({queued.length}){queueEditIdx !== null ? ` · editing ${queueEditIdx + 1}` : ''}
|
||||
queued ({queued.length})
|
||||
{queueEditIdx !== null ? ` · editing ${queueEditIdx + 1} · ⌃X delete · esc cancel` : ''}
|
||||
</Text>
|
||||
|
||||
{q.showLead && (
|
||||
|
||||
@@ -23,6 +23,7 @@ export const HOTKEYS: [string, string][] = [
|
||||
[paste + '+V / /paste', 'paste text; /paste attaches clipboard image'],
|
||||
['Tab', 'apply completion'],
|
||||
['↑/↓', 'completions / queue edit / history'],
|
||||
['Ctrl+X', 'delete the queued message you’re editing (esc cancels edit)'],
|
||||
[action + '+A/E', 'home / end of line'],
|
||||
[action + '+Z / ' + action + '+Y', 'undo / redo input edits'],
|
||||
[action + '+W', 'delete word'],
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
|
||||
export function removeAt<T>(arr: T[], i: number): T[] {
|
||||
if (i < 0 || i >= arr.length) {
|
||||
return arr
|
||||
}
|
||||
|
||||
arr.splice(i, 1)
|
||||
|
||||
return arr
|
||||
}
|
||||
|
||||
export function useQueue() {
|
||||
const queueRef = useRef<string[]>([])
|
||||
const [queuedDisplay, setQueuedDisplay] = useState<string[]>([])
|
||||
@@ -36,6 +46,19 @@ export function useQueue() {
|
||||
[syncQueue]
|
||||
)
|
||||
|
||||
const removeQ = useCallback(
|
||||
(i: number) => {
|
||||
const before = queueRef.current.length
|
||||
|
||||
removeAt(queueRef.current, i)
|
||||
|
||||
if (queueRef.current.length !== before) {
|
||||
syncQueue()
|
||||
}
|
||||
},
|
||||
[syncQueue]
|
||||
)
|
||||
|
||||
return {
|
||||
dequeue,
|
||||
enqueue,
|
||||
@@ -43,6 +66,7 @@ export function useQueue() {
|
||||
queueEditRef,
|
||||
queueRef,
|
||||
queuedDisplay,
|
||||
removeQ,
|
||||
replaceQ,
|
||||
setQueueEdit,
|
||||
syncQueue
|
||||
|
||||
Reference in New Issue
Block a user