fix: streamline quick start onboarding flow
Maintainer edit: continue the wizard simplification pass by making Quick Start save after the API key path, hiding save/summary actions until they are needed, removing failed-key side effects, and aligning beginner docs with the local WebUI path.
This commit is contained in:
@@ -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`. It avoids system-wide pip installs by using an active virtual environment, `uv`, `pipx`, or a managed venv under `~/.nanobot/venv`. If you finish the wizard and save the config, 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`. 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**.
|
||||
|
||||
To preview the plan without changing your environment, pass `--dry-run`; combine it with `--dev` when you want to preview the main-branch install.
|
||||
|
||||
@@ -290,7 +290,7 @@ nanobot --version
|
||||
|
||||
**1. Initialize**
|
||||
|
||||
Skip this step if the one-command setup already started the wizard and you saved the config there.
|
||||
Skip this step if the one-command setup already started the wizard and Quick Start finished there.
|
||||
|
||||
```bash
|
||||
nanobot onboard
|
||||
|
||||
+2
-2
@@ -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`. It avoids system-wide pip installs by using an active virtual environment, `uv`, `pipx`, or a managed venv under `~/.nanobot/venv`. If you finish the wizard and save the config, 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`. 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).
|
||||
|
||||
To preview the plan without changing your environment, pass `--dry-run`; combine it with `--dev` when you want to preview the main-branch install.
|
||||
|
||||
@@ -96,7 +96,7 @@ The docs use `python` in commands. If your system exposes Python 3.11+ as `pytho
|
||||
|
||||
## 2. Initialize
|
||||
|
||||
Skip this section if the one-command setup already started the wizard and you saved the config there.
|
||||
Skip this section if the one-command setup already started the wizard and Quick Start finished there.
|
||||
|
||||
```bash
|
||||
nanobot onboard
|
||||
|
||||
@@ -2,23 +2,20 @@
|
||||
|
||||
This page is for you if you have never used a terminal, edited a JSON file, or configured an AI model before.
|
||||
|
||||
The goal is small: get one local nanobot reply. Do not connect Telegram, Discord, WebUI, Docker, local models, or deployment yet. Those are easier after the first reply works.
|
||||
The goal is small: get one local nanobot reply in your browser. Do not connect Telegram, Discord, Docker, local models, or deployment yet. Those are easier after the first reply works.
|
||||
|
||||
## What You Are Setting Up
|
||||
|
||||
You will see these words during setup:
|
||||
You only need these words for Quick Start:
|
||||
|
||||
| Word | Plain meaning |
|
||||
|---|---|
|
||||
| Terminal | A text window where you paste commands and press Enter. |
|
||||
| Command | One line of text you run in the terminal. |
|
||||
| API key | A password-like token from an AI provider. Do not share it publicly. |
|
||||
| Provider | The service that owns the API key or local model endpoint. |
|
||||
| Model | The AI model ID that the provider can run. |
|
||||
| Config file | The settings file nanobot reads when it starts. |
|
||||
| Wizard | An interactive terminal menu that edits the config file for you. |
|
||||
| Model preset | A named model choice in the config file. |
|
||||
| `apiBase` | The HTTP address of a provider endpoint. Leave it blank unless your provider, proxy, or local server tells you to set one. |
|
||||
| Browser UI | The local web page where you chat with nanobot. |
|
||||
|
||||
## 1. Open a Terminal
|
||||
|
||||
@@ -64,8 +61,6 @@ If `python3` works but `python` does not, replace `python` with `python3` in the
|
||||
|
||||
nanobot does not create AI accounts or API keys for you. Use an AI provider account, company endpoint, subscription endpoint, or local model server that you already control. The steps below use OpenRouter because it is the recommended beginner path in the wizard; it is not a ranking or endorsement.
|
||||
|
||||
If you use another provider, keep the same shape but replace the provider name, API key, and model ID with values from that provider. [`provider-cookbook.md`](./provider-cookbook.md) has copyable snippets for several common patterns.
|
||||
|
||||
For the example path:
|
||||
|
||||
1. Open [openrouter.ai/keys](https://openrouter.ai/keys).
|
||||
@@ -161,12 +156,10 @@ The wizard is a terminal menu. It is not a graphical app, but it lets you choose
|
||||
You will see a menu like this:
|
||||
|
||||
```text
|
||||
> What would you like to configure?
|
||||
> What would you like to do?
|
||||
[Q] Quick Start (API key only)
|
||||
[A] Advanced Settings
|
||||
[V] View Configuration Summary
|
||||
[S] Save and Exit
|
||||
[X] Exit Without Saving
|
||||
[X] Exit
|
||||
```
|
||||
|
||||
Move through the wizard like this:
|
||||
@@ -174,33 +167,20 @@ Move through the wizard like this:
|
||||
| When you see | Do this |
|
||||
|---|---|
|
||||
| A menu | Use the arrow keys to highlight an option, then press `Enter`. |
|
||||
| A text field | Type or paste the value, then press `Enter`. |
|
||||
| A field you do not need | Keep the shown default or leave it blank, then press `Enter`. |
|
||||
| A back option | Choose it to return to the previous menu. |
|
||||
| The OpenRouter API key field | Paste the key, then press `Enter`. |
|
||||
| A back option in Advanced Settings | Choose it to return to the previous menu. |
|
||||
|
||||
For the first setup, choose `[Q] Quick Start (API key only)`. It configures the recommended local browser UI and model choice for you. Use `Advanced Settings` later only if you need a different provider, model, channel, gateway, or tool setup.
|
||||
For the first setup, choose `[Q] Quick Start (API key only)`. It configures the recommended local browser UI and default AI settings for you. Use `Advanced Settings` later only if you need a different provider, chat app, or tool setup.
|
||||
|
||||
If you are following the OpenRouter example:
|
||||
|
||||
1. Choose `[Q] Quick Start (API key only)`.
|
||||
2. Paste your OpenRouter API key, or press `Enter` and add it to the config file later.
|
||||
3. Review the Quick Start summary.
|
||||
4. Choose `[S] Save and Exit`.
|
||||
2. Paste your OpenRouter API key.
|
||||
3. Review the Quick Start summary. The wizard saves and exits when Quick Start finishes.
|
||||
|
||||
The recommended path enables the local WebUI with default WebSocket settings and uses a built-in model choice. You do not need to choose a model ID for the first run.
|
||||
The recommended path enables the local WebUI and default AI settings. You do not need to choose a model, endpoint, or chat channel for the first run.
|
||||
|
||||
If OpenRouter later says your account cannot use the built-in model choice, return to the wizard, choose `Advanced Settings`, then edit `Model Presets` and enter another OpenRouter model ID that your account can access.
|
||||
|
||||
If you already know that you need another provider, choose `Advanced Settings` instead of Quick Start and enter that provider's values:
|
||||
|
||||
| Wizard field | What to enter |
|
||||
|---|---|
|
||||
| Provider menu | The provider that owns your API key or endpoint. |
|
||||
| API key | The key from that provider, or leave it blank only if the provider does not use one. |
|
||||
| `apiBase` | Leave blank unless the provider docs, proxy docs, or local server docs give you a URL. |
|
||||
| Model ID | A model ID that provider can actually serve. |
|
||||
|
||||
Then choose `[S] Save and Exit`.
|
||||
If you already know that you need another provider, choose `Advanced Settings` instead of Quick Start. [`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`.
|
||||
|
||||
The wizard creates or updates:
|
||||
|
||||
@@ -209,7 +189,9 @@ The wizard creates or updates:
|
||||
| `~/.nanobot/config.json` | Settings file. |
|
||||
| `~/.nanobot/workspace/` | Working folder for memory, sessions, and generated files. |
|
||||
|
||||
## How to Merge JSON Snippets
|
||||
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.
|
||||
|
||||
## Manual Setup: How to Merge JSON Snippets
|
||||
|
||||
Most docs examples are snippets, not whole files. Your `config.json` has one outer `{ ... }`. Add new top-level sections such as `providers`, `modelPresets`, `agents`, or `channels` inside that same outer object.
|
||||
|
||||
@@ -243,7 +225,7 @@ Merge them into one object:
|
||||
|
||||
Notice the comma after the `providers` block. JSON needs commas between sibling sections, but not after the last section. If this feels hard, use `nanobot onboard` whenever possible.
|
||||
|
||||
## 6. Manual Config Fallback
|
||||
## 6. Manual Setup: Config Fallback
|
||||
|
||||
Use this only if the wizard is unavailable or you prefer opening the file yourself.
|
||||
|
||||
@@ -292,6 +274,11 @@ If this is a brand-new install and you have not configured anything else yet, re
|
||||
"defaults": {
|
||||
"modelPreset": "primary"
|
||||
}
|
||||
},
|
||||
"channels": {
|
||||
"websocket": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -314,15 +301,19 @@ This should show the config file path, workspace path, and the active model or p
|
||||
|
||||
It is normal for most providers to say `not set`. Only the provider you selected for the active preset needs to look configured.
|
||||
|
||||
Run:
|
||||
Start the local browser UI:
|
||||
|
||||
```bash
|
||||
nanobot agent -m "Hello!"
|
||||
nanobot gateway
|
||||
```
|
||||
|
||||
If that works, nanobot is installed and can call the model.
|
||||
Leave that terminal open, then open `http://127.0.0.1:8765` in your browser and send:
|
||||
|
||||
You should see a normal assistant reply in the terminal. The exact words will differ, but it should look like this shape:
|
||||
```text
|
||||
Hello!
|
||||
```
|
||||
|
||||
If that works, nanobot is installed and can call the model. You should see a normal assistant reply in the browser. The exact words will differ, but it should look like this shape:
|
||||
|
||||
```text
|
||||
Hello! How can I help you today?
|
||||
@@ -331,12 +322,12 @@ Hello! How can I help you today?
|
||||
If `nanobot` is not found, run:
|
||||
|
||||
```bash
|
||||
python -m nanobot agent -m "Hello!"
|
||||
python -m nanobot gateway
|
||||
```
|
||||
|
||||
Use `python3 -m nanobot agent -m "Hello!"` or `py -m nanobot agent -m "Hello!"` if that is the Python command that worked in step 2.
|
||||
Use `python3 -m nanobot gateway` or `py -m nanobot gateway` if that is the Python command that worked in step 2.
|
||||
|
||||
Once this works, nanobot can help with its own next setup step. Run `nanobot agent`, ask it to read these docs and update your current config for one specific goal, then run `/restart` when nanobot tells you the config is ready. For example, ask it to enable the browser UI, add one provider preset, or configure one chat app.
|
||||
Once this works, nanobot can help with its own next setup step. In the browser UI, ask it to read these docs and update your current config for one specific goal, then run `/restart` when nanobot tells you the config is ready. For example, ask it to add one provider preset or configure one chat app.
|
||||
|
||||
## 8. If Something Fails
|
||||
|
||||
@@ -346,7 +337,7 @@ Do not change many things at once. Check the exact error:
|
||||
|---|---|
|
||||
| `JSON parse error` | The config file has a missing comma, extra comma, or mismatched brace. Copy the example again. |
|
||||
| `401`, `unauthorized`, or `invalid API key` | The API key is wrong, expired, has extra spaces, or was pasted under the wrong provider. |
|
||||
| `model not found` | The model ID is not available through the selected provider or your account cannot use it. |
|
||||
| `model not found` | Your account cannot use the default model. Return to `nanobot onboard`, choose `Advanced Settings`, then edit `Model Presets`. |
|
||||
| `nanobot: command not found` | The install worked in Python, but your shell cannot find the script. Use `python -m nanobot ...`, `python3 -m nanobot ...`, or `py -m nanobot ...`, matching the Python command that worked earlier. |
|
||||
| No response after editing config | Restart the command. Long-running processes read config when they start. |
|
||||
|
||||
@@ -357,7 +348,7 @@ For a fuller diagnosis path, see [`troubleshooting.md`](./troubleshooting.md).
|
||||
Skip these until the first local message works:
|
||||
|
||||
- `apiBase`: hosted built-in providers often already have default endpoints. You only need `apiBase` for local models, proxies, custom OpenAI-compatible providers, or special regional/subscription endpoints.
|
||||
- WebUI and chat apps: first prove `nanobot agent -m "Hello!"`.
|
||||
- chat apps: first prove the local browser UI can answer.
|
||||
- fallback models: useful later, but not needed for the first reply.
|
||||
- Langfuse: useful for observability, but not needed for first setup.
|
||||
|
||||
@@ -365,22 +356,15 @@ Skip these until the first local message works:
|
||||
|
||||
After the first reply works, choose only one next goal. Keep the terminal that runs `nanobot gateway` open whenever you use the WebUI or a chat app.
|
||||
|
||||
### Open the Browser UI
|
||||
### Open the Browser UI Again
|
||||
|
||||
1. Add this snippet to `~/.nanobot/config.json`. Merge it into the existing file instead of replacing the whole file:
|
||||
|
||||
```json
|
||||
{ "channels": { "websocket": { "enabled": true } } }
|
||||
```
|
||||
|
||||
2. Run:
|
||||
Run:
|
||||
|
||||
```bash
|
||||
nanobot gateway
|
||||
```
|
||||
|
||||
3. Leave that terminal open.
|
||||
4. Open `http://127.0.0.1:8765` in your browser.
|
||||
Leave that terminal open, then open `http://127.0.0.1:8765` in your browser.
|
||||
|
||||
To stop the WebUI later, return to the gateway terminal and press `Ctrl+C`.
|
||||
|
||||
@@ -413,7 +397,7 @@ When you ask for help, include:
|
||||
- the command you ran;
|
||||
- `nanobot --version`;
|
||||
- `nanobot status`;
|
||||
- whether `nanobot agent -m "Hello!"` works;
|
||||
- whether the browser UI can answer `Hello!`;
|
||||
- the exact error text;
|
||||
- a config snippet with API keys and tokens removed.
|
||||
|
||||
|
||||
+53
-51
@@ -388,9 +388,9 @@ def _show_main_menu_header() -> None:
|
||||
body = Table.grid(expand=True)
|
||||
body.add_column(ratio=1)
|
||||
body.add_row(f"{__logo__} [bold {_UI_TEXT}]nanobot[/] [{_UI_MUTED}]v{__version__}[/]")
|
||||
body.add_row(f"[{_UI_ACCENT}]Quick Start only needs an API key.[/]")
|
||||
body.add_row(f"[{_UI_ACCENT}]Quick Start only needs an OpenRouter API key.[/]")
|
||||
body.add_row(
|
||||
f"[{_UI_MUTED}]Provider, channel, model, and gateway settings stay in Advanced.[/]"
|
||||
f"[{_UI_MUTED}]Use Advanced later for other providers or chat apps.[/]"
|
||||
)
|
||||
console.print(
|
||||
Panel(
|
||||
@@ -1377,9 +1377,9 @@ def _show_summary(config: Config) -> None:
|
||||
_pause()
|
||||
|
||||
|
||||
def _pause() -> None:
|
||||
def _pause(message: str = "Press Enter to continue...") -> None:
|
||||
"""Pause for user acknowledgement before clearing the screen."""
|
||||
_get_questionary().text("Press Enter to continue...", default="").ask()
|
||||
_get_questionary().text(message, default="").ask()
|
||||
|
||||
|
||||
# --- Quick Start ---
|
||||
@@ -1422,16 +1422,19 @@ def _configure_recommended_provider(config: Config) -> bool:
|
||||
_display, _is_gateway, _is_local, default_api_base = _get_provider_info().get(
|
||||
provider_name, (provider_name, False, False, "")
|
||||
)
|
||||
if default_api_base and not provider_config.api_base:
|
||||
provider_config.api_base = default_api_base
|
||||
|
||||
api_key = _input_with_existing(
|
||||
"OpenRouter API key (get one at https://openrouter.ai/keys; Enter to add later)",
|
||||
"OpenRouter API key (get one at https://openrouter.ai/keys)",
|
||||
provider_config.api_key,
|
||||
"str",
|
||||
)
|
||||
if api_key is not None:
|
||||
provider_config.api_key = api_key or None
|
||||
provider_config.api_key = api_key.strip() or None
|
||||
if not provider_config.api_key:
|
||||
console.print("[yellow]! OpenRouter API key is required for Quick Start[/yellow]")
|
||||
return False
|
||||
if default_api_base and not provider_config.api_base:
|
||||
provider_config.api_base = default_api_base
|
||||
|
||||
_set_primary_quick_start_preset(
|
||||
config,
|
||||
@@ -1457,49 +1460,35 @@ def _enable_quick_start_websocket_defaults(config: Config) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def _show_quick_start_summary(config: Config, channel_name: str | None) -> None:
|
||||
def _show_quick_start_summary(config: Config) -> None:
|
||||
"""Show the small summary users need before returning to the menu."""
|
||||
_show_quick_start_progress(3)
|
||||
preset = config.model_presets.get("primary")
|
||||
api_key_status = None
|
||||
provider_label = "AI provider"
|
||||
has_api_key = True
|
||||
if preset:
|
||||
provider_config = getattr(config.providers, preset.provider, None)
|
||||
_display, _is_gateway, is_local, _api_base = _get_provider_info().get(
|
||||
provider_label, _is_gateway, is_local, _api_base = _get_provider_info().get(
|
||||
preset.provider, (preset.provider, False, False, "")
|
||||
)
|
||||
if not is_local:
|
||||
api_key_status = (
|
||||
"configured"
|
||||
if provider_config and provider_config.api_key
|
||||
else "add later"
|
||||
)
|
||||
has_api_key = is_local or bool(provider_config and provider_config.api_key)
|
||||
|
||||
next_step = (
|
||||
"Save, then run `nanobot gateway`"
|
||||
if channel_name
|
||||
else "Save, then run `nanobot agent -m \"Hello!\"`"
|
||||
)
|
||||
if api_key_status == "add later":
|
||||
next_step = (
|
||||
"Save, add your API key to config, then run `nanobot gateway`"
|
||||
if channel_name
|
||||
else "Save, add your API key to config, then run `nanobot agent -m \"Hello!\"`"
|
||||
)
|
||||
start_command = "`nanobot gateway`"
|
||||
next_step = f"Run {start_command}"
|
||||
status = "Ready"
|
||||
if not has_api_key:
|
||||
status = f"{provider_label} API key missing"
|
||||
next_step = f"Add your {provider_label} API key, then run {start_command}"
|
||||
|
||||
rows = [
|
||||
("Provider", preset.provider if preset else "[not set]"),
|
||||
("Model", preset.model if preset else "[not set]"),
|
||||
("Entry point", "Not enabled yet" if channel_name is None else channel_name),
|
||||
("Status", status),
|
||||
("Next", next_step),
|
||||
("Open", "http://127.0.0.1:8765"),
|
||||
]
|
||||
if api_key_status:
|
||||
rows.insert(2, ("API key", api_key_status))
|
||||
if channel_name == "websocket":
|
||||
rows.append(("WebUI", "Open http://127.0.0.1:8765 after the gateway starts"))
|
||||
_print_summary_panel(rows, "Quick Start")
|
||||
|
||||
|
||||
def _configure_quick_start(config: Config) -> None:
|
||||
def _configure_quick_start(config: Config) -> bool:
|
||||
"""First-run path: API key + local WebUI, with advanced settings hidden."""
|
||||
console.clear()
|
||||
_show_section_header(
|
||||
@@ -1508,12 +1497,13 @@ def _configure_quick_start(config: Config) -> None:
|
||||
)
|
||||
if not _configure_recommended_provider(config):
|
||||
_pause()
|
||||
return
|
||||
return False
|
||||
if not _enable_quick_start_websocket_defaults(config):
|
||||
_pause()
|
||||
return
|
||||
_show_quick_start_summary(config, "websocket")
|
||||
_pause()
|
||||
return False
|
||||
_show_quick_start_summary(config)
|
||||
_pause("Press Enter to save and exit...")
|
||||
return True
|
||||
|
||||
|
||||
# --- Main Entry Point ---
|
||||
@@ -1547,6 +1537,19 @@ def _prompt_main_menu_exit(has_unsaved_changes: bool) -> str:
|
||||
return "resume"
|
||||
|
||||
|
||||
def _get_main_menu_choices(has_unsaved_changes: bool) -> list[str]:
|
||||
"""Return the top-level choices, keeping save actions hidden until needed."""
|
||||
choices = [
|
||||
"[Q] Quick Start (API key only)",
|
||||
"[A] Advanced Settings",
|
||||
]
|
||||
if has_unsaved_changes:
|
||||
choices.extend(["[S] Save and Exit", "[X] Exit Without Saving"])
|
||||
else:
|
||||
choices.append("[X] Exit")
|
||||
return choices
|
||||
|
||||
|
||||
def _configure_advanced_settings(config: Config) -> None:
|
||||
"""Show lower-frequency setup options behind one advanced menu."""
|
||||
last_choice: str | None = None
|
||||
@@ -1568,6 +1571,7 @@ def _configure_advanced_settings(config: Config) -> None:
|
||||
"[I] API Server",
|
||||
"[G] Gateway",
|
||||
"[T] Tools",
|
||||
"[V] View Configuration Summary",
|
||||
"<- Back",
|
||||
],
|
||||
default=last_choice,
|
||||
@@ -1588,6 +1592,7 @@ def _configure_advanced_settings(config: Config) -> None:
|
||||
"[I] API Server": lambda: _configure_general_settings(config, "API Server"),
|
||||
"[G] Gateway": lambda: _configure_general_settings(config, "Gateway"),
|
||||
"[T] Tools": lambda: _configure_general_settings(config, "Tools"),
|
||||
"[V] View Configuration Summary": lambda: _show_summary(config),
|
||||
}
|
||||
action_fn = _advanced_dispatch.get(answer)
|
||||
if action_fn:
|
||||
@@ -1624,14 +1629,8 @@ def run_onboard(initial_config: Config | None = None) -> OnboardResult:
|
||||
|
||||
try:
|
||||
answer = _get_questionary().select(
|
||||
"What would you like to configure?",
|
||||
choices=[
|
||||
"[Q] Quick Start (API key only)",
|
||||
"[A] Advanced Settings",
|
||||
"[V] View Configuration Summary",
|
||||
"[S] Save and Exit",
|
||||
"[X] Exit Without Saving",
|
||||
],
|
||||
"What would you like to do?",
|
||||
choices=_get_main_menu_choices(_has_unsaved_changes(original_config, config)),
|
||||
default=last_main_choice,
|
||||
qmark=">",
|
||||
).ask()
|
||||
@@ -1646,15 +1645,18 @@ def run_onboard(initial_config: Config | None = None) -> OnboardResult:
|
||||
return OnboardResult(config=original_config, should_save=False)
|
||||
continue
|
||||
|
||||
if answer == "[Q] Quick Start (API key only)":
|
||||
if _configure_quick_start(config):
|
||||
return OnboardResult(config=config, should_save=True)
|
||||
continue
|
||||
|
||||
_menu_dispatch = {
|
||||
"[Q] Quick Start (API key only)": lambda: _configure_quick_start(config),
|
||||
"[A] Advanced Settings": lambda: _configure_advanced_settings(config),
|
||||
"[V] View Configuration Summary": lambda: _show_summary(config),
|
||||
}
|
||||
|
||||
if answer == "[S] Save and Exit":
|
||||
return OnboardResult(config=config, should_save=True)
|
||||
if answer == "[X] Exit Without Saving":
|
||||
if answer in {"[X] Exit", "[X] Exit Without Saving"}:
|
||||
return OnboardResult(config=original_config, should_save=False)
|
||||
|
||||
action_fn = _menu_dispatch.get(answer)
|
||||
|
||||
@@ -858,13 +858,29 @@ class TestApiServerRegistration:
|
||||
class TestMainMenuUpdate:
|
||||
"""Tests for main menu including new Channel Common and API Server items."""
|
||||
|
||||
def test_main_menu_hides_save_actions_until_needed(self):
|
||||
"""The first screen should not show save or summary actions before edits."""
|
||||
from nanobot.cli.onboard import _get_main_menu_choices
|
||||
|
||||
clean_choices = _get_main_menu_choices(False)
|
||||
dirty_choices = _get_main_menu_choices(True)
|
||||
|
||||
assert clean_choices == [
|
||||
"[Q] Quick Start (API key only)",
|
||||
"[A] Advanced Settings",
|
||||
"[X] Exit",
|
||||
]
|
||||
assert "[S] Save and Exit" not in clean_choices
|
||||
assert "[V] View Configuration Summary" not in clean_choices
|
||||
assert "[S] Save and Exit" in dirty_choices
|
||||
assert "[X] Exit Without Saving" in dirty_choices
|
||||
|
||||
def test_run_onboard_quick_start_edit(self, monkeypatch):
|
||||
"""run_onboard should route [Q] to Quick Start."""
|
||||
initial_config = Config()
|
||||
|
||||
responses = iter([
|
||||
"[Q] Quick Start (API key only)",
|
||||
"[S] Save and Exit",
|
||||
])
|
||||
|
||||
class FakePrompt:
|
||||
@@ -879,6 +895,7 @@ class TestMainMenuUpdate:
|
||||
|
||||
def fake_quick_start(config):
|
||||
config.agents.defaults.bot_name = "quickbot"
|
||||
return True
|
||||
|
||||
monkeypatch.setattr(onboard_wizard, "_show_main_menu_header", lambda: None)
|
||||
monkeypatch.setattr(onboard_wizard, "questionary", SimpleNamespace(select=fake_select))
|
||||
@@ -899,16 +916,19 @@ class TestMainMenuUpdate:
|
||||
def fail_websocket_config(*_args, **_kwargs):
|
||||
raise AssertionError("recommended Quick Start should not open WebSocket settings")
|
||||
|
||||
pause_messages: list[str] = []
|
||||
|
||||
monkeypatch.setattr(onboard_wizard.console, "clear", lambda: None)
|
||||
monkeypatch.setattr(onboard_wizard, "_show_section_header", lambda *a, **kw: None)
|
||||
monkeypatch.setattr(onboard_wizard, "_input_with_existing", lambda *a, **kw: "sk-or-test")
|
||||
monkeypatch.setattr(onboard_wizard, "_input_model_with_autocomplete", fail_model_input)
|
||||
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: None)
|
||||
monkeypatch.setattr(onboard_wizard, "_pause", lambda message="": pause_messages.append(message))
|
||||
|
||||
onboard_wizard._configure_quick_start(config)
|
||||
assert onboard_wizard._configure_quick_start(config) is True
|
||||
|
||||
assert pause_messages == ["Press Enter to save and exit..."]
|
||||
assert config.providers.openrouter.api_key == "sk-or-test"
|
||||
assert config.providers.openrouter.api_base == "https://openrouter.ai/api/v1"
|
||||
assert config.agents.defaults.model_preset == "primary"
|
||||
@@ -918,6 +938,19 @@ class TestMainMenuUpdate:
|
||||
assert websocket["enabled"] is True
|
||||
assert websocket["websocketRequiresToken"] is True
|
||||
|
||||
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()
|
||||
|
||||
monkeypatch.setattr(onboard_wizard, "_show_quick_start_progress", lambda *_args: None)
|
||||
monkeypatch.setattr(onboard_wizard, "_input_with_existing", lambda *a, **kw: "")
|
||||
|
||||
assert onboard_wizard._configure_recommended_provider(config) is False
|
||||
|
||||
assert config.providers.openrouter.api_key is None
|
||||
assert config.providers.openrouter.api_base is None
|
||||
assert "primary" not in config.model_presets
|
||||
|
||||
def test_quick_start_summary_calls_out_missing_api_key(self, monkeypatch):
|
||||
"""Quick Start summary should not tell users to run gateway before adding a key."""
|
||||
config = Config()
|
||||
@@ -935,11 +968,18 @@ class TestMainMenuUpdate:
|
||||
lambda rows, _title: captured.setdefault("rows", rows),
|
||||
)
|
||||
|
||||
onboard_wizard._show_quick_start_summary(config, "websocket")
|
||||
onboard_wizard._show_quick_start_summary(config)
|
||||
|
||||
labels = [label for label, _value in captured["rows"]]
|
||||
rows = dict(captured["rows"])
|
||||
assert rows["API key"] == "add later"
|
||||
assert "add your API key" in rows["Next"]
|
||||
assert rows["Status"] == "OpenRouter API key missing"
|
||||
assert "API key" in rows["Next"]
|
||||
assert "nanobot gateway" in rows["Next"]
|
||||
assert labels.index("Next") < labels.index("Open")
|
||||
assert "Model" not in rows
|
||||
assert "Entry point" not in rows
|
||||
assert "API key" not in rows
|
||||
assert "Defaults" not in rows
|
||||
|
||||
def test_configure_login_channel_defaults_to_login(self, monkeypatch):
|
||||
"""The channel wizard should start login before exposing advanced fields."""
|
||||
@@ -1088,13 +1128,15 @@ class TestMainMenuUpdate:
|
||||
assert result.config.api.port == 9999
|
||||
|
||||
def test_view_summary_calls_pause(self, monkeypatch):
|
||||
"""[V] View Summary should pause before returning to main menu."""
|
||||
"""Advanced [V] View Summary should pause before returning to the menu."""
|
||||
initial_config = Config()
|
||||
pause_called = {"n": 0}
|
||||
|
||||
responses = iter([
|
||||
"[A] Advanced Settings",
|
||||
"[V] View Configuration Summary",
|
||||
"[S] Save and Exit",
|
||||
KeyboardInterrupt(),
|
||||
"[X] Exit",
|
||||
])
|
||||
|
||||
class FakePrompt:
|
||||
@@ -1123,7 +1165,7 @@ class TestMainMenuUpdate:
|
||||
|
||||
result = run_onboard(initial_config=initial_config)
|
||||
|
||||
assert result.should_save is True
|
||||
assert result.should_save is False
|
||||
assert pause_called["n"] == 1
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user