mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-07 08:02:23 +00:00
fix(tui): allow transcript scroll + Esc during approval/clarify/confirm prompts (#26414)
When an approval / clarify / confirm overlay was active, the global input
handler in useInputHandlers returned for every key that wasn't Ctrl+C, which
silently disabled transcript scrolling. On long threads the context the
prompt was asking about often lived above the visible viewport, and being
unable to scroll while answering felt like the prompt had locked the UI.
ApprovalPrompt also had no Esc handler at all, so the one obvious 'abort'
key did nothing during a permission prompt and the user had to memorize
Ctrl+C or hunt for the deny number.
Fixes:
- Extract shouldFallThroughForScroll(key) (pure, exported) covering wheel
scrolls, PageUp/PageDown, and Shift+ArrowUp/Down. When a prompt overlay
is up and the pressed key is a scroll input, skip the early return so it
reaches the existing wheel/PageUp/Shift+arrow handlers below. Plain
arrows still drive in-prompt selection — they don't fall through.
- ApprovalPrompt now maps Esc to onChoice('deny'), parity with the global
Ctrl+C cancellation path that already invokes cancelOverlayFromCtrlC()
for approvals. The bottom-of-prompt hint now advertises 'Esc/Ctrl+C deny'.
- Extract approvalAction(ch, key, sel) — pure key-dispatch helper for the
approval prompt, exported so the regression matrix (Esc, numbers, Enter,
arrows, edge clamping, precedence) is testable without mounting Ink.
Tests:
- useInputHandlers.test.ts: 6 cases covering shouldFallThroughForScroll
positives (wheel/PageUp/PageDown/Shift+arrows) and negatives (plain
arrows, bare shift, no scroll key).
- approvalAction.test.ts: 8 cases covering Esc→deny, numeric mapping,
Enter, ↑↓ within bounds, edge clamping, Esc-beats-others precedence,
unrelated keystrokes.
This commit is contained in:
parent
97a32afdc4
commit
44b63fc6de
4 changed files with 201 additions and 21 deletions
|
|
@ -11,28 +11,65 @@ const OPTS = ['once', 'session', 'always', 'deny'] as const
|
|||
const LABELS = { always: 'Always allow', deny: 'Deny', once: 'Allow once', session: 'Allow this session' } as const
|
||||
const CMD_PREVIEW_LINES = 10
|
||||
|
||||
type ApprovalKey = {
|
||||
downArrow?: boolean
|
||||
escape?: boolean
|
||||
return?: boolean
|
||||
upArrow?: boolean
|
||||
}
|
||||
|
||||
type ApprovalAction =
|
||||
| { kind: 'choose'; choice: (typeof OPTS)[number] }
|
||||
| { kind: 'move'; delta: -1 | 1 }
|
||||
| { kind: 'noop' }
|
||||
|
||||
/**
|
||||
* Pure key-dispatch for the approval prompt — exported so the regression
|
||||
* matrix (Esc, Ctrl+C-equivalent, number keys, Enter, ↑↓) is testable
|
||||
* without mounting React + Ink + a fake stdin. The component just maps the
|
||||
* action onto its own state setters.
|
||||
*
|
||||
* Esc and number keys both terminate the prompt; Esc maps to deny (parity
|
||||
* with the global Ctrl+C handler that already calls cancelOverlayFromCtrlC
|
||||
* for approvals). Numbers 1..OPTS.length pick the labelled choice. Enter
|
||||
* confirms the current selection. ↑/↓ moves the selection within bounds.
|
||||
*/
|
||||
export function approvalAction(ch: string, key: ApprovalKey, sel: number): ApprovalAction {
|
||||
if (key.escape) {
|
||||
return { kind: 'choose', choice: 'deny' }
|
||||
}
|
||||
|
||||
const n = parseInt(ch, 10)
|
||||
|
||||
if (n >= 1 && n <= OPTS.length) {
|
||||
return { kind: 'choose', choice: OPTS[n - 1]! }
|
||||
}
|
||||
|
||||
if (key.return) {
|
||||
return { kind: 'choose', choice: OPTS[sel]! }
|
||||
}
|
||||
|
||||
if (key.upArrow && sel > 0) {
|
||||
return { kind: 'move', delta: -1 }
|
||||
}
|
||||
|
||||
if (key.downArrow && sel < OPTS.length - 1) {
|
||||
return { kind: 'move', delta: 1 }
|
||||
}
|
||||
|
||||
return { kind: 'noop' }
|
||||
}
|
||||
|
||||
export function ApprovalPrompt({ onChoice, req, t }: ApprovalPromptProps) {
|
||||
const [sel, setSel] = useState(0)
|
||||
|
||||
useInput((ch, key) => {
|
||||
if (key.upArrow && sel > 0) {
|
||||
setSel(s => s - 1)
|
||||
}
|
||||
const action = approvalAction(ch, key, sel)
|
||||
|
||||
if (key.downArrow && sel < OPTS.length - 1) {
|
||||
setSel(s => s + 1)
|
||||
}
|
||||
|
||||
const n = parseInt(ch, 10)
|
||||
|
||||
if (n >= 1 && n <= OPTS.length) {
|
||||
onChoice(OPTS[n - 1]!)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (key.return) {
|
||||
onChoice(OPTS[sel]!)
|
||||
if (action.kind === 'choose') {
|
||||
onChoice(action.choice)
|
||||
} else if (action.kind === 'move') {
|
||||
setSel(s => s + action.delta)
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -71,7 +108,7 @@ export function ApprovalPrompt({ onChoice, req, t }: ApprovalPromptProps) {
|
|||
</Text>
|
||||
))}
|
||||
|
||||
<Text color={t.color.muted}>↑/↓ select · Enter confirm · 1-4 quick pick · Ctrl+C deny</Text>
|
||||
<Text color={t.color.muted}>↑/↓ select · Enter confirm · 1-4 quick pick · Esc/Ctrl+C deny</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue