From 7d39a45749ba8cd16765e0aa8ff9ebf4bf92cd90 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Tue, 28 Apr 2026 22:22:32 -0500 Subject: [PATCH] fix(tui): show /browser connect progress like CLI Return CLI-style browser connect status messages from the gateway and render them in the TUI so local Chrome launch attempts are visible instead of ending in a silent delayed failure. --- tests/test_tui_gateway_server.py | 32 ++++++++++++++----- tui_gateway/server.py | 31 +++++++++++++++--- .../src/__tests__/createSlashHandler.test.ts | 27 ++++++++++++++++ ui-tui/src/app/slash/commands/ops.ts | 8 +++-- ui-tui/src/gatewayTypes.ts | 1 + 5 files changed, 85 insertions(+), 14 deletions(-) diff --git a/tests/test_tui_gateway_server.py b/tests/test_tui_gateway_server.py index c5a5666028..cee08a4100 100644 --- a/tests/test_tui_gateway_server.py +++ b/tests/test_tui_gateway_server.py @@ -2811,7 +2811,8 @@ def test_browser_manage_status_reads_env_var(monkeypatch): {"id": "1", "method": "browser.manage", "params": {"action": "status"}} ) - assert resp["result"] == {"connected": True, "url": "http://127.0.0.1:9222"} + assert resp["result"]["connected"] is True + assert resp["result"]["url"] == "http://127.0.0.1:9222" def test_browser_manage_status_falls_back_to_config_cdp_url(monkeypatch): @@ -2874,7 +2875,9 @@ def test_browser_manage_connect_sets_env_and_cleans_twice(monkeypatch): } ) - assert resp["result"] == {"connected": True, "url": "http://127.0.0.1:9222"} + assert resp["result"]["connected"] is True + assert resp["result"]["url"] == "http://127.0.0.1:9222" + assert resp["result"]["messages"] == ["Chrome is already listening on port 9222"] assert os.environ.get("BROWSER_CDP_URL") == "http://127.0.0.1:9222" # First cleanup runs against the OLD env (none here), second against the NEW. assert cleanup_calls == ["", "http://127.0.0.1:9222"] @@ -2892,7 +2895,9 @@ def test_browser_manage_connect_defaults_to_loopback(monkeypatch): {"id": "1", "method": "browser.manage", "params": {"action": "connect"}} ) - assert resp["result"] == {"connected": True, "url": "http://127.0.0.1:9222"} + assert resp["result"]["connected"] is True + assert resp["result"]["url"] == "http://127.0.0.1:9222" + assert resp["result"]["messages"] == ["Chrome is already listening on port 9222"] assert urls[0] == "http://127.0.0.1:9222/json/version" @@ -2907,12 +2912,18 @@ def test_browser_manage_connect_default_local_reports_launch_hint(monkeypatch): with patch("hermes_cli.browser_connect.try_launch_chrome_debug", return_value=False), \ patch("hermes_cli.browser_connect.get_chrome_debug_candidates", return_value=[]): resp = server.handle_request( - {"id": "1", "method": "browser.manage", "params": {"action": "connect"}} + { + "id": "1", + "method": "browser.manage", + "params": {"action": "connect", "url": "http://localhost:9222"}, + } ) - assert resp["error"]["code"] == 5031 - assert "No Chrome/Chromium executable was found" in resp["error"]["message"] - assert "--remote-debugging-port=9222" in resp["error"]["message"] + assert resp["result"]["connected"] is False + assert resp["result"]["url"] == "http://127.0.0.1:9222" + assert resp["result"]["messages"][0] == "Chrome isn't running with remote debugging — attempting to launch..." + assert any("No Chrome/Chromium executable was found" in line for line in resp["result"]["messages"]) + assert any("--remote-debugging-port=9222" in line for line in resp["result"]["messages"]) assert "BROWSER_CDP_URL" not in os.environ @@ -2950,7 +2961,12 @@ def test_browser_manage_connect_default_local_retries_after_launch(monkeypatch): {"id": "1", "method": "browser.manage", "params": {"action": "connect"}} ) - assert resp["result"] == {"connected": True, "url": "http://127.0.0.1:9222"} + assert resp["result"]["connected"] is True + assert resp["result"]["url"] == "http://127.0.0.1:9222" + assert resp["result"]["messages"] == [ + "Chrome isn't running with remote debugging — attempting to launch...", + "Chrome launched and listening on port 9222", + ] assert os.environ["BROWSER_CDP_URL"] == "http://127.0.0.1:9222" diff --git a/tui_gateway/server.py b/tui_gateway/server.py index 798fdeb55c..cb9a871500 100644 --- a/tui_gateway/server.py +++ b/tui_gateway/server.py @@ -4777,6 +4777,15 @@ def _browser_connect_error(url: str, port: int) -> str: ) +def _browser_connect_failure_messages(url: str, port: int) -> list[str]: + command = _browser_connect_error(url, port) + return [ + "Chrome isn't running with remote debugging — attempting to launch...", + *command.splitlines(), + "Browser not connected — start Chrome with remote debugging and retry /browser connect", + ] + + @method("browser.manage") def _(rid, params: dict) -> dict: action = params.get("action", "status") @@ -4793,6 +4802,7 @@ def _(rid, params: dict) -> dict: from hermes_cli.browser_connect import DEFAULT_BROWSER_CDP_URL url = params.get("url", DEFAULT_BROWSER_CDP_URL) + messages: list[str] = [] try: import urllib.request from urllib.parse import urlparse @@ -4801,6 +4811,9 @@ def _(rid, params: dict) -> dict: parsed = urlparse(url if "://" in url else f"http://{url}") if parsed.scheme not in {"http", "https", "ws", "wss"}: return _err(rid, 4015, f"unsupported browser url: {url}") + if _is_default_local_cdp(parsed): + url = DEFAULT_BROWSER_CDP_URL + parsed = urlparse(url) # A concrete ``ws[s]://.../devtools/browser/`` endpoint is # already directly connectable — those are the URLs Browserbase @@ -4846,8 +4859,10 @@ def _(rid, params: dict) -> dict: from hermes_cli.browser_connect import try_launch_chrome_debug port = parsed.port or 9222 - if try_launch_chrome_debug(port, platform.system()): - for _ in range(10): + messages.append("Chrome isn't running with remote debugging — attempting to launch...") + launched = try_launch_chrome_debug(port, platform.system()) + if launched: + for _ in range(20): time.sleep(0.5) for probe in probe_urls: try: @@ -4859,10 +4874,15 @@ def _(rid, params: dict) -> dict: continue if ok: break + if ok: + messages.append(f"Chrome launched and listening on port {port}") if not ok: - return _err(rid, 5031, _browser_connect_error(url, port)) + messages.extend(_browser_connect_failure_messages(url, port)[1:]) + return _ok(rid, {"connected": False, "url": url, "messages": messages}) else: return _err(rid, 5031, f"could not reach browser CDP at {url}") + elif _is_default_local_cdp(parsed): + messages.append(f"Chrome is already listening on port {parsed.port or 9222}") # Persist a normalized URL for downstream CDP resolution. # Discovery-style inputs (`http://host:port` or @@ -4898,7 +4918,10 @@ def _(rid, params: dict) -> dict: cleanup_all_browsers() except Exception as e: return _err(rid, 5031, str(e)) - return _ok(rid, {"connected": True, "url": normalized}) + payload = {"connected": True, "url": normalized} + if messages: + payload["messages"] = messages + return _ok(rid, payload) if action == "disconnect": try: from tools.browser_tool import cleanup_all_browsers diff --git a/ui-tui/src/__tests__/createSlashHandler.test.ts b/ui-tui/src/__tests__/createSlashHandler.test.ts index 14f76b625b..52ad7b8486 100644 --- a/ui-tui/src/__tests__/createSlashHandler.test.ts +++ b/ui-tui/src/__tests__/createSlashHandler.test.ts @@ -207,6 +207,33 @@ describe('createSlashHandler', () => { expect(ctx.gateway.gw.request).not.toHaveBeenCalled() }) + it('renders browser connect progress messages from the gateway', async () => { + const rpc = vi.fn(() => + Promise.resolve({ + connected: false, + messages: [ + "Chrome isn't running with remote debugging — attempting to launch...", + 'Browser not connected — start Chrome with remote debugging and retry /browser connect' + ], + url: 'http://127.0.0.1:9222' + }) + ) + const ctx = buildCtx({ gateway: { ...buildGateway(), rpc } }) + + expect(createSlashHandler(ctx)('/browser connect')).toBe(true) + expect(ctx.transcript.sys).toHaveBeenCalledWith('checking Chrome remote debugging at http://127.0.0.1:9222...') + + await vi.waitFor(() => { + expect(ctx.transcript.sys).toHaveBeenCalledWith( + "Chrome isn't running with remote debugging — attempting to launch..." + ) + expect(ctx.transcript.sys).toHaveBeenCalledWith( + 'Browser not connected — start Chrome with remote debugging and retry /browser connect' + ) + expect(ctx.transcript.sys).not.toHaveBeenCalledWith('browser connect failed') + }) + }) + it('routes /rollback through native RPC when a session is active', () => { patchUiState({ sid: 'sid-abc' }) const rpc = vi.fn(() => Promise.resolve({})) diff --git a/ui-tui/src/app/slash/commands/ops.ts b/ui-tui/src/app/slash/commands/ops.ts index 9a7fc9d79c..52f9d8d3b2 100644 --- a/ui-tui/src/app/slash/commands/ops.ts +++ b/ui-tui/src/app/slash/commands/ops.ts @@ -108,12 +108,15 @@ export const opsCommands: SlashCommand[] = [ if (action === 'connect') { payload.url = requested || 'http://127.0.0.1:9222' + ctx.transcript.sys(`checking Chrome remote debugging at ${payload.url}...`) } ctx.gateway .rpc('browser.manage', payload) .then( ctx.guarded(r => { + r.messages?.forEach(message => ctx.transcript.sys(message)) + if (action === 'status') { return ctx.transcript.sys( r.connected @@ -124,13 +127,14 @@ export const opsCommands: SlashCommand[] = [ if (action === 'connect') { if (r.connected) { - ctx.transcript.sys(`browser connected: ${r.url || '(url unavailable)'}`) + ctx.transcript.sys(`Browser connected to live Chrome via CDP`) + ctx.transcript.sys(`Endpoint: ${r.url || '(url unavailable)'}`) ctx.transcript.sys('next browser tool call will use this CDP endpoint') return } - return ctx.transcript.sys('browser connect failed') + return } ctx.transcript.sys('browser disconnected') diff --git a/ui-tui/src/gatewayTypes.ts b/ui-tui/src/gatewayTypes.ts index f1df5edfce..cb4346ce38 100644 --- a/ui-tui/src/gatewayTypes.ts +++ b/ui-tui/src/gatewayTypes.ts @@ -314,6 +314,7 @@ export interface ProcessStopResponse { export interface BrowserManageResponse { connected?: boolean + messages?: string[] url?: string }