Skip to content

TUI approval overlay freezes terminal — useInput handlers compete, keystrokes never reach ApprovalPrompt #13618

@frannunpal

Description

@frannunpal

Bug: TUI Approval Overlay Freezes Terminal Input

Severity: High — terminal becomes completely unusable, requires closing the terminal window

Description

When the Hermes TUI displays an approval overlay (dangerous command detected), the terminal freezes completely. The overlay renders visually with the options once / session / always / deny, but no keyboard input reaches the ApprovalPrompt component. Arrow keys, number keys 1-4, and Enter do nothing. The only way out is closing the terminal window entirely.

Root Cause

There is a conflict between two useInput hooks registered on Ink's EventEmitter without any exclusion mechanism:

  1. useInputHandlers (ui-tui/src/app/useInputHandlers.ts:172) — global handler at app level. When isBlocked is true (any overlay active), it ONLY handles:

    • pager (Enter/Space/Escape/q)
    • Ctrl+CcancelOverlayFromCtrlC() (sends approval.respond with deny via RPC)
    • Esc + picker → closes picker
    • Everything else: return — consumes the event and discards it
  2. ApprovalPrompt (ui-tui/src/components/prompts.tsx:17) — registers its own useInput to handle arrow keys, numbers 1-4, and Enter for the approval overlay.

Both hooks are registered on the same EventEmitter. useInputHandlers executes first (app-level), consumes keystrokes in the isBlocked block with return, and the keystrokes never reach ApprovalPrompt. Ink processes handlers in registration order with no propagation control.

Frozen Input Flow

1. Agent executes command matching dangerous pattern
2. approval.py detects pattern, emits approval.request via gateway callback
3. TUI receives event → patchOverlayState({ approval: {...} })
4. isBlocked → true
5. ApprovalPrompt renders visually
6. User presses ↓ or 2 or Enter
7. useInputHandlers receives keystroke FIRST
8. isBlocked? true → not pager, not Ctrl+C → RETURN (discards keystroke)
9. ApprovalPrompt NEVER receives the keystroke
10. Overlay stays active forever → terminal unusable

Why Ctrl+C Also Fails Unreliably

cancelOverlayFromCtrlC() (useInputHandlers.ts:49) sends an RPC approval.respond with choice deny and calls patchOverlayState({ approval: null }). But if the agent thread is blocked in entry.event.wait() and the backend gateway doesn't process the response (e.g., dispatcher busy with a LONG_HANDLER), the RPC doesn't complete and the overlay doesn't close.

Additionally, once patchOverlayState({ approval: null }) executes, isBlocked returns to false, but the agent thread is still blocked waiting for approval. If the user types something new, the agent can't respond, creating an inconsistent state.

Proposed Fixes

Option A — In useInputHandlers, when isBlocked && overlay.approval, don't consume non-Ctrl+C keys. Let ApprovalPrompt handle them:

// useInputHandlers.ts, line ~175
if (isBlocked) {
  if (overlay.approval) {
    if (isCtrl(key, ch, 'c')) {
      cancelOverlayFromCtrlC()
    }
    return // Don't consume arrow/number/enter keys — let ApprovalPrompt handle them
  }
  // ... existing pager/confirm/clarity logic
}

Option B — Use Ink's isActive prop in ApprovalPrompt to only register its useInput when visible:

// prompts.tsx
useInput((ch, key) => { ... }, { isActive: !!req })

Option C (most robust) — Consolidate all overlay input handling into a single handler instead of multiple competing useInput hooks.

Steps to Reproduce

  1. Open Hermes TUI (hermes)
  2. Ask the agent to execute a command matching a dangerous pattern (e.g., git reset --hard, python3 -c "print('hi')", or any command from DANGEROUS_PATTERNS in tools/approval.py)
  3. Approval overlay appears
  4. Try pressing arrow keys, numbers 1-4, or Enter
  5. Nothing happens — terminal is frozen

Environment

  • Hermes Agent v0.10.0 (2026.4.16)
  • TUI mode (default Ink interface)
  • Ink v6.8.0
  • Linux (also likely affects macOS)
  • Konsole
  • zsh

Workaround

Set approvals.mode: smart or approvals.mode: off in ~/.hermes/config.yaml to bypass the approval overlay entirely.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1High — major feature broken, no workaroundcomp/tuiTerminal UI (ui-tui/ + tui_gateway/)type/bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions