fix(cli): migrate setup model/provider pickers off simple_term_menu to curses
The setup provider->model sub-menu (and three sibling pickers) used simple_term_menu.TerminalMenu, whose ESC and arrow-key handling was unreliable across terminals — notably ESC failed to back out of the model selection list on terminals that emit raw escape sequences (e.g. Ghostty). The codebase already notes simple_term_menu 'conflicts with /dev/tty' and causes 'ghost-duplication rendering', and a prior attempt to migrate these (closed PR) confirmed the same root cause. Route all four single-select pickers through the shared, already-hardened curses_radiolist (which decodes raw CSI/SS3 escape sequences and handles ESC consistently, fixed in #35776): - auth.py _prompt_model_selection — model picker; the pricing column header and the unavailable-models block are passed as the radiolist description so they survive the curses screen clear. ESC now cancels. - main.py _prompt_reasoning_effort_selection — reasoning-effort picker. - main.py _model_flow_named_custom — named custom-provider model picker. - main.py _remove_custom_provider — provider-removal picker. simple_term_menu is no longer imported anywhere (only stale comments referenced it; one in setup.py is corrected). The numbered-input fallbacks are unchanged and still trigger on curses errors / non-TTY. Tests: updated test_terminal_menu_fallbacks / test_reasoning_effort_menu / test_custom_provider_model_switch / test_model_provider_persistence to drive the fallback via curses_radiolist errors instead of breaking simple_term_menu. New test_setup_menu_curses_migration.py asserts each picker routes through curses_radiolist, ESC cancels, and the pricing header is preserved. Net -147/+183 (mostly the new test file; production code shrinks by removing TerminalMenu boilerplate).
This commit is contained in:
+25
-47
@@ -4455,23 +4455,17 @@ def _remove_custom_provider(config):
|
||||
choices.append("Cancel")
|
||||
|
||||
try:
|
||||
from simple_term_menu import TerminalMenu
|
||||
from hermes_cli.curses_ui import curses_radiolist
|
||||
|
||||
menu = TerminalMenu(
|
||||
[f" {c}" for c in choices],
|
||||
cursor_index=0,
|
||||
menu_cursor="-> ",
|
||||
menu_cursor_style=("fg_red", "bold"),
|
||||
menu_highlight_style=("fg_red",),
|
||||
cycle_cursor=True,
|
||||
clear_screen=False,
|
||||
title="Select provider to remove:",
|
||||
idx = curses_radiolist(
|
||||
"Select provider to remove:",
|
||||
list(choices),
|
||||
selected=0,
|
||||
cancel_returns=-1,
|
||||
)
|
||||
idx = menu.show()
|
||||
from hermes_cli.curses_ui import flush_stdin
|
||||
|
||||
flush_stdin()
|
||||
print()
|
||||
if idx < 0:
|
||||
idx = None
|
||||
except (ImportError, NotImplementedError, OSError, subprocess.SubprocessError):
|
||||
for i, c in enumerate(choices, 1):
|
||||
print(f" {i}. {c}")
|
||||
@@ -4538,27 +4532,19 @@ def _model_flow_named_custom(config, provider_info):
|
||||
|
||||
print(f"Found {len(models)} model(s):\n")
|
||||
try:
|
||||
from simple_term_menu import TerminalMenu
|
||||
from hermes_cli.curses_ui import curses_radiolist
|
||||
|
||||
menu_items = [
|
||||
f" {m} (current)" if m == saved_model else f" {m}" for m in models
|
||||
] + [" Cancel"]
|
||||
menu = TerminalMenu(
|
||||
f"{m} (current)" if m == saved_model else m for m in models
|
||||
] + ["Cancel"]
|
||||
idx = curses_radiolist(
|
||||
f"Select model from {name}:",
|
||||
menu_items,
|
||||
cursor_index=default_idx,
|
||||
menu_cursor="-> ",
|
||||
menu_cursor_style=("fg_green", "bold"),
|
||||
menu_highlight_style=("fg_green",),
|
||||
cycle_cursor=True,
|
||||
clear_screen=False,
|
||||
title=f"Select model from {name}:",
|
||||
selected=default_idx,
|
||||
cancel_returns=-1,
|
||||
)
|
||||
idx = menu.show()
|
||||
from hermes_cli.curses_ui import flush_stdin
|
||||
|
||||
flush_stdin()
|
||||
print()
|
||||
if idx is None or idx >= len(models):
|
||||
if idx < 0 or idx >= len(models):
|
||||
print("Cancelled.")
|
||||
return
|
||||
model_name = models[idx]
|
||||
@@ -4735,26 +4721,18 @@ def _prompt_reasoning_effort_selection(efforts, current_effort=""):
|
||||
default_idx = 0
|
||||
|
||||
try:
|
||||
from simple_term_menu import TerminalMenu
|
||||
from hermes_cli.curses_ui import curses_radiolist
|
||||
|
||||
choices = [f" {_label(effort)}" for effort in ordered]
|
||||
choices.append(f" {disable_label}")
|
||||
choices.append(f" {skip_label}")
|
||||
menu = TerminalMenu(
|
||||
choices = [_label(effort) for effort in ordered]
|
||||
choices.append(disable_label)
|
||||
choices.append(skip_label)
|
||||
idx = curses_radiolist(
|
||||
"Select reasoning effort:",
|
||||
choices,
|
||||
cursor_index=default_idx,
|
||||
menu_cursor="-> ",
|
||||
menu_cursor_style=("fg_green", "bold"),
|
||||
menu_highlight_style=("fg_green",),
|
||||
cycle_cursor=True,
|
||||
clear_screen=False,
|
||||
title="Select reasoning effort:",
|
||||
selected=default_idx,
|
||||
cancel_returns=-1,
|
||||
)
|
||||
idx = menu.show()
|
||||
from hermes_cli.curses_ui import flush_stdin
|
||||
|
||||
flush_stdin()
|
||||
if idx is None:
|
||||
if idx < 0:
|
||||
return None
|
||||
print()
|
||||
if idx < len(ordered):
|
||||
|
||||
Reference in New Issue
Block a user