@@ -901,7 +901,7 @@ def select_provider_and_model(args=None):
901901 try :
902902 active = resolve_provider ("auto" )
903903 except AuthError :
904- active = "openrouter" # no provider yet; show full picker
904+ active = None # no provider yet; default to first in list
905905
906906 # Detect custom endpoint
907907 if active == "openrouter" and get_env_value ("OPENAI_BASE_URL" ):
@@ -926,21 +926,25 @@ def select_provider_and_model(args=None):
926926 "huggingface" : "Hugging Face" ,
927927 "custom" : "Custom endpoint" ,
928928 }
929- active_label = provider_labels .get (active , active )
929+ active_label = provider_labels .get (active , active ) if active else "none"
930930
931931 print ()
932932 print (f" Current model: { current_model } " )
933933 print (f" Active provider: { active_label } " )
934934 print ()
935935
936- # Step 1: Provider selection — put active provider first with marker
937- providers = [
938- ("openrouter" , "OpenRouter (100+ models, pay-per-use)" ),
936+ # Step 1: Provider selection — top providers shown first, rest behind "More..."
937+ top_providers = [
939938 ("nous" , "Nous Portal (Nous Research subscription)" ),
939+ ("openrouter" , "OpenRouter (100+ models, pay-per-use)" ),
940+ ("anthropic" , "Anthropic (Claude models — API key or Claude Code)" ),
940941 ("openai-codex" , "OpenAI Codex" ),
941- ("copilot-acp" , "GitHub Copilot ACP (spawns `copilot --acp --stdio`)" ),
942942 ("copilot" , "GitHub Copilot (uses GITHUB_TOKEN or gh auth token)" ),
943- ("anthropic" , "Anthropic (Claude models — API key or Claude Code)" ),
943+ ("huggingface" , "Hugging Face Inference Providers (20+ open models)" ),
944+ ]
945+
946+ extended_providers = [
947+ ("copilot-acp" , "GitHub Copilot ACP (spawns `copilot --acp --stdio`)" ),
944948 ("zai" , "Z.AI / GLM (Zhipu AI direct API)" ),
945949 ("kimi-coding" , "Kimi / Moonshot (Moonshot AI direct API)" ),
946950 ("minimax" , "MiniMax (global direct API)" ),
@@ -950,7 +954,6 @@ def select_provider_and_model(args=None):
950954 ("opencode-go" , "OpenCode Go (open models, $10/month subscription)" ),
951955 ("ai-gateway" , "AI Gateway (Vercel — 200+ models, pay-per-use)" ),
952956 ("alibaba" , "Alibaba Cloud / DashScope Coding (Qwen + multi-provider)" ),
953- ("huggingface" , "Hugging Face Inference Providers (20+ open models)" ),
954957 ]
955958
956959 # Add user-defined custom providers from config.yaml
@@ -964,44 +967,66 @@ def select_provider_and_model(args=None):
964967 base_url = (entry .get ("base_url" ) or "" ).strip ()
965968 if not name or not base_url :
966969 continue
967- # Generate a stable key from the name
968970 key = "custom:" + name .lower ().replace (" " , "-" )
969971 short_url = base_url .replace ("https://" , "" ).replace ("http://" , "" ).rstrip ("/" )
970972 saved_model = entry .get ("model" , "" )
971973 model_hint = f" — { saved_model } " if saved_model else ""
972- providers .append ((key , f"{ name } ({ short_url } ){ model_hint } " ))
974+ top_providers .append ((key , f"{ name } ({ short_url } ){ model_hint } " ))
973975 _custom_provider_map [key ] = {
974976 "name" : name ,
975977 "base_url" : base_url ,
976978 "api_key" : entry .get ("api_key" , "" ),
977979 "model" : saved_model ,
978980 }
979981
980- # Always add the manual custom endpoint option last
981- providers . append (( "custom" , "Custom endpoint (enter URL manually)" ))
982+ top_keys = { k for k , _ in top_providers }
983+ extended_keys = { k for k , _ in extended_providers }
982984
983- # Add removal option if there are saved custom providers
984- if _custom_provider_map :
985- providers .append (("remove-custom" , "Remove a saved custom provider" ))
985+ # If the active provider is in the extended list, promote it into top
986+ if active and active in extended_keys :
987+ promoted = [(k , l ) for k , l in extended_providers if k == active ]
988+ extended_providers = [(k , l ) for k , l in extended_providers if k != active ]
989+ top_providers = promoted + top_providers
990+ top_keys .add (active )
986991
987- # Reorder so the active provider is at the top
988- known_keys = {k for k , _ in providers }
989- active_key = active if active in known_keys else "custom"
992+ # Build the primary menu
990993 ordered = []
991- for key , label in providers :
992- if key == active_key :
993- ordered .insert (0 , (key , f"{ label } ← currently active" ))
994+ default_idx = 0
995+ for key , label in top_providers :
996+ if active and key == active :
997+ ordered .append ((key , f"{ label } ← currently active" ))
998+ default_idx = len (ordered ) - 1
994999 else :
9951000 ordered .append ((key , label ))
1001+
1002+ ordered .append (("more" , "More providers..." ))
9961003 ordered .append (("cancel" , "Cancel" ))
9971004
998- provider_idx = _prompt_provider_choice ([label for _ , label in ordered ])
1005+ provider_idx = _prompt_provider_choice (
1006+ [label for _ , label in ordered ], default = default_idx ,
1007+ )
9991008 if provider_idx is None or ordered [provider_idx ][0 ] == "cancel" :
10001009 print ("No change." )
10011010 return
10021011
10031012 selected_provider = ordered [provider_idx ][0 ]
10041013
1014+ # "More providers..." — show the extended list
1015+ if selected_provider == "more" :
1016+ ext_ordered = list (extended_providers )
1017+ ext_ordered .append (("custom" , "Custom endpoint (enter URL manually)" ))
1018+ if _custom_provider_map :
1019+ ext_ordered .append (("remove-custom" , "Remove a saved custom provider" ))
1020+ ext_ordered .append (("cancel" , "Cancel" ))
1021+
1022+ ext_idx = _prompt_provider_choice (
1023+ [label for _ , label in ext_ordered ], default = 0 ,
1024+ )
1025+ if ext_idx is None or ext_ordered [ext_idx ][0 ] == "cancel" :
1026+ print ("No change." )
1027+ return
1028+ selected_provider = ext_ordered [ext_idx ][0 ]
1029+
10051030 # Step 2: Provider-specific setup + model selection
10061031 if selected_provider == "openrouter" :
10071032 _model_flow_openrouter (config , current_model )
@@ -1027,34 +1052,33 @@ def select_provider_and_model(args=None):
10271052 _model_flow_api_key_provider (config , selected_provider , current_model )
10281053
10291054
1030- def _prompt_provider_choice (choices ):
1031- """Show provider selection menu. Returns index or None."""
1055+ def _prompt_provider_choice (choices , * , default = 0 ):
1056+ """Show provider selection menu with curses arrow-key navigation.
1057+
1058+ Falls back to a numbered list when curses is unavailable (e.g. piped
1059+ stdin, non-TTY environments). Returns the selected index, or None
1060+ if the user cancels.
1061+ """
10321062 try :
1033- from simple_term_menu import TerminalMenu
1034- menu_items = [f" { c } " for c in choices ]
1035- menu = TerminalMenu (
1036- menu_items , cursor_index = 0 ,
1037- menu_cursor = "-> " , menu_cursor_style = ("fg_green" , "bold" ),
1038- menu_highlight_style = ("fg_green" ,),
1039- cycle_cursor = True , clear_screen = False ,
1040- title = "Select provider:" ,
1041- )
1042- idx = menu .show ()
1043- print ()
1044- return idx
1045- except (ImportError , NotImplementedError ):
1063+ from hermes_cli .setup import _curses_prompt_choice
1064+ idx = _curses_prompt_choice ("Select provider:" , choices , default )
1065+ if idx >= 0 :
1066+ print ()
1067+ return idx
1068+ except Exception :
10461069 pass
10471070
10481071 # Fallback: numbered list
10491072 print ("Select provider:" )
10501073 for i , c in enumerate (choices , 1 ):
1051- print (f" { i } . { c } " )
1074+ marker = "→" if i - 1 == default else " "
1075+ print (f" { marker } { i } . { c } " )
10521076 print ()
10531077 while True :
10541078 try :
1055- val = input (f"Choice [1-{ len (choices )} ]: " ).strip ()
1079+ val = input (f"Choice [1-{ len (choices )} ] ( { default + 1 } ) : " ).strip ()
10561080 if not val :
1057- return None
1081+ return default
10581082 idx = int (val ) - 1
10591083 if 0 <= idx < len (choices ):
10601084 return idx
@@ -1077,7 +1101,8 @@ def _model_flow_openrouter(config, current_model=""):
10771101 print ("Get one at: https://openrouter.ai/keys" )
10781102 print ()
10791103 try :
1080- key = input ("OpenRouter API key (or Enter to cancel): " ).strip ()
1104+ import getpass
1105+ key = getpass .getpass ("OpenRouter API key (or Enter to cancel): " ).strip ()
10811106 except (KeyboardInterrupt , EOFError ):
10821107 print ()
10831108 return
@@ -1294,7 +1319,8 @@ def _model_flow_custom(config):
12941319
12951320 try :
12961321 base_url = input (f"API base URL [{ current_url or 'e.g. https://api.example.com/v1' } ]: " ).strip ()
1297- api_key = input (f"API key [{ current_key [:8 ] + '...' if current_key else 'optional' } ]: " ).strip ()
1322+ import getpass
1323+ api_key = getpass .getpass (f"API key [{ current_key [:8 ] + '...' if current_key else 'optional' } ]: " ).strip ()
12981324 except (KeyboardInterrupt , EOFError ):
12991325 print ("\n Cancelled." )
13001326 return
@@ -1803,7 +1829,8 @@ def _model_flow_copilot(config, current_model=""):
18031829 return
18041830 elif choice == "2" :
18051831 try :
1806- new_key = input (" Token (COPILOT_GITHUB_TOKEN): " ).strip ()
1832+ import getpass
1833+ new_key = getpass .getpass (" Token (COPILOT_GITHUB_TOKEN): " ).strip ()
18071834 except (KeyboardInterrupt , EOFError ):
18081835 print ()
18091836 return
@@ -2044,7 +2071,8 @@ def _model_flow_kimi(config, current_model=""):
20442071 print (f"No { pconfig .name } API key configured." )
20452072 if key_env :
20462073 try :
2047- new_key = input (f"{ key_env } (or Enter to cancel): " ).strip ()
2074+ import getpass
2075+ new_key = getpass .getpass (f"{ key_env } (or Enter to cancel): " ).strip ()
20482076 except (KeyboardInterrupt , EOFError ):
20492077 print ()
20502078 return
@@ -2138,7 +2166,8 @@ def _model_flow_api_key_provider(config, provider_id, current_model=""):
21382166 print (f"No { pconfig .name } API key configured." )
21392167 if key_env :
21402168 try :
2141- new_key = input (f"{ key_env } (or Enter to cancel): " ).strip ()
2169+ import getpass
2170+ new_key = getpass .getpass (f"{ key_env } (or Enter to cancel): " ).strip ()
21422171 except (KeyboardInterrupt , EOFError ):
21432172 print ()
21442173 return
@@ -2272,7 +2301,8 @@ def _activate_claude_code_credentials_if_available() -> bool:
22722301 print (" If the setup-token was displayed above, paste it here:" )
22732302 print ()
22742303 try :
2275- manual_token = input (" Paste setup-token (or Enter to cancel): " ).strip ()
2304+ import getpass
2305+ manual_token = getpass .getpass (" Paste setup-token (or Enter to cancel): " ).strip ()
22762306 except (KeyboardInterrupt , EOFError ):
22772307 print ()
22782308 return False
@@ -2299,7 +2329,8 @@ def _activate_claude_code_credentials_if_available() -> bool:
22992329 print (" Or paste an existing setup-token now (sk-ant-oat-...):" )
23002330 print ()
23012331 try :
2302- token = input (" Setup-token (or Enter to cancel): " ).strip ()
2332+ import getpass
2333+ token = getpass .getpass (" Setup-token (or Enter to cancel): " ).strip ()
23032334 except (KeyboardInterrupt , EOFError ):
23042335 print ()
23052336 return False
@@ -2392,7 +2423,8 @@ def _model_flow_anthropic(config, current_model=""):
23922423 print (" Get an API key at: https://console.anthropic.com/settings/keys" )
23932424 print ()
23942425 try :
2395- api_key = input (" API key (sk-ant-...): " ).strip ()
2426+ import getpass
2427+ api_key = getpass .getpass (" API key (sk-ant-...): " ).strip ()
23962428 except (KeyboardInterrupt , EOFError ):
23972429 print ()
23982430 return
0 commit comments