From 3be12cf134e0c1b4787ad0a95bd71e45162e4f2f Mon Sep 17 00:00:00 2001 From: Xubin Ren <52506698+Re-bin@users.noreply.github.com> Date: Wed, 26 Aug 2026 20:24:47 +0800 Subject: [PATCH] fix(cli): foreground the WebUI browser --- nanobot/cli/gateway_runtime.py | 8 ++++--- nanobot/cli/webui_support.py | 25 ++++++++++++++++---- tests/cli/test_commands.py | 42 +++++++++++++++++++++++++++++++++- 3 files changed, 67 insertions(+), 8 deletions(-) diff --git a/nanobot/cli/gateway_runtime.py b/nanobot/cli/gateway_runtime.py index 9aef3e18d..38360d7df 100644 --- a/nanobot/cli/gateway_runtime.py +++ b/nanobot/cli/gateway_runtime.py @@ -23,6 +23,7 @@ from nanobot.cli.webui_support import ( _gateway_health_bind_note, _gateway_health_url, _host_for_local_browser, + _launch_browser, _prepare_webui_bundle_for_gateway, _print_foreground_port_conflict, _tcp_endpoint_reachable, @@ -864,7 +865,6 @@ def _run_gateway( """Wait for the gateway to bind, then point the user's browser at the webui.""" if not open_browser_url: return - import webbrowser from urllib.parse import urlparse # Channels start asynchronously. When the caller supplies a backend @@ -896,8 +896,10 @@ def _run_gateway( await asyncio.sleep(0.1) display_url = _webui_display_url(open_browser_url) try: - webbrowser.open(open_browser_url) - console.print(f"[green]✓[/green] Opened browser at {display_url}") + if _launch_browser(open_browser_url): + console.print(f"[green]✓[/green] Opened browser at {display_url}") + else: + console.print(f"[yellow]Could not open browser; visit {display_url}[/yellow]") except Exception as e: console.print(f"[yellow]Could not open browser ({e}); visit {display_url}[/yellow]") diff --git a/nanobot/cli/webui_support.py b/nanobot/cli/webui_support.py index b27d53af5..34c256615 100644 --- a/nanobot/cli/webui_support.py +++ b/nanobot/cli/webui_support.py @@ -1,7 +1,9 @@ """Shared WebUI setup, URL, health, and browser helpers.""" +import subprocess import sys import time +import webbrowser from collections.abc import Callable from pathlib import Path from typing import TYPE_CHECKING, Any @@ -40,6 +42,7 @@ __all__ = [ "_gateway_instance_command", "_host_for_local_browser", "_load_webui_setup_config", + "_launch_browser", "_open_webui_browser", "_prepare_webui_bundle_for_gateway", "_print_foreground_port_conflict", @@ -60,6 +63,20 @@ __all__ = [ console = Console() +def _launch_browser(url: str) -> bool: + """Open *url* and request a foreground browser window.""" + if sys.platform == "darwin": + result = subprocess.run( + ["open", url], + stdin=subprocess.DEVNULL, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + check=False, + ) + return result.returncode == 0 + return bool(webbrowser.open(url, new=2, autoraise=True)) + + def _confirm_webui_action(message: str, *, yes: bool) -> None: """Confirm a WebUI first-run mutation or fail clearly in non-interactive shells.""" if yes: @@ -419,14 +436,14 @@ def _print_foreground_port_conflict( def _open_webui_browser(url: str, *, wait: bool = True) -> None: """Open the WebUI in the user's default browser, with a copyable fallback.""" - import webbrowser - if wait: _wait_for_webui(url) display_url = _webui_display_url(url) try: - webbrowser.open(url) - console.print(f"[green]✓[/green] Opened WebUI: [cyan]{display_url}[/cyan]") + if _launch_browser(url): + console.print(f"[green]✓[/green] Opened WebUI: [cyan]{display_url}[/cyan]") + else: + console.print(f"[yellow]Could not open browser; visit {display_url}[/yellow]") except Exception as exc: console.print(f"[yellow]Could not open browser ({exc}); visit {display_url}[/yellow]") diff --git a/tests/cli/test_commands.py b/tests/cli/test_commands.py index c50ade5d9..41d33edc3 100644 --- a/tests/cli/test_commands.py +++ b/tests/cli/test_commands.py @@ -2534,7 +2534,11 @@ def test_webui_yes_still_refuses_invalid_custom_model_setup( def test_open_webui_browser_redacts_bootstrap_secret(monkeypatch, capsys) -> None: opened: list[str] = [] url = "http://127.0.0.1:8765/#/?bootstrapSecret=super-secret" - monkeypatch.setattr("webbrowser.open", lambda value: opened.append(value)) + monkeypatch.setattr( + cli_webui_support, + "_launch_browser", + lambda value: opened.append(value) or True, + ) cli_webui_support._open_webui_browser(url, wait=False) @@ -2544,6 +2548,42 @@ def test_open_webui_browser_redacts_bootstrap_secret(monkeypatch, capsys) -> Non assert "super-secret" not in output +def test_open_webui_browser_reports_launch_failure(monkeypatch, capsys) -> None: + monkeypatch.setattr(cli_webui_support, "_launch_browser", lambda _value: False) + + cli_webui_support._open_webui_browser("http://127.0.0.1:8765/", wait=False) + + assert "Could not open browser; visit http://127.0.0.1:8765/" in _strip_ansi( + capsys.readouterr().out + ) + + +def test_launch_browser_uses_macos_foreground_opener(monkeypatch) -> None: + seen: list[list[str]] = [] + monkeypatch.setattr(cli_webui_support.sys, "platform", "darwin") + monkeypatch.setattr( + cli_webui_support.subprocess, + "run", + lambda command, **_kwargs: seen.append(command) or SimpleNamespace(returncode=0), + ) + + assert cli_webui_support._launch_browser("http://127.0.0.1:8765/") is True + assert seen == [["open", "http://127.0.0.1:8765/"]] + + +def test_launch_browser_uses_default_browser_off_macos(monkeypatch) -> None: + opened: list[tuple[str, int, bool]] = [] + monkeypatch.setattr(cli_webui_support.sys, "platform", "linux") + monkeypatch.setattr( + cli_webui_support.webbrowser, + "open", + lambda url, *, new, autoraise: opened.append((url, new, autoraise)) or True, + ) + + assert cli_webui_support._launch_browser("http://127.0.0.1:8765/") is True + assert opened == [("http://127.0.0.1:8765/", 2, True)] + + def test_webui_foreground_attaches_to_existing_managed_gateway(monkeypatch, tmp_path: Path) -> None: config_file = tmp_path / "config.json" config_file.write_text("{}")