mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-15 04:12:25 +00:00
fix(cli): submit LF enter in thin PTYs (#20896)
This commit is contained in:
parent
d8b85bfd1c
commit
5044e1cbf1
2 changed files with 48 additions and 7 deletions
25
cli.py
25
cli.py
|
|
@ -1774,6 +1774,20 @@ _TERMINAL_INPUT_MODE_RESET_SEQ = (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _bind_prompt_submit_keys(kb, handler) -> None:
|
||||||
|
"""Bind both CR and LF terminal Enter forms to the submit handler."""
|
||||||
|
for key in ("enter", "c-j"):
|
||||||
|
kb.add(key)(handler)
|
||||||
|
|
||||||
|
|
||||||
|
def _disable_prompt_toolkit_cpr_warning(app) -> None:
|
||||||
|
"""Let prompt_toolkit fall back from CPR without printing into the prompt."""
|
||||||
|
try:
|
||||||
|
app.renderer.cpr_not_supported_callback = None
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def _strip_leaked_terminal_responses_with_meta(text: str) -> tuple[str, bool]:
|
def _strip_leaked_terminal_responses_with_meta(text: str) -> tuple[str, bool]:
|
||||||
"""Strip leaked terminal control-response sequences from user input.
|
"""Strip leaked terminal control-response sequences from user input.
|
||||||
|
|
||||||
|
|
@ -10338,7 +10352,6 @@ class HermesCLI:
|
||||||
# Key bindings for the input area
|
# Key bindings for the input area
|
||||||
kb = KeyBindings()
|
kb = KeyBindings()
|
||||||
|
|
||||||
@kb.add('enter')
|
|
||||||
def handle_enter(event):
|
def handle_enter(event):
|
||||||
"""Handle Enter key - submit input.
|
"""Handle Enter key - submit input.
|
||||||
|
|
||||||
|
|
@ -10498,16 +10511,13 @@ class HermesCLI:
|
||||||
self._pending_input.put(payload)
|
self._pending_input.put(payload)
|
||||||
event.app.current_buffer.reset(append_to_history=True)
|
event.app.current_buffer.reset(append_to_history=True)
|
||||||
|
|
||||||
|
_bind_prompt_submit_keys(kb, handle_enter)
|
||||||
|
|
||||||
@kb.add('escape', 'enter')
|
@kb.add('escape', 'enter')
|
||||||
def handle_alt_enter(event):
|
def handle_alt_enter(event):
|
||||||
"""Alt+Enter inserts a newline for multi-line input."""
|
"""Alt+Enter inserts a newline for multi-line input."""
|
||||||
event.current_buffer.insert_text('\n')
|
event.current_buffer.insert_text('\n')
|
||||||
|
|
||||||
@kb.add('c-j')
|
|
||||||
def handle_ctrl_enter(event):
|
|
||||||
"""Ctrl+Enter (c-j) inserts a newline. Most terminals send c-j for Ctrl+Enter."""
|
|
||||||
event.current_buffer.insert_text('\n')
|
|
||||||
|
|
||||||
# VSCode/Cursor bind Ctrl+G to "Find Next" at the editor level, so
|
# VSCode/Cursor bind Ctrl+G to "Find Next" at the editor level, so
|
||||||
# the keystroke never reaches the embedded terminal. Alt+G is unbound
|
# the keystroke never reaches the embedded terminal. Alt+G is unbound
|
||||||
# in those IDEs and arrives here as ('escape', 'g') — register it as
|
# in those IDEs and arrives here as ('escape', 'g') — register it as
|
||||||
|
|
@ -11106,7 +11116,7 @@ class HermesCLI:
|
||||||
def get_prompt():
|
def get_prompt():
|
||||||
return cli_ref._get_tui_prompt_fragments()
|
return cli_ref._get_tui_prompt_fragments()
|
||||||
|
|
||||||
# Create the input area with multiline (shift+enter), autocomplete, and paste handling
|
# Create the input area with multiline (Alt+Enter), autocomplete, and paste handling
|
||||||
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
|
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -11848,6 +11858,7 @@ class HermesCLI:
|
||||||
mouse_support=False,
|
mouse_support=False,
|
||||||
**({'cursor': _STEADY_CURSOR} if _STEADY_CURSOR is not None else {}),
|
**({'cursor': _STEADY_CURSOR} if _STEADY_CURSOR is not None else {}),
|
||||||
)
|
)
|
||||||
|
_disable_prompt_toolkit_cpr_warning(app)
|
||||||
self._app = app # Store reference for clarify_callback
|
self._app = app # Store reference for clarify_callback
|
||||||
|
|
||||||
# ── Fix ghost status-bar lines on terminal resize ──────────────
|
# ── Fix ghost status-bar lines on terminal resize ──────────────
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ that only manifest at runtime (not in mocked unit tests)."""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
from types import SimpleNamespace
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||||
|
|
@ -161,6 +162,35 @@ class TestBusyInputMode:
|
||||||
assert cli._pending_input.empty()
|
assert cli._pending_input.empty()
|
||||||
|
|
||||||
|
|
||||||
|
class TestPromptToolkitTerminalCompatibility:
|
||||||
|
def test_lf_enter_binds_to_submit_handler(self):
|
||||||
|
"""Some thin PTYs deliver Enter as LF/c-j instead of CR/enter."""
|
||||||
|
from prompt_toolkit.key_binding import KeyBindings
|
||||||
|
|
||||||
|
from cli import _bind_prompt_submit_keys
|
||||||
|
|
||||||
|
kb = KeyBindings()
|
||||||
|
|
||||||
|
def submit_handler(event):
|
||||||
|
return None
|
||||||
|
|
||||||
|
_bind_prompt_submit_keys(kb, submit_handler)
|
||||||
|
|
||||||
|
bindings = {tuple(key.value for key in binding.keys): binding.handler for binding in kb.bindings}
|
||||||
|
assert bindings[("c-m",)] is submit_handler
|
||||||
|
assert bindings[("c-j",)] is submit_handler
|
||||||
|
|
||||||
|
def test_cpr_warning_callback_is_disabled(self):
|
||||||
|
from cli import _disable_prompt_toolkit_cpr_warning
|
||||||
|
|
||||||
|
renderer = SimpleNamespace(cpr_not_supported_callback=lambda: None)
|
||||||
|
app = SimpleNamespace(renderer=renderer)
|
||||||
|
|
||||||
|
_disable_prompt_toolkit_cpr_warning(app)
|
||||||
|
|
||||||
|
assert renderer.cpr_not_supported_callback is None
|
||||||
|
|
||||||
|
|
||||||
class TestSingleQueryState:
|
class TestSingleQueryState:
|
||||||
def test_voice_and_interrupt_state_initialized_before_run(self):
|
def test_voice_and_interrupt_state_initialized_before_run(self):
|
||||||
"""Single-query mode calls chat() without going through run()."""
|
"""Single-query mode calls chat() without going through run()."""
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue