From f9654c4874717c5882a8668cd29d88e1f14db451 Mon Sep 17 00:00:00 2001 From: "fangyaozheng@bytedance.com" Date: Sat, 11 Jul 2026 09:58:00 +0800 Subject: [PATCH] feat(cli): optionally open the browser when `veadk frontend` is ready Add a --open/--no-open flag (default off): with --open, once the server's port accepts connections, open the served UI (http://:) in the default browser. Off by default since most deployments are server-hosted without a local browser; ignored with --dev (the Vite dev server serves the UI). Browser opening runs on a daemon thread and never blocks serving. --- veadk/cli/cli_frontend.py | 54 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/veadk/cli/cli_frontend.py b/veadk/cli/cli_frontend.py index 66c1173f..06d4bc76 100644 --- a/veadk/cli/cli_frontend.py +++ b/veadk/cli/cli_frontend.py @@ -81,6 +81,36 @@ def _resolve_frontend_dir(arg: str | None) -> Path: return (Path.cwd() / "frontend" / "dist").resolve() +def _open_browser_when_ready( + url: str, host: str, port: int, timeout: float = 15.0 +) -> None: + """Open ``url`` in the default browser once the server accepts connections. + + Polls the TCP port (up to ``timeout`` seconds) so the tab lands on a ready + server rather than a connection error. Runs on a daemon thread; any failure + is logged and ignored — a browser that will not open must never block the + server from serving. + """ + import socket + import time + import webbrowser + + deadline = time.time() + timeout + while time.time() < deadline: + try: + with socket.create_connection((host, port), timeout=0.5): + break + except OSError: + time.sleep(0.25) + else: + logger.warning("Server not ready in time; skipped opening the browser.") + return + try: + webbrowser.open(url) + except Exception as e: # noqa: BLE001 - opening a browser is best-effort + logger.warning(f"Could not open the browser automatically: {e}") + + # Built-in provider presets so users only need to supply client id/secret. # Google is OIDC (endpoints come from discovery via OAUTH2_ISSUER); GitHub is # not OIDC, so its endpoints are explicit and it needs Accept: application/json @@ -286,6 +316,15 @@ def _build_generic_oauth2(provider_id: str, redirect_uri: str): "forwards as an Authorization: Bearer — parse the user from it and run " "no in-app login (use when deployed behind the AgentKit runtime gateway).", ) +@click.option( + "--open/--no-open", + "open_browser", + default=False, + show_default=True, + help="Open the web UI in your default browser once the server is ready. " + "Off by default (typical server-hosted deployments have no local browser); " + "pass --open for local use. Ignored with --dev.", +) def frontend( agents_dir: str, frontend_dir: str | None, @@ -300,6 +339,7 @@ def frontend( oauth2_provider: str | None, oauth2_provider_label: str | None, auth_mode: str, + open_browser: bool, ) -> None: """Launch the A2UI web UI backed by the ADK agent API server.""" # Explicitly load .env file before any agent code runs @@ -1173,6 +1213,20 @@ async def _spa_fallback(path: str, request: Request): f"A2UI UI + API serving on http://{host}:{port} (UI: {webui}, agents: {agents_dir})" ) + # Open the UI in the browser once the server is up. Only in non-dev mode, + # where this server serves the UI; with --dev the Vite dev server owns it. + if open_browser and not dev: + import threading + + browse_host = "127.0.0.1" if host in ("0.0.0.0", "", "::") else host + url = f"http://{browse_host}:{port}" + threading.Thread( + target=_open_browser_when_ready, + args=(url, browse_host, port), + daemon=True, + ).start() + logger.info(f"Opening {url} in your browser…") + import uvicorn uvicorn.run(app, host=host, port=port)