diff --git a/README.md b/README.md index d937ec57..06aee9fe 100644 --- a/README.md +++ b/README.md @@ -234,7 +234,7 @@ Windows PowerShell: irm https://raw.githubusercontent.com/HKUDS/nanobot/main/scripts/install.ps1 | iex ``` -The default command installs or upgrades `nanobot-ai` from PyPI, then starts `nanobot onboard --wizard`. It avoids system-wide pip installs by using an active virtual environment, `uv`, `pipx`, or a managed venv under `~/.nanobot/venv`. If Quick Start finishes, skip the manual initialize/configure steps below and go straight to **Test one message**. +The default command installs or upgrades `nanobot-ai` from PyPI, then starts `nanobot onboard --wizard`. It avoids system-wide pip installs by using an active virtual environment, `uv`, `pipx`, or a managed venv under `~/.nanobot/venv`. If Quick Start finishes and you enabled the WebSocket channel, skip the manual initialize/configure steps below and go straight to **Open the WebUI**. To preview the plan without changing your environment, pass `--dry-run`; combine it with `--dev` when you want to preview the main-branch install. @@ -353,7 +353,17 @@ For another provider, the same config shape still applies: | Model ID | `modelPresets.primary.model` | | Endpoint URL, only when needed | `providers..apiBase` | -**3. Test one message** +**3. Open the WebUI** + +If Quick Start enabled the WebSocket channel, start the gateway: + +```bash +nanobot gateway +``` + +Leave that terminal open, then open `http://127.0.0.1:8765` in your browser. Enter the WebUI password you set in the wizard, then send your first message there. + +For manual or terminal-only setup, test one CLI message: ```bash nanobot status diff --git a/docs/configuration.md b/docs/configuration.md index 997e6926..b370905e 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -201,7 +201,7 @@ Tracing covers the providers that go through nanobot's OpenAI-compatible client > - **VolcEngine / BytePlus Coding Plan**: Subscription endpoints are configured through dedicated providers `volcengineCodingPlan` or `byteplusCodingPlan`, separate from the pay-per-use `volcengine` / `byteplus` providers. > - **Zhipu Coding Plan**: If you're on Zhipu's coding plan, set `"apiBase": "https://open.bigmodel.cn/api/coding/paas/v4"` in your zhipu provider config. > - **Alibaba Cloud BaiLian**: If you're using Alibaba Cloud BaiLian's OpenAI-compatible endpoint, set `"apiBase": "https://dashscope.aliyuncs.com/compatible-mode/v1"` in your dashscope provider config. -> - **StepFun Step Plan**: If you're on StepFun's Step Plan subscription, set `"apiBase": "https://api.stepfun.com/step_plan/v1"` in your stepfun provider config. Supported models include `step-3.5-flash`, `step-3.5-flash-2603`, and `step-router-v1`. +> - **StepFun Step Plan**: If you're on StepFun's Step Plan subscription, set `"apiBase": "https://api.stepfun.ai/step_plan/v1"` in your stepfun provider config. Supported models include `step-3.5-flash`, `step-3.5-flash-2603`, and `step-router-v1`. > - **Step Fun (Mainland China)**: If your API key is from Step Fun's mainland China platform (stepfun.com), set `"apiBase": "https://api.stepfun.com/v1"` in your stepfun provider config. > - **Xiaomi MiMo thinking mode**: MiMo models (e.g. `mimo-v2.5-pro`) default to enabled thinking. Use `agents.defaults.reasoningEffort: "none"` to disable it, or `"low"` / `"medium"` / `"high"` to keep it on. Omitting the field preserves the provider's per-model default. > - **Xiaomi MiMo Token Plan**: If you're on MiMo's token plan, set `"apiBase": "https://token-plan-sgp.xiaomimimo.com/v1"` in your xiaomi_mimo provider config. @@ -752,7 +752,7 @@ Step Plan is StepFun's subscription-based service for high-frequency AI develope "providers": { "stepfun": { "apiKey": "${STEPFUN_API_KEY}", - "apiBase": "https://api.stepfun.com/step_plan/v1" + "apiBase": "https://api.stepfun.ai/step_plan/v1" } }, "modelPresets": { diff --git a/docs/image-generation.md b/docs/image-generation.md index c749d452..a7d0f2b4 100644 --- a/docs/image-generation.md +++ b/docs/image-generation.md @@ -272,7 +272,7 @@ StepPlan is StepFun's subscription tier and uses a different API base URL. The i "providers": { "stepfun": { "apiKey": "${STEPFUN_API_KEY}", - "apiBase": "https://api.stepfun.com/step_plan/v1" + "apiBase": "https://api.stepfun.ai/step_plan/v1" } }, "tools": { @@ -285,7 +285,7 @@ StepPlan is StepFun's subscription tier and uses a different API base URL. The i } ``` -`apiBase` takes precedence over the registry default, so with the StepPlan base URL configured, image requests are sent to `https://api.stepfun.com/step_plan/v1/images/generations` — the same path prefix used for LLM calls. The API key is shared with the standard StepFun provider. +`apiBase` takes precedence over the registry default, so with the StepPlan base URL configured, image requests are sent to `https://api.stepfun.ai/step_plan/v1/images/generations` — the same path prefix used for LLM calls. The API key is shared with the standard StepFun provider. ### Zhipu diff --git a/docs/quick-start.md b/docs/quick-start.md index 24038d71..f1343ddf 100644 --- a/docs/quick-start.md +++ b/docs/quick-start.md @@ -32,7 +32,7 @@ On Windows PowerShell: irm https://raw.githubusercontent.com/HKUDS/nanobot/main/scripts/install.ps1 | iex ``` -The default command installs or upgrades `nanobot-ai` from PyPI, then starts `nanobot onboard --wizard`. It avoids system-wide pip installs by using an active virtual environment, `uv`, `pipx`, or a managed venv under `~/.nanobot/venv`. If Quick Start finishes, skip the manual initialize/configure steps and go straight to [Check the Setup](#4-check-the-setup). +The default command installs or upgrades `nanobot-ai` from PyPI, then starts `nanobot onboard --wizard`. It avoids system-wide pip installs by using an active virtual environment, `uv`, `pipx`, or a managed venv under `~/.nanobot/venv`. If Quick Start finishes and you enabled the WebSocket channel, go straight to [Open the WebUI](#5-open-the-webui). To preview the plan without changing your environment, pass `--dry-run`; combine it with `--dev` when you want to preview the main-branch install. @@ -233,7 +233,19 @@ Read it like this: | `Model` | The model or preset you expect. | | Provider list | Most providers can say `not set`; the provider used by the active preset should show a check mark, OAuth status, or local URL. | -## 5. Test One Message +## 5. Open the WebUI + +If Quick Start enabled the WebSocket channel, start the gateway: + +```bash +nanobot gateway +``` + +Leave that terminal open, then open `http://127.0.0.1:8765` in your browser. Enter the WebUI password you set in the wizard, then send your first message there. + +## 6. Test One CLI Message + +Use this path if you skipped Quick Start, declined the WebSocket channel, or want a terminal-only check. Run a one-shot CLI message: diff --git a/docs/start-without-technical-background.md b/docs/start-without-technical-background.md index 0d34641e..c839fa8f 100644 --- a/docs/start-without-technical-background.md +++ b/docs/start-without-technical-background.md @@ -167,6 +167,7 @@ Move through the wizard like this: |---|---| | A menu | Use the arrow keys to highlight an option, then press `Enter`. | | The provider menu | Choose the company or service you want to use. | +| An endpoint menu | Choose the standard API or subscription plan endpoint that matches your key. | | An API key field | Paste the key, then press `Enter`. | | A provider base URL field | Paste the provider base URL from its docs, then press `Enter`. | | The Model ID field | Paste a model name from your provider, then press `Enter`. | @@ -176,12 +177,15 @@ For the first setup, choose `[Q] Quick Start`. It configures the recommended loc 1. Choose `[Q] Quick Start`. 2. Choose the provider you want to use. -3. Paste your API key if the wizard asks for one. -4. Paste the provider base URL if the wizard asks for one. -5. Paste a model ID that provider can run. -6. Review the Quick Start summary. The wizard saves and exits when Quick Start finishes. +3. Choose the endpoint if the wizard asks, such as Standard API, Coding Plan, Token Plan, or Step Plan. +4. Paste your API key if the wizard asks for one. +5. Paste the provider base URL if the wizard asks for one. +6. Paste a model ID that provider can run. +7. Confirm that Quick Start should enable the WebSocket channel for the local WebUI. +8. Set the WebUI password when prompted. +9. Review the Quick Start summary. The wizard saves and exits when Quick Start finishes. -The recommended path enables the local WebUI and default AI settings. You do not need to choose a chat channel for the first run. +The recommended path enables `channels.websocket` for the local WebUI, requires a WebUI password, and writes default AI settings. You do not need to choose a separate chat app for the first run. If you already know that you need custom headers, provider-specific request fields, a chat app, or tools, choose `Advanced Settings` instead. [`provider-cookbook.md`](./provider-cookbook.md) has copyable examples for several common provider setups. After you change advanced settings, a save option appears in the main menu. Choose `[S] Save and Exit`. @@ -192,7 +196,7 @@ The wizard creates or updates: | `~/.nanobot/config.json` | Settings file. | | `~/.nanobot/workspace/` | Working folder for memory, sessions, and generated files. | -If Quick Start finished successfully, skip to [Send the First Message](#7-send-the-first-message). The next two sections are only for manual setup. +If Quick Start finished successfully, skip to [Open the WebUI](#7-open-the-webui). The next two sections are only for manual setup. ## Manual Setup: How to Merge JSON Snippets @@ -294,7 +298,7 @@ For copyable provider-specific examples, use [`provider-cookbook.md`](./provider Save the file. -## 7. Send the First Message +## 7. Open the WebUI First check that nanobot can read the saved setup: @@ -312,7 +316,9 @@ Start the local browser UI: nanobot gateway ``` -Leave that terminal open, then open `http://127.0.0.1:8765` in your browser and send: +Leave that terminal open, then open `http://127.0.0.1:8765` in your browser. If Quick Start enabled the WebSocket channel, enter the WebUI password you set in the wizard. + +Send this first message in the browser: ```text Hello! diff --git a/docs/websocket.md b/docs/websocket.md index d6a816ac..24f95033 100644 --- a/docs/websocket.md +++ b/docs/websocket.md @@ -26,7 +26,8 @@ Add to `config.json` under `channels.websocket`: "host": "127.0.0.1", "port": 8765, "path": "/", - "websocketRequiresToken": false, + "tokenIssueSecret": "your-webui-password", + "websocketRequiresToken": true, "allowFrom": ["*"], "streaming": true } diff --git a/docs/webui.md b/docs/webui.md index 56fe2936..cf5aadda 100644 --- a/docs/webui.md +++ b/docs/webui.md @@ -15,10 +15,19 @@ First confirm your provider and model can answer: nanobot agent -m "Hello!" ``` -Then merge the WebSocket channel into your existing `~/.nanobot/config.json`: +Then merge the WebSocket channel into your existing `~/.nanobot/config.json`. +Set `tokenIssueSecret` to the password you will enter in the WebUI login form: ```json -{ "channels": { "websocket": { "enabled": true } } } +{ + "channels": { + "websocket": { + "enabled": true, + "tokenIssueSecret": "your-webui-password", + "websocketRequiresToken": true + } + } +} ``` If you are new to JSON snippets, see @@ -34,6 +43,7 @@ Leave the gateway running and open [`http://127.0.0.1:8765`](http://127.0.0.1:8765). The WebUI is served by the WebSocket channel on port `8765` by default. The gateway health endpoint, `18790` by default, is not the browser UI. +Enter `tokenIssueSecret` when the WebUI asks for a password. ## What It Is For diff --git a/nanobot/cli/onboard.py b/nanobot/cli/onboard.py index 1c4e78dd..18d135f9 100644 --- a/nanobot/cli/onboard.py +++ b/nanobot/cli/onboard.py @@ -46,6 +46,13 @@ class _QuickStartProviderInfo(NamedTuple): is_direct: bool +class _QuickStartEndpointChoice(NamedTuple): + """Provider endpoint option used by Quick Start.""" + + label: str + api_base: str + + # --- Field Hints for Select Fields --- # Maps field names to (choices, hint_text) # To add a new select field with hints, add an entry: @@ -69,7 +76,32 @@ _QUICK_START_CUSTOM_PROVIDER_CHOICE = "Other OpenAI-compatible" _CLEAR_CHOICE = "Clear value" _QUICK_START_MENU_CHOICE = "[Q] Quick Start" -_QUICK_START_STEPS = ("Provider + model", "WebUI", "Review") +_QUICK_START_STEPS = ("Provider setup", "WebSocket channel", "Review") +_QUICK_START_ENDPOINT_CHOICES: dict[str, tuple[_QuickStartEndpointChoice, ...]] = { + "zhipu": ( + _QuickStartEndpointChoice("Standard API", "https://open.bigmodel.cn/api/paas/v4"), + _QuickStartEndpointChoice("Coding Plan", "https://open.bigmodel.cn/api/coding/paas/v4"), + ), + "minimax": ( + _QuickStartEndpointChoice("Global API", "https://api.minimax.io/v1"), + _QuickStartEndpointChoice("Mainland China Token Plan", "https://api.minimaxi.com/v1"), + ), + "minimax_anthropic": ( + _QuickStartEndpointChoice("Global Anthropic API", "https://api.minimax.io/anthropic"), + _QuickStartEndpointChoice( + "Mainland China Anthropic Token Plan", + "https://api.minimaxi.com/anthropic", + ), + ), + "stepfun": ( + _QuickStartEndpointChoice("Standard API", "https://api.stepfun.com/v1"), + _QuickStartEndpointChoice("Step Plan", "https://api.stepfun.ai/step_plan/v1"), + ), + "xiaomi_mimo": ( + _QuickStartEndpointChoice("Standard API", "https://api.xiaomimimo.com/v1"), + _QuickStartEndpointChoice("Token Plan", "https://token-plan-sgp.xiaomimimo.com/v1"), + ), +} # Low-contrast terminal palette inspired by JetBrains Darcula/Islands. _UI_ACCENT = "#6B9BFA" @@ -487,6 +519,17 @@ def _input_text(display_name: str, current: Any, field_type: str, field_info=Non return value +def _input_secret(display_name: str) -> str | None: + """Get a secret value without echoing it when questionary supports password input.""" + prompt_factory = getattr(_get_questionary(), "password", None) + if prompt_factory is None: + prompt_factory = _get_questionary().text + value = prompt_factory(f"{display_name}:").ask() + if value is None: + return None + return str(value).strip() + + def _input_with_existing( display_name: str, current: Any, field_type: str, field_info=None ) -> Any: @@ -1467,6 +1510,8 @@ def _quick_start_requires_base_url(provider_name: str, info: _QuickStartProvider """Return whether Quick Start must ask for a provider base URL.""" if provider_name == "custom": return True + if provider_name in _QUICK_START_ENDPOINT_CHOICES: + return False if info is None or info.default_api_base: return False return info.backend == "azure_openai" or ( @@ -1474,6 +1519,43 @@ def _quick_start_requires_base_url(provider_name: str, info: _QuickStartProvider ) +def _select_quick_start_api_base( + provider_name: str, + provider_display: str, + info: _QuickStartProviderInfo | None, +) -> tuple[str, bool] | None: + """Return the api_base and whether the user explicitly selected or entered it.""" + endpoint_choices = _QUICK_START_ENDPOINT_CHOICES.get(provider_name) + if endpoint_choices: + choices = {choice.label: choice.api_base for choice in endpoint_choices} + answer = _select_with_back( + f"Which {provider_display} endpoint should Quick Start use?", + list(choices) + ["<- Back"], + default=endpoint_choices[0].label, + ) + if answer is _BACK_PRESSED or answer is None or answer == "<- Back": + return None + assert isinstance(answer, str) + return choices[answer], True + + api_base = info.default_api_base if info else "" + if not _quick_start_requires_base_url(provider_name, info): + return api_base, False + + base_answer = _input_text( + "Provider base URL", + api_base, + "str", + ) + if base_answer is None: + return None + api_base = base_answer.strip().rstrip("/") + if not api_base: + console.print("[yellow]! Provider base URL is required for this provider[/yellow]") + return None + return api_base, True + + def _configure_quick_start_provider(config: Config) -> bool: """Configure the beginner path from provider credentials and model.""" _show_quick_start_progress(1) @@ -1489,6 +1571,14 @@ def _configure_quick_start_provider(config: Config) -> bool: provider_name = provider_choices[answer] provider_info = _get_quick_start_provider_info().get(provider_name) + api_base = provider_info.default_api_base if provider_info else "" + base_was_prompted = False + if provider_name in _QUICK_START_ENDPOINT_CHOICES: + api_base_result = _select_quick_start_api_base(provider_name, answer, provider_info) + if api_base_result is None: + return False + api_base, base_was_prompted = api_base_result + api_key: str | None = None if _quick_start_requires_api_key(provider_name, provider_info): api_key = _input_text(f"{answer} API key", "", "str") @@ -1499,21 +1589,14 @@ def _configure_quick_start_provider(config: Config) -> bool: console.print("[yellow]! API key is required for Quick Start[/yellow]") return False - api_base = provider_info.default_api_base if provider_info else "" - base_was_prompted = False - if _quick_start_requires_base_url(provider_name, provider_info): - base_answer = _input_text( - "Provider base URL", - api_base, - "str", - ) - if base_answer is None: - return False - base_was_prompted = True - api_base = base_answer.strip().rstrip("/") - if not api_base: - console.print("[yellow]! Provider base URL is required for this provider[/yellow]") + 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 None: return False + api_base, base_was_prompted = api_base_result provider_config = getattr(config.providers, provider_name, None) if provider_config is None: @@ -1545,6 +1628,25 @@ def _configure_quick_start_provider(config: Config) -> bool: def _enable_quick_start_websocket_defaults(config: Config) -> bool: """Enable local WebUI with the default WebSocket settings.""" _show_quick_start_progress(2) + console.print( + f"[{_UI_ACCENT}]Quick Start will enable the WebSocket channel for the local WebUI.[/]" + ) + console.print( + f"[{_UI_MUTED}]This lets the browser UI at http://127.0.0.1:8765 connect to nanobot.[/]" + ) + console.print() + answer = _get_questionary().confirm( + "Enable WebSocket channel now?", + default=True, + ).ask() + if not answer: + console.print("[yellow]! Quick Start needs the WebSocket channel for the local WebUI[/yellow]") + return False + webui_secret = _input_secret("Set a WebUI password") + if not webui_secret: + console.print("[yellow]! WebUI password is required when enabling WebSocket[/yellow]") + return False + config_cls = _get_channel_config_class("websocket") if config_cls is None: console.print("[red]No configuration class found for websocket[/red]") @@ -1554,6 +1656,10 @@ def _enable_quick_start_websocket_defaults(config: Config) -> bool: model = config_cls.model_validate(current) if hasattr(model, "enabled"): setattr(model, "enabled", True) + if hasattr(model, "token_issue_secret"): + setattr(model, "token_issue_secret", webui_secret) + if hasattr(model, "websocket_requires_token"): + setattr(model, "websocket_requires_token", True) setattr(config.channels, "websocket", model.model_dump(by_alias=True, exclude_none=True)) return True @@ -1581,6 +1687,7 @@ def _show_quick_start_summary(config: Config) -> None: rows = [ ("Status", status), ("Next", next_step), + ("WebSocket channel", "enabled"), ("Open", "http://127.0.0.1:8765"), ] _print_summary_panel(rows, "Quick Start") @@ -1591,7 +1698,7 @@ def _configure_quick_start(config: Config) -> bool: console.clear() _show_section_header( "Quick Start", - "Choose the API provider, paste the key, enter the model, then use the local WebUI.", + "Choose provider endpoint, add credentials and model, then enable the local WebUI channel.", ) if not _configure_quick_start_provider(config): _pause() diff --git a/tests/agent/test_onboard_logic.py b/tests/agent/test_onboard_logic.py index eeb79882..7fb010de 100644 --- a/tests/agent/test_onboard_logic.py +++ b/tests/agent/test_onboard_logic.py @@ -942,6 +942,13 @@ class TestMainMenuUpdate: pause_messages: list[str] = [] + class FakePrompt: + def __init__(self, response): + self.response = response + + def ask(self): + return self.response + monkeypatch.setattr(onboard_wizard.console, "clear", lambda: None) monkeypatch.setattr(onboard_wizard, "_show_section_header", lambda *a, **kw: None) monkeypatch.setattr(onboard_wizard, "_select_with_back", lambda *a, **kw: "DeepSeek") @@ -951,6 +958,14 @@ class TestMainMenuUpdate: "_input_model_with_autocomplete", lambda *a, **kw: "deepseek-v4-flash", ) + monkeypatch.setattr( + onboard_wizard, + "questionary", + SimpleNamespace( + confirm=lambda *a, **kw: FakePrompt(True), + password=lambda *a, **kw: FakePrompt("webui-secret"), + ), + ) monkeypatch.setattr(onboard_wizard, "_configure_pydantic_model", fail_websocket_config) monkeypatch.setattr(onboard_wizard, "_print_summary_panel", lambda *a, **kw: None) monkeypatch.setattr(onboard_wizard, "_pause", lambda message="": pause_messages.append(message)) @@ -966,6 +981,7 @@ class TestMainMenuUpdate: websocket = getattr(config.channels, "websocket") assert websocket["enabled"] is True assert websocket["websocketRequiresToken"] is True + assert websocket["tokenIssueSecret"] == "webui-secret" def test_quick_start_provider_choice_asks_for_model_id(self, monkeypatch): """Known providers should ask users for the model instead of fetching one.""" @@ -1034,6 +1050,90 @@ class TestMainMenuUpdate: assert config.model_presets["primary"].provider == "openai" assert config.model_presets["primary"].model == "gpt-4o-mini" + def test_quick_start_zhipu_coding_plan_uses_coding_base_url(self, monkeypatch): + """Zhipu Coding Plan should not use the standard Zhipu base URL.""" + config = Config() + choices = iter(["Zhipu AI", "Coding Plan"]) + + monkeypatch.setattr(onboard_wizard, "_show_quick_start_progress", lambda *_args: None) + monkeypatch.setattr(onboard_wizard, "_select_with_back", lambda *a, **kw: next(choices)) + monkeypatch.setattr(onboard_wizard, "_input_text", lambda *a, **kw: "zhipu-key") + monkeypatch.setattr( + onboard_wizard, + "_input_model_with_autocomplete", + lambda *a, **kw: "glm-4.6", + ) + + assert onboard_wizard._configure_quick_start_provider(config) is True + + assert config.providers.zhipu.api_key == "zhipu-key" + assert config.providers.zhipu.api_base == "https://open.bigmodel.cn/api/coding/paas/v4" + assert config.model_presets["primary"].provider == "zhipu" + assert config.model_presets["primary"].model == "glm-4.6" + + def test_quick_start_minimax_mainland_token_plan_uses_mainland_base_url(self, monkeypatch): + """MiniMax mainland token plan should not use the global MiniMax base URL.""" + config = Config() + choices = iter(["MiniMax", "Mainland China Token Plan"]) + + monkeypatch.setattr(onboard_wizard, "_show_quick_start_progress", lambda *_args: None) + monkeypatch.setattr(onboard_wizard, "_select_with_back", lambda *a, **kw: next(choices)) + monkeypatch.setattr(onboard_wizard, "_input_text", lambda *a, **kw: "minimax-key") + monkeypatch.setattr( + onboard_wizard, + "_input_model_with_autocomplete", + lambda *a, **kw: "MiniMax-M2", + ) + + assert onboard_wizard._configure_quick_start_provider(config) is True + + assert config.providers.minimax.api_key == "minimax-key" + assert config.providers.minimax.api_base == "https://api.minimaxi.com/v1" + assert config.model_presets["primary"].provider == "minimax" + assert config.model_presets["primary"].model == "MiniMax-M2" + + def test_quick_start_stepfun_step_plan_uses_plan_base_url(self, monkeypatch): + """StepFun Step Plan should not use the standard StepFun base URL.""" + config = Config() + choices = iter(["Step Fun", "Step Plan"]) + + monkeypatch.setattr(onboard_wizard, "_show_quick_start_progress", lambda *_args: None) + monkeypatch.setattr(onboard_wizard, "_select_with_back", lambda *a, **kw: next(choices)) + monkeypatch.setattr(onboard_wizard, "_input_text", lambda *a, **kw: "stepfun-key") + monkeypatch.setattr( + onboard_wizard, + "_input_model_with_autocomplete", + lambda *a, **kw: "step-3.5-flash", + ) + + assert onboard_wizard._configure_quick_start_provider(config) is True + + assert config.providers.stepfun.api_key == "stepfun-key" + assert config.providers.stepfun.api_base == "https://api.stepfun.ai/step_plan/v1" + assert config.model_presets["primary"].provider == "stepfun" + assert config.model_presets["primary"].model == "step-3.5-flash" + + def test_quick_start_xiaomi_mimo_token_plan_uses_token_plan_base_url(self, monkeypatch): + """Xiaomi MiMo Token Plan should not use the standard MiMo base URL.""" + config = Config() + choices = iter(["Xiaomi MIMO", "Token Plan"]) + + monkeypatch.setattr(onboard_wizard, "_show_quick_start_progress", lambda *_args: None) + monkeypatch.setattr(onboard_wizard, "_select_with_back", lambda *a, **kw: next(choices)) + monkeypatch.setattr(onboard_wizard, "_input_text", lambda *a, **kw: "mimo-key") + monkeypatch.setattr( + onboard_wizard, + "_input_model_with_autocomplete", + lambda *a, **kw: "mimo-v2.5-pro", + ) + + assert onboard_wizard._configure_quick_start_provider(config) is True + + assert config.providers.xiaomi_mimo.api_key == "mimo-key" + assert config.providers.xiaomi_mimo.api_base == "https://token-plan-sgp.xiaomimimo.com/v1" + assert config.model_presets["primary"].provider == "xiaomi_mimo" + assert config.model_presets["primary"].model == "mimo-v2.5-pro" + def test_quick_start_custom_base_url_asks_for_model_id(self, monkeypatch): """Custom providers should ask for base URL and model ID.""" config = Config() @@ -1080,6 +1180,85 @@ class TestMainMenuUpdate: assert config.model_presets["primary"].provider == "azure_openai" assert config.model_presets["primary"].model == "deployment-name" + def test_quick_start_websocket_step_explains_channel_enablement(self, monkeypatch): + """Quick Start should confirm and protect WebSocket for WebUI.""" + config = Config() + messages: list[str] = [] + + class FakePrompt: + def __init__(self, response): + self.response = response + + def ask(self): + return self.response + + monkeypatch.setattr(onboard_wizard, "_show_quick_start_progress", lambda *_args: None) + monkeypatch.setattr(onboard_wizard.console, "print", lambda message="", *a, **kw: messages.append(str(message))) + monkeypatch.setattr( + onboard_wizard, + "questionary", + SimpleNamespace( + confirm=lambda *a, **kw: FakePrompt(True), + password=lambda *a, **kw: FakePrompt("webui-secret"), + ), + ) + + assert onboard_wizard._enable_quick_start_websocket_defaults(config) is True + + assert any("WebSocket channel" in message for message in messages) + assert any("http://127.0.0.1:8765" in message for message in messages) + websocket = getattr(config.channels, "websocket") + assert websocket["enabled"] is True + assert websocket["websocketRequiresToken"] is True + assert websocket["tokenIssueSecret"] == "webui-secret" + + def test_quick_start_websocket_step_can_be_declined(self, monkeypatch): + """Declining WebSocket should stop Quick Start before changing channel config.""" + config = Config() + + class FakePrompt: + def __init__(self, response): + self.response = response + + def ask(self): + return self.response + + monkeypatch.setattr(onboard_wizard, "_show_quick_start_progress", lambda *_args: None) + monkeypatch.setattr(onboard_wizard.console, "print", lambda *a, **kw: None) + monkeypatch.setattr( + onboard_wizard, + "questionary", + SimpleNamespace(confirm=lambda *a, **kw: FakePrompt(False)), + ) + + assert onboard_wizard._enable_quick_start_websocket_defaults(config) is False + assert getattr(config.channels, "websocket", None) is None + + def test_quick_start_websocket_requires_password(self, monkeypatch): + """Accepting WebSocket with an empty password should not enable the channel.""" + config = Config() + + class FakePrompt: + def __init__(self, response): + self.response = response + + def ask(self): + return self.response + + monkeypatch.setattr(onboard_wizard, "_show_quick_start_progress", lambda *_args: None) + monkeypatch.setattr(onboard_wizard.console, "print", lambda *a, **kw: None) + monkeypatch.setattr( + onboard_wizard, + "questionary", + SimpleNamespace( + confirm=lambda *a, **kw: FakePrompt(True), + password=lambda *a, **kw: FakePrompt(""), + ), + ) + + assert onboard_wizard._enable_quick_start_websocket_defaults(config) is False + assert getattr(config.channels, "websocket", None) is None + def test_quick_start_requires_api_key_before_setting_defaults(self, monkeypatch): """Quick Start should not create a ready-looking config without an API key.""" config = Config() @@ -1135,6 +1314,7 @@ class TestMainMenuUpdate: assert rows["Status"] == "DeepSeek API key missing" assert "API key" in rows["Next"] assert "nanobot gateway" in rows["Next"] + assert "agent -m" not in rows["Next"] assert labels.index("Next") < labels.index("Open") assert "Model" not in rows assert "Entry point" not in rows