Skip to content

feat(tui): Esc interrupts turn + configurable display.interrupt_key - #21720

Open
rxdxxxx wants to merge 1 commit into
NousResearch:mainfrom
rxdxxxx:feat/tui-interrupt-key-config
Open

feat(tui): Esc interrupts turn + configurable display.interrupt_key#21720
rxdxxxx wants to merge 1 commit into
NousResearch:mainfrom
rxdxxxx:feat/tui-interrupt-key-config

Conversation

@rxdxxxx

@rxdxxxx rxdxxxx commented May 8, 2026

Copy link
Copy Markdown

Summary

Pressing Esc now interrupts a running agent turn in the TUI — no configuration needed. First press dismisses the completion list; a second press interrupts the turn.

Supersedes #21707 (base feature) + addresses all review feedback from hermes-sweeper.

Scope: TUI only. CLI (prompt_toolkit) interrupt support is out of scope, tracked separately. Partially addresses #65303 (TUI only). Closes #21707.

Optionally configurable

The interrupt key defaults to Esc but can be changed:

# ~/.hermes/config.yaml
display:
  interrupt_key: ctrl+g   # accepts modifier combos; default: escape

Hot-reloads on config save — no restart needed.

Changes (14 files, +332/-33)

File Change
platform.ts Standalone ParsedInterruptKey type, parseInterruptKey, isInterruptKey, formatInterruptKey — independent from voice key logic (no Cmd+B fallback leak, no voice-specific reserved-key rejection)
gatewayTypes.ts interrupt_key?: unknown on ConfigDisplayConfig
config_defaults.py "interrupt_key": "escape" in DEFAULT_CONFIG["display"] — dashboard schema can now discover this setting
useConfigSync.ts applyDisplay reads/propagates interrupt_key + collision detection (falls back to Esc if same as voice key)
interfaces.ts interruptKey on InputHandlerContext, ComposerActions.clearCompletions, AppLayoutComposerProps.interruptKey
useMainApp.ts state + wiring into useConfigSync / useInputHandlers / composer props
useInputHandlers.ts New priority steps: dismiss completions → interrupt turn (via isInterruptKey)
useCompletion.ts clearCompletions exported as stable useCallback, single source of truth
useComposerState.ts Exposes clearCompletions in ComposerActions
textInput.tsx shouldPassThroughToGlobalHandler accepts interruptKey as 4th param — custom keys (e.g. ctrl+g) pass through the composer
appLayout.tsx Busy placeholder dynamically renders configured key via formatInterruptKey
hotkeys.ts New entry: Esc (or interrupt_key)
interruptKey.test.ts Parser, matcher, formatter tests (15 cases)
textInputPassThrough.test.ts Custom interrupt key passthrough tests

Review feedback addressed

  1. Dismiss completions before interrupt — first Esc closes completions, second Esc interrupts (useInputHandlers priority Fix VM instance sharing across tasks #6-7)
  2. Custom key passthroughinterruptKey threaded into shouldPassThroughToGlobalHandler so ctrl+g etc. are not swallowed by TextInput
  3. Python config defaultconfig_defaults.py now includes "interrupt_key": "escape"
  4. Dynamic hotkey display — busy placeholder renders the configured key; /hotkeys uses generic wording

Design decisions

  • parseInterruptKey is standalone (not delegating to parseVoiceRecordKey) — only rejects ctrl+c, not voice-reserved keys like ctrl+l
  • isInterruptKey is standalone (not delegating to isVoiceToggleKey) — no macOS Cmd+B muscle-memory fallback
  • Collision detection in applyDisplay: if interrupt_key matches voice record_key, silently fall back to Esc
  • Ctrl+C behavior is unchanged and hardcoded

Key priority (high to low)

  1. Double-Esc discard draft
  2. isBlocked early-exit (overlays)
  3. Voice toggle chord (escape + voice key)
  4. Queue-edit cancel
  5. Selection clear
  6. Dismiss completions (interrupt key)
  7. Interrupt turn (interrupt key)
  8. Ctrl+C interrupt/clear/exit

@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/) area/config Config system, migrations, profiles labels May 8, 2026
@rxdxxxx
rxdxxxx force-pushed the feat/tui-interrupt-key-config branch from 2da5dae to 0f3312d Compare May 8, 2026 07:03
@rxdxxxx
rxdxxxx force-pushed the feat/tui-interrupt-key-config branch from 0f3312d to 83ca8dd 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 configurable-keybinding implementation. The feature is still needed on current main, but this branch needs a few changes before it can provide the advertised behavior.

