Skip to content

feat(tui): Esc interrupts running agent turn - #21707

Closed
rxdxxxx wants to merge 1 commit into
NousResearch:mainfrom
rxdxxxx:feat/tui-esc-interrupt
Closed

feat(tui): Esc interrupts running agent turn#21707
rxdxxxx wants to merge 1 commit into
NousResearch:mainfrom
rxdxxxx:feat/tui-esc-interrupt

Conversation

@rxdxxxx

@rxdxxxx rxdxxxx commented May 8, 2026

Copy link
Copy Markdown

Problem

Pressing Ctrl+C in the TUI is the only way to interrupt a running agent turn.
This creates a conflict on macOS where Ctrl+C is also the primary copy shortcut
(#16181): users who want to copy an error mid-run accidentally interrupt the agent instead.

Esc is the universally expected "cancel" key in terminal UIs (vim, less, fzf, Claude Code).
Hermes TUI already uses Esc for overlay dismissal and queue-edit cancellation;
extending it to agent interruption is the natural next step.

Related: #16181, #11352, #11355, #4903

Solution

When the agent is busy, Esc calls turnController.interruptTurn() — the same
path Ctrl+C takes — without the clear-draft or exit fallbacks.

Ctrl+C behavior is unchanged.

Changes

useInputHandlers.ts — one guard block before the existing queue-edit handler:

if (isInterruptKey(key, ch, interruptKey) && live.busy && live.sid && !isBlocked && !cState.completions.length) {
  return turnController.interruptTurn({ ... })
}

Guards: live.busy (turn running), live.sid (session exists), !isBlocked (no overlay),
!cState.completions.length (autocomplete gets Esc first).

All existing non-busy Esc behavior preserved: voice-record chord, queue-edit cancel, selection clear.

hotkeys.ts — Esc added to /hotkeys table.

appLayout.tsx — busy placeholder updated to Esc / Ctrl+C to interrupt….

Testing

Build passes. Manual:

  • Esc during stream → interrupts, shows *[interrupted]*
  • Esc with autocomplete open → dismisses list, does not interrupt
  • Esc with overlay open → overlay handles it, no interrupt
  • Esc while idle → existing behavior unchanged
  • Ctrl+C unchanged in all states

Notes

Diff scope: this branch also contains the follow-up commit from #21720.
The changes for this PR are the first commit only (7785cf6).

Configurable keybinding (display.interrupt_key) is in #21720 (depends on this PR).

@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/tui Terminal UI (ui-tui/ + tui_gateway/) labels May 8, 2026
@rxdxxxx
rxdxxxx force-pushed the feat/tui-esc-interrupt branch from 5b99437 to 09b4eb9 Compare May 8, 2026 07:00
When the agent is busy (turn in flight), pressing Esc now fires
interruptTurn() — the same path Ctrl+C takes — without the
clear-draft or exit fallbacks that Ctrl+C carries.

Guards:
- isBlocked: overlays (approval, clarify, pager, sudo, etc.) already
  own their own Esc handlers in the isBlocked branch above; this
  branch is unreachable while any overlay is open, but the explicit
  check keeps intent readable.
- cState.completions.length: autocomplete dropdown gets the first Esc
  (dismiss list), interrupt only fires if no completions are showing.

Existing Esc behaviour is fully preserved in non-busy states:
- queue-edit cancel (queueEditIdx !== null)
- selection clear (terminal.hasSelection)
- voice-record toggle chord (isVoiceToggleKey)

Also:
- hotkeys.ts: document Esc in the /hotkeys help table
- appLayout.tsx: update busy placeholder to 'Esc / Ctrl+C to interrupt…'

Closes NousResearch#16181 (partial — Esc as an alias for interrupt reduces Ctrl+C
copy/interrupt conflict surface).
Related: NousResearch#11352, NousResearch#11355, NousResearch#4903
@rxdxxxx
rxdxxxx force-pushed the feat/tui-esc-interrupt branch from 09b4eb9 to 70d7aae Compare June 18, 2026 09:16

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the focused TUI improvement. Current main still has Ctrl+C as the busy-turn interruption path at ui-tui/src/app/useInputHandlers.ts:520-528, so the premise is valid.

Problems

  • The new !cState.completions.length guard does not make Esc dismiss completions. useCompletion() owns setCompletions privately (ui-tui/src/hooks/useCompletion.ts:42-51) and exposes no clear action (:112), so Esc skips the interrupt branch while completion results remain visible.
  • The PR adds no test for the new busy-Esc route or its stated completion/overlay precedence (ui-tui/src/__tests__/useInputHandlers.test.ts:1-114 currently covers helper functions only).

Suggested changes

  • Expose a completion-dismiss action through the composer state, clear it on the first Esc, then let a subsequent Esc reach turnController.interruptTurn().
  • Add tests for busy interruption, completion-first behavior, and overlay precedence.

Automated hermes-sweeper review.

// handled above in the `isBlocked` branch and never reach here anyway, but
// the explicit check keeps the intent readable. Also skip when completions
// are open so Esc can first dismiss the autocomplete list.
if (key.escape && live.busy && live.sid && !isBlocked && !cState.completions.length) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

!cState.completions.length suppresses interruption, but no later branch clears completion results. useCompletion() keeps setCompletions private, so Esc can remain a no-op while the list is open; expose and invoke a completion-dismiss action before allowing the next Esc to interrupt.

@teknium1 teknium1 added sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 13, 2026
rxdxxxx added a commit to rxdxxxx/hermes-agent that referenced this pull request Aug 5, 2026
Addresses sweeper review on NousResearch#21707 and NousResearch#21720:

PR NousResearch#21707 fixes:
- Expose clearCompletions() from useCompletion hook
- First Esc dismisses completions, second Esc interrupts the turn
- Add Esc to /hotkeys table with full precedence description
- Update busy placeholder to 'Esc / Ctrl+C to interrupt…'
- Add interruptKey.test.ts with parseInterruptKey/isInterruptKey tests

PR NousResearch#21720 fixes:
- Add parseInterruptKey + isInterruptKey + formatInterruptKey to platform.ts
- Add interrupt_key to ConfigDisplayConfig (gatewayTypes.ts)
- Add interrupt_key default to hermes_cli/config_defaults.py
- Thread interruptKey through useConfigSync → useMainApp → useInputHandlers
- Add interruptKey to shouldPassThroughToGlobalHandler so custom keys
  (e.g. ctrl+g) are not consumed by the focused TextInput composer
- Pass interruptKey prop to TextInput component
- Dynamic placeholder renders the configured key label via formatInterruptKey
- Add pass-through tests for custom interrupt key in textInputPassThrough.test

Guards preserved (overlay > voice chord > queue-edit > selection > completions > interrupt):
- Blocked overlays own their Esc handlers — interrupt unreachable while blocked
- Voice-toggle chord (ctrl/alt+escape) wins before generic Esc handlers
- Queue-edit cancel and selection-clear take precedence over interrupt
- Completions dismiss on first Esc; interrupt fires only when list is empty
- Ctrl+C behavior entirely unchanged
rxdxxxx added a commit to rxdxxxx/hermes-agent that referenced this pull request Aug 5, 2026
Addresses sweeper review on NousResearch#21707 and NousResearch#21720:

PR NousResearch#21707 fixes:
- Expose clearCompletions() from useCompletion hook
- First Esc dismisses completions, second Esc interrupts the turn
- Add Esc to /hotkeys table with full precedence description
- Update busy placeholder to 'Esc / Ctrl+C to interrupt…'
- Add interruptKey.test.ts with parseInterruptKey/isInterruptKey tests

PR NousResearch#21720 fixes:
- Add parseInterruptKey + isInterruptKey + formatInterruptKey to platform.ts
- Add interrupt_key to ConfigDisplayConfig (gatewayTypes.ts)
- Add interrupt_key default to hermes_cli/config_defaults.py
- Thread interruptKey through useConfigSync → useMainApp → useInputHandlers
- Add interruptKey to shouldPassThroughToGlobalHandler so custom keys
  (e.g. ctrl+g) are not consumed by the focused TextInput composer
- Pass interruptKey prop to TextInput component
- Dynamic placeholder renders the configured key label via formatInterruptKey
- Add pass-through tests for custom interrupt key in textInputPassThrough.test

Guards preserved (overlay > voice chord > queue-edit > selection > completions > interrupt):
- Blocked overlays own their Esc handlers — interrupt unreachable while blocked
- Voice-toggle chord (ctrl/alt+escape) wins before generic Esc handlers
- Queue-edit cancel and selection-clear take precedence over interrupt
- Completions dismiss on first Esc; interrupt fires only when list is empty
- Ctrl+C behavior entirely unchanged
@rxdxxxx

rxdxxxx commented Aug 5, 2026

Copy link
Copy Markdown
Author

Superseded by #21720 which now includes the base Esc-interrupt feature plus all review fixes in a single PR.

@rxdxxxx rxdxxxx closed this Aug 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/tui Terminal UI (ui-tui/ + tui_gateway/) P3 Low — cosmetic, nice to have sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants