fix: polish onboard wizard keyboard navigation
This commit is contained in:
+205
-118
@@ -109,6 +109,7 @@ _UI_BORDER = "#4E5254"
|
|||||||
_UI_TEXT = "#A9B7C6"
|
_UI_TEXT = "#A9B7C6"
|
||||||
_UI_MUTED = "#80868B"
|
_UI_MUTED = "#80868B"
|
||||||
_UI_SUCCESS = "#6AAB73"
|
_UI_SUCCESS = "#6AAB73"
|
||||||
|
_PROMPT_ESCAPE_TIMEOUT_SECONDS = 0.05
|
||||||
_CHANNEL_LOGIN_CHOICE = "Login with QR/link"
|
_CHANNEL_LOGIN_CHOICE = "Login with QR/link"
|
||||||
_CHANNEL_ADVANCED_CHOICE = "Edit advanced settings"
|
_CHANNEL_ADVANCED_CHOICE = "Edit advanced settings"
|
||||||
|
|
||||||
@@ -138,6 +139,8 @@ def _select_with_back(
|
|||||||
The selected choice string if user confirmed
|
The selected choice string if user confirmed
|
||||||
None if user cancelled (Ctrl+C)
|
None if user cancelled (Ctrl+C)
|
||||||
"""
|
"""
|
||||||
|
import shutil
|
||||||
|
|
||||||
from prompt_toolkit.application import Application
|
from prompt_toolkit.application import Application
|
||||||
from prompt_toolkit.key_binding import KeyBindings
|
from prompt_toolkit.key_binding import KeyBindings
|
||||||
from prompt_toolkit.keys import Keys
|
from prompt_toolkit.keys import Keys
|
||||||
@@ -158,11 +161,15 @@ def _select_with_back(
|
|||||||
|
|
||||||
# State holder for the result
|
# State holder for the result
|
||||||
state: dict[str, str | None | object] = {"result": None}
|
state: dict[str, str | None | object] = {"result": None}
|
||||||
|
terminal_lines = shutil.get_terminal_size((80, 24)).lines
|
||||||
|
visible_count = min(len(choices), max(1, terminal_lines - 3))
|
||||||
|
|
||||||
# Build menu items (uses closure over selected_index)
|
# Build menu items (uses closure over selected_index)
|
||||||
def get_menu_text():
|
def get_menu_text():
|
||||||
items = []
|
items = []
|
||||||
for i, choice in enumerate(choices):
|
start, end = _choice_viewport(selected_index, len(choices), visible_count)
|
||||||
|
for i in range(start, end):
|
||||||
|
choice = choices[i]
|
||||||
if i == selected_index:
|
if i == selected_index:
|
||||||
items.append(("class:selected", f"> {choice}\n"))
|
items.append(("class:selected", f"> {choice}\n"))
|
||||||
else:
|
else:
|
||||||
@@ -170,11 +177,15 @@ def _select_with_back(
|
|||||||
return items
|
return items
|
||||||
|
|
||||||
# Create layout
|
# Create layout
|
||||||
menu_control = FormattedTextControl(get_menu_text)
|
menu_control = FormattedTextControl(get_menu_text, show_cursor=False)
|
||||||
menu_window = Window(content=menu_control, height=len(choices))
|
menu_window = Window(content=menu_control, height=visible_count, always_hide_cursor=True)
|
||||||
|
|
||||||
prompt_control = FormattedTextControl(lambda: [("class:question", f"> {prompt}")])
|
def get_prompt_text():
|
||||||
prompt_window = Window(content=prompt_control, height=1)
|
suffix = f" ({selected_index + 1}/{len(choices)})" if len(choices) > visible_count else ""
|
||||||
|
return [("class:question", f"{prompt}{suffix}")]
|
||||||
|
|
||||||
|
prompt_control = FormattedTextControl(get_prompt_text, show_cursor=False)
|
||||||
|
prompt_window = Window(content=prompt_control, height=1, always_hide_cursor=True)
|
||||||
|
|
||||||
layout = Layout(HSplit([prompt_window, menu_window]))
|
layout = Layout(HSplit([prompt_window, menu_window]))
|
||||||
|
|
||||||
@@ -220,6 +231,8 @@ def _select_with_back(
|
|||||||
})
|
})
|
||||||
|
|
||||||
app = Application(layout=layout, key_bindings=bindings, style=style)
|
app = Application(layout=layout, key_bindings=bindings, style=style)
|
||||||
|
app.ttimeoutlen = 0.05
|
||||||
|
app.timeoutlen = 0.05
|
||||||
try:
|
try:
|
||||||
app.run()
|
app.run()
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -228,6 +241,18 @@ def _select_with_back(
|
|||||||
|
|
||||||
return state["result"]
|
return state["result"]
|
||||||
|
|
||||||
|
|
||||||
|
def _choice_viewport(selected_index: int, total: int, visible_count: int) -> tuple[int, int]:
|
||||||
|
"""Return the visible slice for a long terminal menu."""
|
||||||
|
if total <= 0:
|
||||||
|
return 0, 0
|
||||||
|
visible_count = max(1, min(visible_count, total))
|
||||||
|
selected_index = max(0, min(selected_index, total - 1))
|
||||||
|
half = visible_count // 2
|
||||||
|
start = selected_index - half
|
||||||
|
start = max(0, min(start, total - visible_count))
|
||||||
|
return start, start + visible_count
|
||||||
|
|
||||||
# --- Type Introspection ---
|
# --- Type Introspection ---
|
||||||
|
|
||||||
|
|
||||||
@@ -474,14 +499,44 @@ def _input_bool(display_name: str, current: bool | None) -> bool | None:
|
|||||||
).ask()
|
).ask()
|
||||||
|
|
||||||
|
|
||||||
|
def _input_back_key_bindings():
|
||||||
|
"""Return key bindings that make Escape behave like a local back action."""
|
||||||
|
from prompt_toolkit.key_binding import KeyBindings
|
||||||
|
|
||||||
|
bindings = KeyBindings()
|
||||||
|
|
||||||
|
@bindings.add("escape")
|
||||||
|
def _escape(event):
|
||||||
|
event.app.exit(result=_BACK_PRESSED)
|
||||||
|
|
||||||
|
return bindings
|
||||||
|
|
||||||
|
|
||||||
|
def _ask_prompt(prompt):
|
||||||
|
"""Ask a questionary prompt with responsive Escape handling."""
|
||||||
|
app = getattr(prompt, "application", None)
|
||||||
|
if app is not None:
|
||||||
|
if hasattr(app, "ttimeoutlen"):
|
||||||
|
app.ttimeoutlen = _PROMPT_ESCAPE_TIMEOUT_SECONDS
|
||||||
|
if hasattr(app, "timeoutlen"):
|
||||||
|
app.timeoutlen = _PROMPT_ESCAPE_TIMEOUT_SECONDS
|
||||||
|
return prompt.ask()
|
||||||
|
|
||||||
|
|
||||||
def _input_text(display_name: str, current: Any, field_type: str, field_info=None) -> Any:
|
def _input_text(display_name: str, current: Any, field_type: str, field_info=None) -> Any:
|
||||||
"""Get text input and parse based on field type."""
|
"""Get text input and parse based on field type."""
|
||||||
default = _format_value_for_input(current, field_type)
|
default = _format_value_for_input(current, field_type)
|
||||||
|
|
||||||
value = _get_questionary().text(f"{display_name}:", default=default).ask()
|
value = _ask_prompt(
|
||||||
|
_get_questionary().text(
|
||||||
|
f"{display_name}:",
|
||||||
|
default=default,
|
||||||
|
key_bindings=_input_back_key_bindings(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
if value is None:
|
if value is _BACK_PRESSED or value is None:
|
||||||
return None
|
return None if value is None else _BACK_PRESSED
|
||||||
|
|
||||||
if field_type == "int":
|
if field_type == "int":
|
||||||
try:
|
try:
|
||||||
@@ -519,14 +574,14 @@ def _input_text(display_name: str, current: Any, field_type: str, field_info=Non
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
def _input_secret(display_name: str) -> str | None:
|
def _input_secret(display_name: str) -> str | None | object:
|
||||||
"""Get a secret value without echoing it when questionary supports password input."""
|
"""Get a secret value without echoing it when questionary supports password input."""
|
||||||
prompt_factory = getattr(_get_questionary(), "password", None)
|
prompt_factory = getattr(_get_questionary(), "password", None)
|
||||||
if prompt_factory is None:
|
if prompt_factory is None:
|
||||||
prompt_factory = _get_questionary().text
|
prompt_factory = _get_questionary().text
|
||||||
value = prompt_factory(f"{display_name}:").ask()
|
value = _ask_prompt(prompt_factory(f"{display_name}:", key_bindings=_input_back_key_bindings()))
|
||||||
if value is None:
|
if value is _BACK_PRESSED or value is None:
|
||||||
return None
|
return None if value is None else _BACK_PRESSED
|
||||||
return str(value).strip()
|
return str(value).strip()
|
||||||
|
|
||||||
|
|
||||||
@@ -560,7 +615,7 @@ def _get_current_provider(model: BaseModel) -> str:
|
|||||||
|
|
||||||
def _input_model_with_autocomplete(
|
def _input_model_with_autocomplete(
|
||||||
display_name: str, current: Any, provider: str
|
display_name: str, current: Any, provider: str
|
||||||
) -> str | None:
|
) -> str | None | object:
|
||||||
"""Get model input with autocomplete suggestions.
|
"""Get model input with autocomplete suggestions.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@@ -587,20 +642,25 @@ def _input_model_with_autocomplete(
|
|||||||
display=model,
|
display=model,
|
||||||
)
|
)
|
||||||
|
|
||||||
value = _get_questionary().autocomplete(
|
value = _ask_prompt(
|
||||||
f"{display_name}:",
|
_get_questionary().autocomplete(
|
||||||
choices=[""], # Placeholder, actual completions from completer
|
f"{display_name}:",
|
||||||
completer=DynamicModelCompleter(provider),
|
choices=[""], # Placeholder, actual completions from completer
|
||||||
default=default,
|
completer=DynamicModelCompleter(provider),
|
||||||
qmark=">",
|
default=default,
|
||||||
).ask()
|
key_bindings=_input_back_key_bindings(),
|
||||||
|
qmark=">",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
return value if value is not None else None
|
if value is _BACK_PRESSED or value is None:
|
||||||
|
return None if value is None else _BACK_PRESSED
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
def _input_context_window_with_recommendation(
|
def _input_context_window_with_recommendation(
|
||||||
display_name: str, current: Any, model_obj: BaseModel
|
display_name: str, current: Any, model_obj: BaseModel
|
||||||
) -> int | None:
|
) -> int | None | object:
|
||||||
"""Get context window input with option to fetch recommended value."""
|
"""Get context window input with option to fetch recommended value."""
|
||||||
current_val = current if current else ""
|
current_val = current if current else ""
|
||||||
|
|
||||||
@@ -645,8 +705,11 @@ def _input_context_window_with_recommendation(
|
|||||||
value = _get_questionary().text(
|
value = _get_questionary().text(
|
||||||
f"{display_name}:",
|
f"{display_name}:",
|
||||||
default=str(current_val) if current_val else "",
|
default=str(current_val) if current_val else "",
|
||||||
|
key_bindings=_input_back_key_bindings(),
|
||||||
).ask()
|
).ask()
|
||||||
|
|
||||||
|
if value is _BACK_PRESSED:
|
||||||
|
return _BACK_PRESSED
|
||||||
if value is None or value == "":
|
if value is None or value == "":
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -663,6 +726,8 @@ def _handle_model_field(
|
|||||||
"""Handle the 'model' field with autocomplete and context-window auto-fill."""
|
"""Handle the 'model' field with autocomplete and context-window auto-fill."""
|
||||||
provider = _get_current_provider(working_model)
|
provider = _get_current_provider(working_model)
|
||||||
new_value = _input_model_with_autocomplete(field_display, current_value, provider)
|
new_value = _input_model_with_autocomplete(field_display, current_value, provider)
|
||||||
|
if new_value is _BACK_PRESSED:
|
||||||
|
return
|
||||||
if new_value is not None and new_value != current_value:
|
if new_value is not None and new_value != current_value:
|
||||||
setattr(working_model, field_name, new_value)
|
setattr(working_model, field_name, new_value)
|
||||||
_try_auto_fill_context_window(working_model, new_value)
|
_try_auto_fill_context_window(working_model, new_value)
|
||||||
@@ -675,6 +740,8 @@ def _handle_context_window_field(
|
|||||||
new_value = _input_context_window_with_recommendation(
|
new_value = _input_context_window_with_recommendation(
|
||||||
field_display, current_value, working_model
|
field_display, current_value, working_model
|
||||||
)
|
)
|
||||||
|
if new_value is _BACK_PRESSED:
|
||||||
|
return
|
||||||
if new_value is not None:
|
if new_value is not None:
|
||||||
setattr(working_model, field_name, new_value)
|
setattr(working_model, field_name, new_value)
|
||||||
|
|
||||||
@@ -902,6 +969,8 @@ def _configure_pydantic_model(
|
|||||||
new_value = _input_bool(field_display, current_value)
|
new_value = _input_bool(field_display, current_value)
|
||||||
else:
|
else:
|
||||||
new_value = _input_with_existing(field_display, current_value, ftype.type_name, field_info=field_info)
|
new_value = _input_with_existing(field_display, current_value, ftype.type_name, field_info=field_info)
|
||||||
|
if new_value is _BACK_PRESSED:
|
||||||
|
continue
|
||||||
if new_value is not None:
|
if new_value is not None:
|
||||||
# Normalize empty string to None for optional string fields so that
|
# Normalize empty string to None for optional string fields so that
|
||||||
# clearing an api_key / api_base actually removes the value.
|
# clearing an api_key / api_base actually removes the value.
|
||||||
@@ -1523,7 +1592,7 @@ def _select_quick_start_api_base(
|
|||||||
provider_name: str,
|
provider_name: str,
|
||||||
provider_display: str,
|
provider_display: str,
|
||||||
info: _QuickStartProviderInfo | None,
|
info: _QuickStartProviderInfo | None,
|
||||||
) -> tuple[str, bool] | None:
|
) -> tuple[str, bool] | None | object:
|
||||||
"""Return the api_base and whether the user explicitly selected or entered it."""
|
"""Return the api_base and whether the user explicitly selected or entered it."""
|
||||||
endpoint_choices = _QUICK_START_ENDPOINT_CHOICES.get(provider_name)
|
endpoint_choices = _QUICK_START_ENDPOINT_CHOICES.get(provider_name)
|
||||||
if endpoint_choices:
|
if endpoint_choices:
|
||||||
@@ -1533,7 +1602,9 @@ def _select_quick_start_api_base(
|
|||||||
list(choices) + ["<- Back"],
|
list(choices) + ["<- Back"],
|
||||||
default=endpoint_choices[0].label,
|
default=endpoint_choices[0].label,
|
||||||
)
|
)
|
||||||
if answer is _BACK_PRESSED or answer is None or answer == "<- Back":
|
if answer is _BACK_PRESSED or answer == "<- Back":
|
||||||
|
return _BACK_PRESSED
|
||||||
|
if answer is None:
|
||||||
return None
|
return None
|
||||||
assert isinstance(answer, str)
|
assert isinstance(answer, str)
|
||||||
return choices[answer], True
|
return choices[answer], True
|
||||||
@@ -1547,6 +1618,8 @@ def _select_quick_start_api_base(
|
|||||||
api_base,
|
api_base,
|
||||||
"str",
|
"str",
|
||||||
)
|
)
|
||||||
|
if base_answer is _BACK_PRESSED:
|
||||||
|
return _BACK_PRESSED
|
||||||
if base_answer is None:
|
if base_answer is None:
|
||||||
return None
|
return None
|
||||||
api_base = base_answer.strip().rstrip("/")
|
api_base = base_answer.strip().rstrip("/")
|
||||||
@@ -1556,73 +1629,82 @@ def _select_quick_start_api_base(
|
|||||||
return api_base, True
|
return api_base, True
|
||||||
|
|
||||||
|
|
||||||
def _configure_quick_start_provider(config: Config) -> bool:
|
def _configure_quick_start_provider(config: Config) -> bool | object:
|
||||||
"""Configure the beginner path from provider credentials and model."""
|
"""Configure the beginner path from provider credentials and model."""
|
||||||
_show_quick_start_progress(1)
|
while True:
|
||||||
|
_show_quick_start_progress(1)
|
||||||
|
|
||||||
provider_choices = _get_quick_start_provider_choices()
|
provider_choices = _get_quick_start_provider_choices()
|
||||||
answer = _select_with_back(
|
answer = _select_with_back(
|
||||||
"Which provider do you want to use?",
|
"Which provider do you want to use?",
|
||||||
list(provider_choices) + ["<- Back"],
|
list(provider_choices) + ["<- Back"],
|
||||||
)
|
)
|
||||||
if answer is _BACK_PRESSED or answer is None or answer == "<- Back":
|
if answer is _BACK_PRESSED or answer is None or answer == "<- Back":
|
||||||
return False
|
return _BACK_PRESSED
|
||||||
assert isinstance(answer, str)
|
assert isinstance(answer, str)
|
||||||
provider_name = provider_choices[answer]
|
provider_name = provider_choices[answer]
|
||||||
provider_info = _get_quick_start_provider_info().get(provider_name)
|
provider_info = _get_quick_start_provider_info().get(provider_name)
|
||||||
|
|
||||||
api_base = provider_info.default_api_base if provider_info else ""
|
api_base = provider_info.default_api_base if provider_info else ""
|
||||||
base_was_prompted = False
|
base_was_prompted = False
|
||||||
if provider_name in _QUICK_START_ENDPOINT_CHOICES:
|
if provider_name in _QUICK_START_ENDPOINT_CHOICES:
|
||||||
api_base_result = _select_quick_start_api_base(provider_name, answer, provider_info)
|
api_base_result = _select_quick_start_api_base(provider_name, answer, provider_info)
|
||||||
if api_base_result is None:
|
if api_base_result is _BACK_PRESSED:
|
||||||
return False
|
continue
|
||||||
api_base, base_was_prompted = api_base_result
|
if api_base_result is None:
|
||||||
|
return False
|
||||||
|
api_base, base_was_prompted = api_base_result
|
||||||
|
|
||||||
api_key: str | None = None
|
api_key: str | None = None
|
||||||
if _quick_start_requires_api_key(provider_name, provider_info):
|
if _quick_start_requires_api_key(provider_name, provider_info):
|
||||||
api_key = _input_text(f"{answer} API key", "", "str")
|
api_key = _input_text(f"{answer} API key", "", "str")
|
||||||
if api_key is None:
|
if api_key is _BACK_PRESSED:
|
||||||
return False
|
continue
|
||||||
api_key = api_key.strip()
|
if api_key is None:
|
||||||
if not api_key:
|
return False
|
||||||
console.print("[yellow]! API key is required for Quick Start[/yellow]")
|
api_key = api_key.strip()
|
||||||
|
if not api_key:
|
||||||
|
console.print("[yellow]! API key is required for Quick Start[/yellow]")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if (
|
||||||
|
provider_name not in _QUICK_START_ENDPOINT_CHOICES
|
||||||
|
and _quick_start_requires_base_url(provider_name, provider_info)
|
||||||
|
):
|
||||||
|
api_base_result = _select_quick_start_api_base(provider_name, answer, provider_info)
|
||||||
|
if api_base_result is _BACK_PRESSED:
|
||||||
|
continue
|
||||||
|
if api_base_result is None:
|
||||||
|
return False
|
||||||
|
api_base, base_was_prompted = api_base_result
|
||||||
|
|
||||||
|
provider_config = getattr(config.providers, provider_name, None)
|
||||||
|
if provider_config is None:
|
||||||
|
console.print(f"[red]Unknown provider: {provider_name}[/red]")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if (
|
model = _input_model_with_autocomplete("Model ID", "", provider_name)
|
||||||
provider_name not in _QUICK_START_ENDPOINT_CHOICES
|
if model is _BACK_PRESSED:
|
||||||
and _quick_start_requires_base_url(provider_name, provider_info)
|
continue
|
||||||
):
|
model = (model or "").strip()
|
||||||
api_base_result = _select_quick_start_api_base(provider_name, answer, provider_info)
|
if not model:
|
||||||
if api_base_result is None:
|
console.print("[yellow]! Model ID is required for Quick Start[/yellow]")
|
||||||
return False
|
return False
|
||||||
api_base, base_was_prompted = api_base_result
|
|
||||||
|
|
||||||
provider_config = getattr(config.providers, provider_name, None)
|
if api_key is not None:
|
||||||
if provider_config is None:
|
provider_config.api_key = api_key
|
||||||
console.print(f"[red]Unknown provider: {provider_name}[/red]")
|
if api_base:
|
||||||
return False
|
if base_was_prompted:
|
||||||
|
provider_config.api_base = api_base
|
||||||
|
elif not provider_config.api_base:
|
||||||
|
provider_config.api_base = api_base
|
||||||
|
|
||||||
model = _input_model_with_autocomplete("Model ID", "", provider_name)
|
_set_primary_quick_start_preset(
|
||||||
model = (model or "").strip()
|
config,
|
||||||
if not model:
|
provider_name,
|
||||||
console.print("[yellow]! Model ID is required for Quick Start[/yellow]")
|
model,
|
||||||
return False
|
)
|
||||||
|
return True
|
||||||
if api_key is not None:
|
|
||||||
provider_config.api_key = api_key
|
|
||||||
if api_base:
|
|
||||||
if base_was_prompted:
|
|
||||||
provider_config.api_base = api_base
|
|
||||||
elif not provider_config.api_base:
|
|
||||||
provider_config.api_base = api_base
|
|
||||||
|
|
||||||
_set_primary_quick_start_preset(
|
|
||||||
config,
|
|
||||||
provider_name,
|
|
||||||
model,
|
|
||||||
)
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def _enable_quick_start_websocket_defaults(config: Config) -> bool:
|
def _enable_quick_start_websocket_defaults(config: Config) -> bool:
|
||||||
@@ -1635,17 +1717,23 @@ def _enable_quick_start_websocket_defaults(config: Config) -> bool:
|
|||||||
f"[{_UI_MUTED}]This lets the browser UI at http://127.0.0.1:8765 connect to nanobot.[/]"
|
f"[{_UI_MUTED}]This lets the browser UI at http://127.0.0.1:8765 connect to nanobot.[/]"
|
||||||
)
|
)
|
||||||
console.print()
|
console.print()
|
||||||
answer = _get_questionary().confirm(
|
while True:
|
||||||
"Enable WebSocket channel now?",
|
answer = _get_questionary().confirm(
|
||||||
default=True,
|
"Enable WebSocket channel now?",
|
||||||
).ask()
|
default=True,
|
||||||
if not answer:
|
).ask()
|
||||||
console.print("[yellow]! Quick Start needs the WebSocket channel for the local WebUI[/yellow]")
|
if not answer:
|
||||||
return False
|
console.print(
|
||||||
webui_secret = _input_secret("Set a WebUI password")
|
"[yellow]! Quick Start needs the WebSocket channel for the local WebUI[/yellow]"
|
||||||
if not webui_secret:
|
)
|
||||||
console.print("[yellow]! WebUI password is required when enabling WebSocket[/yellow]")
|
return False
|
||||||
return False
|
webui_secret = _input_secret("Set a WebUI password")
|
||||||
|
if webui_secret is _BACK_PRESSED:
|
||||||
|
continue
|
||||||
|
if not webui_secret:
|
||||||
|
console.print("[yellow]! WebUI password is required when enabling WebSocket[/yellow]")
|
||||||
|
return False
|
||||||
|
break
|
||||||
|
|
||||||
config_cls = _get_channel_config_class("websocket")
|
config_cls = _get_channel_config_class("websocket")
|
||||||
if config_cls is None:
|
if config_cls is None:
|
||||||
@@ -1701,7 +1789,10 @@ def _configure_quick_start(config: Config) -> bool:
|
|||||||
"Choose provider endpoint, add credentials and model, then enable the local WebUI channel.",
|
"Choose provider endpoint, add credentials and model, then enable the local WebUI channel.",
|
||||||
)
|
)
|
||||||
draft = config.model_copy(deep=True)
|
draft = config.model_copy(deep=True)
|
||||||
if not _configure_quick_start_provider(draft):
|
provider_result = _configure_quick_start_provider(draft)
|
||||||
|
if provider_result is _BACK_PRESSED:
|
||||||
|
return False
|
||||||
|
if not provider_result:
|
||||||
_pause()
|
_pause()
|
||||||
return False
|
return False
|
||||||
if not _enable_quick_start_websocket_defaults(draft):
|
if not _enable_quick_start_websocket_defaults(draft):
|
||||||
@@ -1761,6 +1852,18 @@ def _get_main_menu_choices(has_unsaved_changes: bool) -> list[str]:
|
|||||||
def _configure_advanced_settings(config: Config) -> None:
|
def _configure_advanced_settings(config: Config) -> None:
|
||||||
"""Show lower-frequency setup options behind one advanced menu."""
|
"""Show lower-frequency setup options behind one advanced menu."""
|
||||||
last_choice: str | None = None
|
last_choice: str | None = None
|
||||||
|
choices = [
|
||||||
|
"[P] LLM Provider",
|
||||||
|
"[M] Model Presets",
|
||||||
|
"[C] Chat Channel",
|
||||||
|
"[H] Channel Common",
|
||||||
|
"[A] Agent Settings",
|
||||||
|
"[I] API Server",
|
||||||
|
"[G] Gateway",
|
||||||
|
"[T] Tools",
|
||||||
|
"[V] View Configuration Summary",
|
||||||
|
"<- Back",
|
||||||
|
]
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
console.clear()
|
console.clear()
|
||||||
@@ -1768,27 +1871,15 @@ def _configure_advanced_settings(config: Config) -> None:
|
|||||||
"Advanced Settings",
|
"Advanced Settings",
|
||||||
"Use these when the default API-key setup is not enough.",
|
"Use these when the default API-key setup is not enough.",
|
||||||
)
|
)
|
||||||
answer = _get_questionary().select(
|
answer = _select_with_back(
|
||||||
"What would you like to configure?",
|
"What would you like to configure?",
|
||||||
choices=[
|
choices,
|
||||||
"[P] LLM Provider",
|
|
||||||
"[M] Model Presets",
|
|
||||||
"[C] Chat Channel",
|
|
||||||
"[H] Channel Common",
|
|
||||||
"[A] Agent Settings",
|
|
||||||
"[I] API Server",
|
|
||||||
"[G] Gateway",
|
|
||||||
"[T] Tools",
|
|
||||||
"[V] View Configuration Summary",
|
|
||||||
"<- Back",
|
|
||||||
],
|
|
||||||
default=last_choice,
|
default=last_choice,
|
||||||
qmark=">",
|
)
|
||||||
).ask()
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
break
|
break
|
||||||
|
|
||||||
if answer is None or answer == "<- Back":
|
if answer is _BACK_PRESSED or answer is None or answer == "<- Back":
|
||||||
break
|
break
|
||||||
|
|
||||||
_advanced_dispatch = {
|
_advanced_dispatch = {
|
||||||
@@ -1830,22 +1921,19 @@ def run_onboard(initial_config: Config | None = None) -> OnboardResult:
|
|||||||
config = base_config.model_copy(deep=True)
|
config = base_config.model_copy(deep=True)
|
||||||
_sync_preset_cache(config)
|
_sync_preset_cache(config)
|
||||||
|
|
||||||
last_main_choice: str | None = None
|
|
||||||
while True:
|
while True:
|
||||||
console.clear()
|
console.clear()
|
||||||
_show_main_menu_header()
|
_show_main_menu_header()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
answer = _get_questionary().select(
|
answer = _select_with_back(
|
||||||
"What would you like to do?",
|
"What would you like to do?",
|
||||||
choices=_get_main_menu_choices(_has_unsaved_changes(original_config, config)),
|
_get_main_menu_choices(_has_unsaved_changes(original_config, config)),
|
||||||
default=last_main_choice,
|
)
|
||||||
qmark=">",
|
|
||||||
).ask()
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
answer = None
|
answer = None
|
||||||
|
|
||||||
if answer is None:
|
if answer is _BACK_PRESSED or answer is None:
|
||||||
action = _prompt_main_menu_exit(_has_unsaved_changes(original_config, config))
|
action = _prompt_main_menu_exit(_has_unsaved_changes(original_config, config))
|
||||||
if action == "save":
|
if action == "save":
|
||||||
return OnboardResult(config=config, should_save=True)
|
return OnboardResult(config=config, should_save=True)
|
||||||
@@ -1863,5 +1951,4 @@ def run_onboard(initial_config: Config | None = None) -> OnboardResult:
|
|||||||
if answer in {"[X] Exit", "[X] Exit Without Saving"}:
|
if answer in {"[X] Exit", "[X] Exit Without Saving"}:
|
||||||
return OnboardResult(config=original_config, should_save=False)
|
return OnboardResult(config=original_config, should_save=False)
|
||||||
if answer == "[A] Advanced Settings":
|
if answer == "[A] Advanced Settings":
|
||||||
last_main_choice = answer
|
|
||||||
_configure_advanced_settings(config)
|
_configure_advanced_settings(config)
|
||||||
|
|||||||
@@ -520,24 +520,18 @@ class TestRunOnboardExitBehavior:
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
class FakePrompt:
|
def fake_select_with_back(*_args, **_kwargs):
|
||||||
def __init__(self, response):
|
response = next(responses)
|
||||||
self.response = response
|
if isinstance(response, BaseException):
|
||||||
|
raise response
|
||||||
def ask(self):
|
return response
|
||||||
if isinstance(self.response, BaseException):
|
|
||||||
raise self.response
|
|
||||||
return self.response
|
|
||||||
|
|
||||||
def fake_select(*_args, **_kwargs):
|
|
||||||
return FakePrompt(next(responses))
|
|
||||||
|
|
||||||
def fake_configure_general_settings(config, section):
|
def fake_configure_general_settings(config, section):
|
||||||
if section == "Agent Settings":
|
if section == "Agent Settings":
|
||||||
config.agents.defaults.model = "test/provider-model"
|
config.agents.defaults.model = "test/provider-model"
|
||||||
|
|
||||||
monkeypatch.setattr(onboard_wizard, "_show_main_menu_header", lambda: None)
|
monkeypatch.setattr(onboard_wizard, "_show_main_menu_header", lambda: None)
|
||||||
monkeypatch.setattr(onboard_wizard, "questionary", SimpleNamespace(select=fake_select))
|
monkeypatch.setattr(onboard_wizard, "_select_with_back", fake_select_with_back)
|
||||||
monkeypatch.setattr(onboard_wizard, "_configure_general_settings", fake_configure_general_settings)
|
monkeypatch.setattr(onboard_wizard, "_configure_general_settings", fake_configure_general_settings)
|
||||||
|
|
||||||
result = run_onboard(initial_config=initial_config)
|
result = run_onboard(initial_config=initial_config)
|
||||||
@@ -868,6 +862,22 @@ class TestApiServerRegistration:
|
|||||||
class TestMainMenuUpdate:
|
class TestMainMenuUpdate:
|
||||||
"""Tests for main menu including new Channel Common and API Server items."""
|
"""Tests for main menu including new Channel Common and API Server items."""
|
||||||
|
|
||||||
|
def test_choice_viewport_keeps_long_menus_within_terminal_height(self):
|
||||||
|
"""Long provider menus should render as a bounded scrolling slice."""
|
||||||
|
assert onboard_wizard._choice_viewport(selected_index=0, total=20, visible_count=5) == (0, 5)
|
||||||
|
assert onboard_wizard._choice_viewport(selected_index=10, total=20, visible_count=5) == (
|
||||||
|
8,
|
||||||
|
13,
|
||||||
|
)
|
||||||
|
assert onboard_wizard._choice_viewport(selected_index=19, total=20, visible_count=5) == (
|
||||||
|
15,
|
||||||
|
20,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_choice_viewport_handles_tiny_terminals(self):
|
||||||
|
"""A one-row menu is still usable instead of failing as window-too-small."""
|
||||||
|
assert onboard_wizard._choice_viewport(selected_index=3, total=5, visible_count=0) == (3, 4)
|
||||||
|
|
||||||
def test_main_menu_hides_save_actions_until_needed(self):
|
def test_main_menu_hides_save_actions_until_needed(self):
|
||||||
"""The first screen should not show save or summary actions before edits."""
|
"""The first screen should not show save or summary actions before edits."""
|
||||||
from nanobot.cli.onboard import _get_main_menu_choices
|
from nanobot.cli.onboard import _get_main_menu_choices
|
||||||
@@ -893,22 +903,15 @@ class TestMainMenuUpdate:
|
|||||||
"[Q] Quick Start",
|
"[Q] Quick Start",
|
||||||
])
|
])
|
||||||
|
|
||||||
class FakePrompt:
|
def fake_select_with_back(*_args, **_kwargs):
|
||||||
def __init__(self, response):
|
return next(responses)
|
||||||
self.response = response
|
|
||||||
|
|
||||||
def ask(self):
|
|
||||||
return self.response
|
|
||||||
|
|
||||||
def fake_select(*_args, **_kwargs):
|
|
||||||
return FakePrompt(next(responses))
|
|
||||||
|
|
||||||
def fake_quick_start(config):
|
def fake_quick_start(config):
|
||||||
config.agents.defaults.bot_name = "quickbot"
|
config.agents.defaults.bot_name = "quickbot"
|
||||||
return True
|
return True
|
||||||
|
|
||||||
monkeypatch.setattr(onboard_wizard, "_show_main_menu_header", lambda: None)
|
monkeypatch.setattr(onboard_wizard, "_show_main_menu_header", lambda: None)
|
||||||
monkeypatch.setattr(onboard_wizard, "questionary", SimpleNamespace(select=fake_select))
|
monkeypatch.setattr(onboard_wizard, "_select_with_back", fake_select_with_back)
|
||||||
monkeypatch.setattr(onboard_wizard, "_configure_quick_start", fake_quick_start)
|
monkeypatch.setattr(onboard_wizard, "_configure_quick_start", fake_quick_start)
|
||||||
|
|
||||||
result = run_onboard(initial_config=initial_config)
|
result = run_onboard(initial_config=initial_config)
|
||||||
@@ -916,6 +919,46 @@ class TestMainMenuUpdate:
|
|||||||
assert result.should_save is True
|
assert result.should_save is True
|
||||||
assert result.config.agents.defaults.bot_name == "quickbot"
|
assert result.config.agents.defaults.bot_name == "quickbot"
|
||||||
|
|
||||||
|
def test_main_menu_default_resets_after_returning_from_advanced(self, monkeypatch):
|
||||||
|
"""Returning from Advanced should not leave its item visually selected."""
|
||||||
|
initial_config = Config()
|
||||||
|
responses = iter([
|
||||||
|
"[A] Advanced Settings",
|
||||||
|
"<- Back",
|
||||||
|
"[X] Exit",
|
||||||
|
])
|
||||||
|
main_defaults: list[str | None] = []
|
||||||
|
|
||||||
|
def fake_select_with_back(prompt, _choices, default=None):
|
||||||
|
if prompt == "What would you like to do?":
|
||||||
|
main_defaults.append(default)
|
||||||
|
return next(responses)
|
||||||
|
|
||||||
|
monkeypatch.setattr(onboard_wizard, "_show_main_menu_header", lambda: None)
|
||||||
|
monkeypatch.setattr(onboard_wizard, "_show_section_header", lambda *a, **kw: None)
|
||||||
|
monkeypatch.setattr(onboard_wizard, "_select_with_back", fake_select_with_back)
|
||||||
|
|
||||||
|
result = run_onboard(initial_config=initial_config)
|
||||||
|
|
||||||
|
assert result.should_save is False
|
||||||
|
assert main_defaults == [None, None]
|
||||||
|
|
||||||
|
def test_ask_prompt_shortens_escape_timeout(self):
|
||||||
|
"""Questionary text prompts should not wait the default timeout on Escape."""
|
||||||
|
|
||||||
|
class FakePrompt:
|
||||||
|
def __init__(self):
|
||||||
|
self.application = SimpleNamespace(ttimeoutlen=0.5, timeoutlen=1.0)
|
||||||
|
|
||||||
|
def ask(self):
|
||||||
|
return "ok"
|
||||||
|
|
||||||
|
prompt = FakePrompt()
|
||||||
|
|
||||||
|
assert onboard_wizard._ask_prompt(prompt) == "ok"
|
||||||
|
assert prompt.application.ttimeoutlen == onboard_wizard._PROMPT_ESCAPE_TIMEOUT_SECONDS
|
||||||
|
assert prompt.application.timeoutlen == onboard_wizard._PROMPT_ESCAPE_TIMEOUT_SECONDS
|
||||||
|
|
||||||
def test_quick_start_provider_choices_include_all_chat_providers(self):
|
def test_quick_start_provider_choices_include_all_chat_providers(self):
|
||||||
"""Quick Start should be driven by the provider registry, not a short allowlist."""
|
"""Quick Start should be driven by the provider registry, not a short allowlist."""
|
||||||
from nanobot.providers.registry import PROVIDERS
|
from nanobot.providers.registry import PROVIDERS
|
||||||
@@ -983,6 +1026,45 @@ class TestMainMenuUpdate:
|
|||||||
assert websocket["websocketRequiresToken"] is True
|
assert websocket["websocketRequiresToken"] is True
|
||||||
assert websocket["tokenIssueSecret"] == "webui-secret"
|
assert websocket["tokenIssueSecret"] == "webui-secret"
|
||||||
|
|
||||||
|
def test_quick_start_provider_menu_escape_returns_back(self, monkeypatch):
|
||||||
|
"""Esc from the first Quick Start menu should return to the main menu."""
|
||||||
|
config = Config()
|
||||||
|
|
||||||
|
monkeypatch.setattr(onboard_wizard, "_show_quick_start_progress", lambda *_args: None)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
onboard_wizard,
|
||||||
|
"_select_with_back",
|
||||||
|
lambda *a, **kw: onboard_wizard._BACK_PRESSED,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert onboard_wizard._configure_quick_start_provider(config) is onboard_wizard._BACK_PRESSED
|
||||||
|
assert "primary" not in config.model_presets
|
||||||
|
|
||||||
|
def test_quick_start_provider_back_skips_pause(self, monkeypatch):
|
||||||
|
"""Returning from Quick Start should not require an extra Enter key press."""
|
||||||
|
config = Config()
|
||||||
|
pause_messages: list[str] = []
|
||||||
|
|
||||||
|
def fail_websocket_defaults(*_args, **_kwargs):
|
||||||
|
raise AssertionError("Back navigation should not continue Quick Start")
|
||||||
|
|
||||||
|
monkeypatch.setattr(onboard_wizard.console, "clear", lambda: None)
|
||||||
|
monkeypatch.setattr(onboard_wizard, "_show_section_header", lambda *a, **kw: None)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
onboard_wizard,
|
||||||
|
"_configure_quick_start_provider",
|
||||||
|
lambda *_args: onboard_wizard._BACK_PRESSED,
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
onboard_wizard,
|
||||||
|
"_enable_quick_start_websocket_defaults",
|
||||||
|
fail_websocket_defaults,
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(onboard_wizard, "_pause", lambda message="": pause_messages.append(message))
|
||||||
|
|
||||||
|
assert onboard_wizard._configure_quick_start(config) is False
|
||||||
|
assert pause_messages == []
|
||||||
|
|
||||||
def test_quick_start_websocket_decline_rolls_back_provider_defaults(self, monkeypatch):
|
def test_quick_start_websocket_decline_rolls_back_provider_defaults(self, monkeypatch):
|
||||||
"""A failed WebSocket step should not leave saveable Quick Start defaults behind."""
|
"""A failed WebSocket step should not leave saveable Quick Start defaults behind."""
|
||||||
config = Config()
|
config = Config()
|
||||||
@@ -1085,6 +1167,34 @@ class TestMainMenuUpdate:
|
|||||||
assert config.model_presets["primary"].provider == "openai"
|
assert config.model_presets["primary"].provider == "openai"
|
||||||
assert config.model_presets["primary"].model == "gpt-4o-mini"
|
assert config.model_presets["primary"].model == "gpt-4o-mini"
|
||||||
|
|
||||||
|
def test_quick_start_api_key_escape_returns_to_provider_choice(self, monkeypatch):
|
||||||
|
"""Esc from an API-key prompt should go back to provider selection."""
|
||||||
|
config = Config()
|
||||||
|
provider_answers = iter(["DeepSeek", "OpenAI"])
|
||||||
|
api_key_answers = iter([onboard_wizard._BACK_PRESSED, "sk-openai-test"])
|
||||||
|
selected_providers: list[str] = []
|
||||||
|
|
||||||
|
def fake_select(*_args, **_kwargs):
|
||||||
|
selected = next(provider_answers)
|
||||||
|
selected_providers.append(selected)
|
||||||
|
return selected
|
||||||
|
|
||||||
|
monkeypatch.setattr(onboard_wizard, "_show_quick_start_progress", lambda *_args: None)
|
||||||
|
monkeypatch.setattr(onboard_wizard, "_select_with_back", fake_select)
|
||||||
|
monkeypatch.setattr(onboard_wizard, "_input_text", lambda *a, **kw: next(api_key_answers))
|
||||||
|
monkeypatch.setattr(
|
||||||
|
onboard_wizard,
|
||||||
|
"_input_model_with_autocomplete",
|
||||||
|
lambda *a, **kw: "gpt-4o-mini",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert onboard_wizard._configure_quick_start_provider(config) is True
|
||||||
|
|
||||||
|
assert selected_providers == ["DeepSeek", "OpenAI"]
|
||||||
|
assert config.providers.deepseek.api_key is None
|
||||||
|
assert config.providers.openai.api_key == "sk-openai-test"
|
||||||
|
assert config.model_presets["primary"].provider == "openai"
|
||||||
|
|
||||||
def test_quick_start_zhipu_coding_plan_uses_coding_base_url(self, monkeypatch):
|
def test_quick_start_zhipu_coding_plan_uses_coding_base_url(self, monkeypatch):
|
||||||
"""Zhipu Coding Plan should not use the standard Zhipu base URL."""
|
"""Zhipu Coding Plan should not use the standard Zhipu base URL."""
|
||||||
config = Config()
|
config = Config()
|
||||||
@@ -1441,24 +1551,18 @@ class TestMainMenuUpdate:
|
|||||||
"[S] Save and Exit",
|
"[S] Save and Exit",
|
||||||
])
|
])
|
||||||
|
|
||||||
class FakePrompt:
|
def fake_select_with_back(*_args, **_kwargs):
|
||||||
def __init__(self, response):
|
response = next(responses)
|
||||||
self.response = response
|
if isinstance(response, BaseException):
|
||||||
|
raise response
|
||||||
def ask(self):
|
return response
|
||||||
if isinstance(self.response, BaseException):
|
|
||||||
raise self.response
|
|
||||||
return self.response
|
|
||||||
|
|
||||||
def fake_select(*_args, **_kwargs):
|
|
||||||
return FakePrompt(next(responses))
|
|
||||||
|
|
||||||
def fake_configure_general_settings(config, section):
|
def fake_configure_general_settings(config, section):
|
||||||
if section == "Channel Common":
|
if section == "Channel Common":
|
||||||
config.channels.send_tool_hints = True
|
config.channels.send_tool_hints = True
|
||||||
|
|
||||||
monkeypatch.setattr(onboard_wizard, "_show_main_menu_header", lambda: None)
|
monkeypatch.setattr(onboard_wizard, "_show_main_menu_header", lambda: None)
|
||||||
monkeypatch.setattr(onboard_wizard, "questionary", SimpleNamespace(select=fake_select))
|
monkeypatch.setattr(onboard_wizard, "_select_with_back", fake_select_with_back)
|
||||||
monkeypatch.setattr(onboard_wizard, "_configure_general_settings", fake_configure_general_settings)
|
monkeypatch.setattr(onboard_wizard, "_configure_general_settings", fake_configure_general_settings)
|
||||||
|
|
||||||
result = run_onboard(initial_config=initial_config)
|
result = run_onboard(initial_config=initial_config)
|
||||||
@@ -1477,24 +1581,18 @@ class TestMainMenuUpdate:
|
|||||||
"[S] Save and Exit",
|
"[S] Save and Exit",
|
||||||
])
|
])
|
||||||
|
|
||||||
class FakePrompt:
|
def fake_select_with_back(*_args, **_kwargs):
|
||||||
def __init__(self, response):
|
response = next(responses)
|
||||||
self.response = response
|
if isinstance(response, BaseException):
|
||||||
|
raise response
|
||||||
def ask(self):
|
return response
|
||||||
if isinstance(self.response, BaseException):
|
|
||||||
raise self.response
|
|
||||||
return self.response
|
|
||||||
|
|
||||||
def fake_select(*_args, **_kwargs):
|
|
||||||
return FakePrompt(next(responses))
|
|
||||||
|
|
||||||
def fake_configure_general_settings(config, section):
|
def fake_configure_general_settings(config, section):
|
||||||
if section == "API Server":
|
if section == "API Server":
|
||||||
config.api.port = 9999
|
config.api.port = 9999
|
||||||
|
|
||||||
monkeypatch.setattr(onboard_wizard, "_show_main_menu_header", lambda: None)
|
monkeypatch.setattr(onboard_wizard, "_show_main_menu_header", lambda: None)
|
||||||
monkeypatch.setattr(onboard_wizard, "questionary", SimpleNamespace(select=fake_select))
|
monkeypatch.setattr(onboard_wizard, "_select_with_back", fake_select_with_back)
|
||||||
monkeypatch.setattr(onboard_wizard, "_configure_general_settings", fake_configure_general_settings)
|
monkeypatch.setattr(onboard_wizard, "_configure_general_settings", fake_configure_general_settings)
|
||||||
|
|
||||||
result = run_onboard(initial_config=initial_config)
|
result = run_onboard(initial_config=initial_config)
|
||||||
@@ -1514,23 +1612,17 @@ class TestMainMenuUpdate:
|
|||||||
"[X] Exit",
|
"[X] Exit",
|
||||||
])
|
])
|
||||||
|
|
||||||
class FakePrompt:
|
def fake_select_with_back(*_args, **_kwargs):
|
||||||
def __init__(self, response):
|
response = next(responses)
|
||||||
self.response = response
|
if isinstance(response, BaseException):
|
||||||
|
raise response
|
||||||
def ask(self):
|
return response
|
||||||
if isinstance(self.response, BaseException):
|
|
||||||
raise self.response
|
|
||||||
return self.response
|
|
||||||
|
|
||||||
def fake_select(*_args, **_kwargs):
|
|
||||||
return FakePrompt(next(responses))
|
|
||||||
|
|
||||||
def fake_pause():
|
def fake_pause():
|
||||||
pause_called["n"] += 1
|
pause_called["n"] += 1
|
||||||
|
|
||||||
monkeypatch.setattr(onboard_wizard, "_show_main_menu_header", lambda: None)
|
monkeypatch.setattr(onboard_wizard, "_show_main_menu_header", lambda: None)
|
||||||
monkeypatch.setattr(onboard_wizard, "questionary", SimpleNamespace(select=fake_select))
|
monkeypatch.setattr(onboard_wizard, "_select_with_back", fake_select_with_back)
|
||||||
# _pause is called inside _show_summary, so we patch it there
|
# _pause is called inside _show_summary, so we patch it there
|
||||||
monkeypatch.setattr(onboard_wizard, "_pause", fake_pause)
|
monkeypatch.setattr(onboard_wizard, "_pause", fake_pause)
|
||||||
# Suppress summary output but still call _pause
|
# Suppress summary output but still call _pause
|
||||||
@@ -1569,6 +1661,19 @@ class TestInputTextEmptyString:
|
|||||||
result = _input_text("Name", "old", "str")
|
result = _input_text("Name", "old", "str")
|
||||||
assert result is None
|
assert result is None
|
||||||
|
|
||||||
|
def test_escape_returns_back_pressed(self, monkeypatch):
|
||||||
|
"""_input_text should preserve the local back sentinel."""
|
||||||
|
monkeypatch.setattr(
|
||||||
|
onboard_wizard,
|
||||||
|
"_get_questionary",
|
||||||
|
lambda: SimpleNamespace(
|
||||||
|
text=lambda *a, **kw: SimpleNamespace(ask=lambda: onboard_wizard._BACK_PRESSED)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
result = _input_text("Name", "old", "str")
|
||||||
|
assert result is onboard_wizard._BACK_PRESSED
|
||||||
|
|
||||||
|
|
||||||
class TestIsStrOrNone:
|
class TestIsStrOrNone:
|
||||||
"""Tests for _is_str_or_none helper."""
|
"""Tests for _is_str_or_none helper."""
|
||||||
@@ -1697,13 +1802,8 @@ class TestModelPresetWizard:
|
|||||||
self.response = response
|
self.response = response
|
||||||
|
|
||||||
def ask(self):
|
def ask(self):
|
||||||
if isinstance(self.response, BaseException):
|
|
||||||
raise self.response
|
|
||||||
return self.response
|
return self.response
|
||||||
|
|
||||||
def fake_select(*_args, **_kwargs):
|
|
||||||
return FakePrompt(next(responses))
|
|
||||||
|
|
||||||
def fake_text(*_args, **_kwargs):
|
def fake_text(*_args, **_kwargs):
|
||||||
return FakePrompt(next(responses))
|
return FakePrompt(next(responses))
|
||||||
|
|
||||||
@@ -1714,9 +1814,7 @@ class TestModelPresetWizard:
|
|||||||
return next(responses)
|
return next(responses)
|
||||||
|
|
||||||
monkeypatch.setattr(onboard_wizard, "_select_with_back", fake_select_with_back)
|
monkeypatch.setattr(onboard_wizard, "_select_with_back", fake_select_with_back)
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(onboard_wizard, "questionary", SimpleNamespace(text=fake_text))
|
||||||
onboard_wizard, "questionary", SimpleNamespace(select=fake_select, text=fake_text)
|
|
||||||
)
|
|
||||||
monkeypatch.setattr(onboard_wizard, "_configure_pydantic_model", fake_configure)
|
monkeypatch.setattr(onboard_wizard, "_configure_pydantic_model", fake_configure)
|
||||||
monkeypatch.setattr(onboard_wizard, "_show_section_header", lambda *a, **kw: None)
|
monkeypatch.setattr(onboard_wizard, "_show_section_header", lambda *a, **kw: None)
|
||||||
monkeypatch.setattr(onboard_wizard, "console", SimpleNamespace(clear=lambda: None))
|
monkeypatch.setattr(onboard_wizard, "console", SimpleNamespace(clear=lambda: None))
|
||||||
@@ -1829,17 +1927,11 @@ class TestModelPresetWizard:
|
|||||||
"[S] Save and Exit",
|
"[S] Save and Exit",
|
||||||
])
|
])
|
||||||
|
|
||||||
class FakePrompt:
|
def fake_select_with_back(*_args, **_kwargs):
|
||||||
def __init__(self, response):
|
response = next(responses)
|
||||||
self.response = response
|
if isinstance(response, BaseException):
|
||||||
|
raise response
|
||||||
def ask(self):
|
return response
|
||||||
if isinstance(self.response, BaseException):
|
|
||||||
raise self.response
|
|
||||||
return self.response
|
|
||||||
|
|
||||||
def fake_select(*_args, **_kwargs):
|
|
||||||
return FakePrompt(next(responses))
|
|
||||||
|
|
||||||
preset_mutated = {"n": 0}
|
preset_mutated = {"n": 0}
|
||||||
|
|
||||||
@@ -1847,7 +1939,7 @@ class TestModelPresetWizard:
|
|||||||
preset_mutated["n"] += 1
|
preset_mutated["n"] += 1
|
||||||
config.model_presets["test"] = ModelPresetConfig(model="gpt-test")
|
config.model_presets["test"] = ModelPresetConfig(model="gpt-test")
|
||||||
|
|
||||||
monkeypatch.setattr(onboard_wizard, "questionary", SimpleNamespace(select=fake_select))
|
monkeypatch.setattr(onboard_wizard, "_select_with_back", fake_select_with_back)
|
||||||
monkeypatch.setattr(onboard_wizard, "_configure_model_presets", fake_configure_model_presets)
|
monkeypatch.setattr(onboard_wizard, "_configure_model_presets", fake_configure_model_presets)
|
||||||
monkeypatch.setattr(onboard_wizard, "_show_main_menu_header", lambda: None)
|
monkeypatch.setattr(onboard_wizard, "_show_main_menu_header", lambda: None)
|
||||||
monkeypatch.setattr(onboard_wizard, "_show_section_header", lambda *a, **kw: None)
|
monkeypatch.setattr(onboard_wizard, "_show_section_header", lambda *a, **kw: None)
|
||||||
|
|||||||
Reference in New Issue
Block a user