Problems

  • The new global guard in ui-tui/src/app/useInputHandlers.ts:429 does not make custom bindings reachable from the focused composer. TextInput returns early unless shouldPassThroughToGlobalHandler() permits the event (ui-tui/src/components/textInput.tsx:924-937); that predicate only knows Ctrl+C/Ctrl+X, navigation, Escape, and the voice key (:1344-1356). Thus the documented ctrl+g example is consumed by the composer rather than interrupting.
  • display.interrupt_key is not registered in hermes_cli/config.py's DEFAULT_CONFIG. The dashboard config schema is generated from that object at hermes_cli/web_server.py:813, so the setting is not a canonical/defaulted config field.
  • ui-tui/src/components/appLayout.tsx and ui-tui/src/content/hotkeys.ts still display Esc after a custom binding is selected.

Suggested changes

  • Thread interruptKey through TextInput and its pass-through predicate, then add focused-composer, parser, and config-reload tests alongside the existing TUI keybinding suites.
  • Add the default configuration entry and document the TUI-only keybinding; render the configured key in user-facing hints.

Automated hermes-sweeper review.

Comment thread ui-tui/src/app/useInputHandlers.ts Outdated
Comment thread ui-tui/src/gatewayTypes.ts Outdated
Comment thread ui-tui/src/content/hotkeys.ts Outdated
@teknium1 teknium1 added sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades 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
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 force-pushed the feat/tui-interrupt-key-config branch from 83ca8dd to b85752a Compare August 5, 2026 11:16
@rxdxxxx rxdxxxx changed the title feat(tui): display.interrupt_key — configurable interrupt keybinding feat(tui): Esc interrupts turn + configurable display.interrupt_key Aug 5, 2026
@rxdxxxx

rxdxxxx commented Aug 5, 2026

Copy link
Copy Markdown
Author

Thanks for the detailed review! All 3 issues have been addressed in the force-push (b85752a):

1. Custom bindings unreachable from focused composer

shouldPassThroughToGlobalHandler now accepts interruptKey as a 4th parameter. When a custom interrupt key (e.g. ctrl+g) is configured, the composer passes it through to the global handler instead of consuming it. Added test coverage in textInputPassThrough.test.ts.

2. Missing DEFAULT_CONFIG entry

Added "interrupt_key": "escape" to hermes_cli/config_defaults.py under DEFAULT_CONFIG["display"].

3. Hardcoded Esc in UI hints

  • Busy placeholder now renders dynamically via formatInterruptKey(composer.interruptKey) → shows e.g. "Ctrl+G / Ctrl+C to interrupt…"
  • /hotkeys entry uses generic wording: Esc (or interrupt_key)

Additional improvements in this revision

  • Standalone parser/matcherparseInterruptKey and isInterruptKey no longer delegate to voice key functions, fixing the Cmd+B fallback leak and voice-reserved-key rejection (ctrl+l, ctrl+d now valid as interrupt keys)
  • Dismiss-before-interrupt — first press of the interrupt key closes the completion list, second press interrupts the turn
  • Collision detection — if interrupt_key collides with voice.record_key, silently falls back to Esc
  • 15 unit tests for parsing, matching, and formatting

This PR now supersedes #21707 (which can be closed).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/config Config system, migrations, profiles 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-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants