From 5818569e8f20fe5b5050d43e6654bda4b18b575b Mon Sep 17 00:00:00 2001 From: chengyongru <2755839590@qq.com> Date: Sat, 18 Apr 2026 17:57:42 +0800 Subject: [PATCH] feat(wizard): auto-detect Literal fields as select menus Literal["standard", "persistent"] fields are now rendered as select dropdowns instead of free-text input. This makes provider_retry_mode and any future Literal fields self-documenting in the wizard. --- nanobot/cli/onboard.py | 13 ++++++++++++- tests/agent/test_onboard_logic.py | 18 ++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/nanobot/cli/onboard.py b/nanobot/cli/onboard.py index e363566b..4c570089 100644 --- a/nanobot/cli/onboard.py +++ b/nanobot/cli/onboard.py @@ -4,7 +4,7 @@ import json import types from dataclasses import dataclass from functools import lru_cache -from typing import Any, NamedTuple, get_args, get_origin +from typing import Any, Literal, NamedTuple, get_args, get_origin try: import questionary @@ -202,6 +202,8 @@ def _get_field_type_info(field_info) -> FieldTypeInfo: return FieldTypeInfo(name, None) if isinstance(annotation, type) and issubclass(annotation, BaseModel): return FieldTypeInfo("model", annotation) + if origin is Literal: + return FieldTypeInfo("literal", list(args)) return FieldTypeInfo("str", None) @@ -681,6 +683,15 @@ def _configure_pydantic_model( continue # Generic field input + if ftype.type_name == "literal" and ftype.inner_type: + select_choices = [str(v) for v in ftype.inner_type] + default_choice = str(current_value) if current_value in ftype.inner_type else select_choices[0] + new_value = _select_with_back(field_display, select_choices, default=default_choice) + if new_value is _BACK_PRESSED: + continue + if new_value is not None: + setattr(working_model, field_name, new_value) + continue if ftype.type_name == "bool": new_value = _input_bool(field_display, current_value) else: diff --git a/tests/agent/test_onboard_logic.py b/tests/agent/test_onboard_logic.py index 17c1f340..32927cfc 100644 --- a/tests/agent/test_onboard_logic.py +++ b/tests/agent/test_onboard_logic.py @@ -210,6 +210,24 @@ class TestGetFieldTypeInfo: assert type_name == "str" assert inner is None + def test_literal_type_returns_literal_with_choices(self): + """Literal["a", "b"] should return ("literal", ["a", "b"]).""" + from typing import Literal + + class Model(BaseModel): + mode: Literal["standard", "persistent"] = "standard" + + type_name, inner = _get_field_type_info(Model.model_fields["mode"]) + assert type_name == "literal" + assert inner == ["standard", "persistent"] + + def test_real_provider_retry_mode_field(self): + """Validate against actual AgentDefaults.provider_retry_mode field.""" + from nanobot.config.schema import AgentDefaults + + type_name, inner = _get_field_type_info(AgentDefaults.model_fields["provider_retry_mode"]) + assert type_name == "literal" + assert inner == ["standard", "persistent"] class TestGetFieldDisplayName: