fix(cli): foreground the WebUI browser

This commit is contained in:
Xubin Ren
2026-08-27 10:45:40 +08:00
parent b632186b5f
commit 2113870e27
3 changed files with 67 additions and 8 deletions
+4 -2
View File
@@ -23,6 +23,7 @@ from nanobot.cli.webui_support import (
_gateway_health_bind_note, _gateway_health_bind_note,
_gateway_health_url, _gateway_health_url,
_host_for_local_browser, _host_for_local_browser,
_launch_browser,
_prepare_webui_bundle_for_gateway, _prepare_webui_bundle_for_gateway,
_print_foreground_port_conflict, _print_foreground_port_conflict,
_tcp_endpoint_reachable, _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.""" """Wait for the gateway to bind, then point the user's browser at the webui."""
if not open_browser_url: if not open_browser_url:
return return
import webbrowser
from urllib.parse import urlparse from urllib.parse import urlparse
# Channels start asynchronously. When the caller supplies a backend # Channels start asynchronously. When the caller supplies a backend
@@ -896,8 +896,10 @@ def _run_gateway(
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
display_url = _webui_display_url(open_browser_url) display_url = _webui_display_url(open_browser_url)
try: try:
webbrowser.open(open_browser_url) if _launch_browser(open_browser_url):
console.print(f"[green]✓[/green] Opened browser at {display_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: except Exception as e:
console.print(f"[yellow]Could not open browser ({e}); visit {display_url}[/yellow]") console.print(f"[yellow]Could not open browser ({e}); visit {display_url}[/yellow]")
+20 -3
View File
@@ -1,7 +1,9 @@
"""Shared WebUI setup, URL, health, and browser helpers.""" """Shared WebUI setup, URL, health, and browser helpers."""
import subprocess
import sys import sys
import time import time
import webbrowser
from collections.abc import Callable from collections.abc import Callable
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
@@ -40,6 +42,7 @@ __all__ = [
"_gateway_instance_command", "_gateway_instance_command",
"_host_for_local_browser", "_host_for_local_browser",
"_load_webui_setup_config", "_load_webui_setup_config",
"_launch_browser",
"_open_webui_browser", "_open_webui_browser",
"_prepare_webui_bundle_for_gateway", "_prepare_webui_bundle_for_gateway",
"_print_foreground_port_conflict", "_print_foreground_port_conflict",
@@ -60,6 +63,20 @@ __all__ = [
console = Console() 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: def _confirm_webui_action(message: str, *, yes: bool) -> None:
"""Confirm a WebUI first-run mutation or fail clearly in non-interactive shells.""" """Confirm a WebUI first-run mutation or fail clearly in non-interactive shells."""
if yes: if yes:
@@ -419,14 +436,14 @@ def _print_foreground_port_conflict(
def _open_webui_browser(url: str, *, wait: bool = True) -> None: def _open_webui_browser(url: str, *, wait: bool = True) -> None:
"""Open the WebUI in the user's default browser, with a copyable fallback.""" """Open the WebUI in the user's default browser, with a copyable fallback."""
import webbrowser
if wait: if wait:
_wait_for_webui(url) _wait_for_webui(url)
display_url = _webui_display_url(url) display_url = _webui_display_url(url)
try: try:
webbrowser.open(url) if _launch_browser(url):
console.print(f"[green]✓[/green] Opened WebUI: [cyan]{display_url}[/cyan]") 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: except Exception as exc:
console.print(f"[yellow]Could not open browser ({exc}); visit {display_url}[/yellow]") console.print(f"[yellow]Could not open browser ({exc}); visit {display_url}[/yellow]")
+41 -1
View File
@@ -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: def test_open_webui_browser_redacts_bootstrap_secret(monkeypatch, capsys) -> None:
opened: list[str] = [] opened: list[str] = []
url = "http://127.0.0.1:8765/#/?bootstrapSecret=super-secret" 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) 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 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: def test_webui_foreground_attaches_to_existing_managed_gateway(monkeypatch, tmp_path: Path) -> None:
config_file = tmp_path / "config.json" config_file = tmp_path / "config.json"
config_file.write_text("{}") config_file.write_text("{}")