diff --git a/hermes_cli/auth.py b/hermes_cli/auth.py index 36590d617a..6f241a930e 100644 --- a/hermes_cli/auth.py +++ b/hermes_cli/auth.py @@ -2616,6 +2616,8 @@ def _prompt_model_selection( title=effective_title, ) idx = menu.show() + from hermes_cli.curses_ui import flush_stdin + flush_stdin() if idx is None: return None print() diff --git a/hermes_cli/curses_ui.py b/hermes_cli/curses_ui.py index c4b79091e8..a531320fab 100644 --- a/hermes_cli/curses_ui.py +++ b/hermes_cli/curses_ui.py @@ -10,6 +10,28 @@ from typing import Callable, List, Optional, Set from hermes_cli.colors import Colors, color +def flush_stdin() -> None: + """Flush any stray bytes from the stdin input buffer. + + Must be called after ``curses.wrapper()`` (or any terminal-mode library + like simple_term_menu) returns, **before** the next ``input()`` / + ``getpass.getpass()`` call. ``curses.endwin()`` restores the terminal + but does NOT drain the OS input buffer — leftover escape-sequence bytes + (from arrow keys, terminal mode-switch responses, or rapid keypresses) + remain buffered and silently get consumed by the next ``input()`` call, + corrupting user data (e.g. writing ``^[^[`` into .env files). + + On non-TTY stdin (piped, redirected) or Windows, this is a no-op. + """ + try: + if not sys.stdin.isatty(): + return + import termios + termios.tcflush(sys.stdin, termios.TCIFLUSH) + except Exception: + pass + + def curses_checklist( title: str, items: List[str], @@ -131,6 +153,7 @@ def curses_checklist( return curses.wrapper(_draw) + flush_stdin() return result_holder[0] if result_holder[0] is not None else cancel_returns except Exception: diff --git a/hermes_cli/main.py b/hermes_cli/main.py index 949f4f808c..615325a135 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -1672,6 +1672,8 @@ def _remove_custom_provider(config): title="Select provider to remove:", ) idx = menu.show() + from hermes_cli.curses_ui import flush_stdin + flush_stdin() print() except (ImportError, NotImplementedError, OSError, subprocess.SubprocessError): for i, c in enumerate(choices, 1): @@ -1749,6 +1751,8 @@ def _model_flow_named_custom(config, provider_info): title=f"Select model from {name}:", ) idx = menu.show() + from hermes_cli.curses_ui import flush_stdin + flush_stdin() print() if idx is None or idx >= len(models): print("Cancelled.") @@ -1867,6 +1871,8 @@ def _prompt_reasoning_effort_selection(efforts, current_effort=""): title="Select reasoning effort:", ) idx = menu.show() + from hermes_cli.curses_ui import flush_stdin + flush_stdin() if idx is None: return None print() diff --git a/hermes_cli/setup.py b/hermes_cli/setup.py index b72cfeef47..60ca76d538 100644 --- a/hermes_cli/setup.py +++ b/hermes_cli/setup.py @@ -338,6 +338,8 @@ def _curses_prompt_choice(question: str, choices: list, default: int = 0) -> int return curses.wrapper(_curses_menu) + from hermes_cli.curses_ui import flush_stdin + flush_stdin() return result_holder[0] except Exception: return -1 diff --git a/hermes_cli/tools_config.py b/hermes_cli/tools_config.py index 9a50a2c5d5..b988f5544a 100644 --- a/hermes_cli/tools_config.py +++ b/hermes_cli/tools_config.py @@ -720,6 +720,8 @@ def _prompt_choice(question: str, choices: list, default: int = 0) -> int: return curses.wrapper(_curses_menu) + from hermes_cli.curses_ui import flush_stdin + flush_stdin() return result_holder[0] except Exception: