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.
This commit is contained in:
committed by
Teknium
parent
69ff114ee2
commit
7d39a45749
@@ -2811,7 +2811,8 @@ def test_browser_manage_status_reads_env_var(monkeypatch):
|
|||||||
{"id": "1", "method": "browser.manage", "params": {"action": "status"}}
|
{"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):
|
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"
|
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.
|
# First cleanup runs against the OLD env (none here), second against the NEW.
|
||||||
assert cleanup_calls == ["", "http://127.0.0.1:9222"]
|
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"}}
|
{"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"
|
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), \
|
with patch("hermes_cli.browser_connect.try_launch_chrome_debug", return_value=False), \
|
||||||
patch("hermes_cli.browser_connect.get_chrome_debug_candidates", return_value=[]):
|
patch("hermes_cli.browser_connect.get_chrome_debug_candidates", return_value=[]):
|
||||||
resp = server.handle_request(
|
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 resp["result"]["connected"] is False
|
||||||
assert "No Chrome/Chromium executable was found" in resp["error"]["message"]
|
assert resp["result"]["url"] == "http://127.0.0.1:9222"
|
||||||
assert "--remote-debugging-port=9222" in resp["error"]["message"]
|
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
|
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"}}
|
{"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"
|
assert os.environ["BROWSER_CDP_URL"] == "http://127.0.0.1:9222"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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")
|
@method("browser.manage")
|
||||||
def _(rid, params: dict) -> dict:
|
def _(rid, params: dict) -> dict:
|
||||||
action = params.get("action", "status")
|
action = params.get("action", "status")
|
||||||
@@ -4793,6 +4802,7 @@ def _(rid, params: dict) -> dict:
|
|||||||
from hermes_cli.browser_connect import DEFAULT_BROWSER_CDP_URL
|
from hermes_cli.browser_connect import DEFAULT_BROWSER_CDP_URL
|
||||||
|
|
||||||
url = params.get("url", DEFAULT_BROWSER_CDP_URL)
|
url = params.get("url", DEFAULT_BROWSER_CDP_URL)
|
||||||
|
messages: list[str] = []
|
||||||
try:
|
try:
|
||||||
import urllib.request
|
import urllib.request
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
@@ -4801,6 +4811,9 @@ def _(rid, params: dict) -> dict:
|
|||||||
parsed = urlparse(url if "://" in url else f"http://{url}")
|
parsed = urlparse(url if "://" in url else f"http://{url}")
|
||||||
if parsed.scheme not in {"http", "https", "ws", "wss"}:
|
if parsed.scheme not in {"http", "https", "ws", "wss"}:
|
||||||
return _err(rid, 4015, f"unsupported browser url: {url}")
|
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/<id>`` endpoint is
|
# A concrete ``ws[s]://.../devtools/browser/<id>`` endpoint is
|
||||||
# already directly connectable — those are the URLs Browserbase
|
# 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
|
from hermes_cli.browser_connect import try_launch_chrome_debug
|
||||||
|
|
||||||
port = parsed.port or 9222
|
port = parsed.port or 9222
|
||||||
if try_launch_chrome_debug(port, platform.system()):
|
messages.append("Chrome isn't running with remote debugging — attempting to launch...")
|
||||||
for _ in range(10):
|
launched = try_launch_chrome_debug(port, platform.system())
|
||||||
|
if launched:
|
||||||
|
for _ in range(20):
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
for probe in probe_urls:
|
for probe in probe_urls:
|
||||||
try:
|
try:
|
||||||
@@ -4859,10 +4874,15 @@ def _(rid, params: dict) -> dict:
|
|||||||
continue
|
continue
|
||||||
if ok:
|
if ok:
|
||||||
break
|
break
|
||||||
|
if ok:
|
||||||
|
messages.append(f"Chrome launched and listening on port {port}")
|
||||||
if not ok:
|
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:
|
else:
|
||||||
return _err(rid, 5031, f"could not reach browser CDP at {url}")
|
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.
|
# Persist a normalized URL for downstream CDP resolution.
|
||||||
# Discovery-style inputs (`http://host:port` or
|
# Discovery-style inputs (`http://host:port` or
|
||||||
@@ -4898,7 +4918,10 @@ def _(rid, params: dict) -> dict:
|
|||||||
cleanup_all_browsers()
|
cleanup_all_browsers()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return _err(rid, 5031, str(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":
|
if action == "disconnect":
|
||||||
try:
|
try:
|
||||||
from tools.browser_tool import cleanup_all_browsers
|
from tools.browser_tool import cleanup_all_browsers
|
||||||
|
|||||||
@@ -207,6 +207,33 @@ describe('createSlashHandler', () => {
|
|||||||
expect(ctx.gateway.gw.request).not.toHaveBeenCalled()
|
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', () => {
|
it('routes /rollback through native RPC when a session is active', () => {
|
||||||
patchUiState({ sid: 'sid-abc' })
|
patchUiState({ sid: 'sid-abc' })
|
||||||
const rpc = vi.fn(() => Promise.resolve({}))
|
const rpc = vi.fn(() => Promise.resolve({}))
|
||||||
|
|||||||
@@ -108,12 +108,15 @@ export const opsCommands: SlashCommand[] = [
|
|||||||
|
|
||||||
if (action === 'connect') {
|
if (action === 'connect') {
|
||||||
payload.url = requested || 'http://127.0.0.1:9222'
|
payload.url = requested || 'http://127.0.0.1:9222'
|
||||||
|
ctx.transcript.sys(`checking Chrome remote debugging at ${payload.url}...`)
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.gateway
|
ctx.gateway
|
||||||
.rpc<BrowserManageResponse>('browser.manage', payload)
|
.rpc<BrowserManageResponse>('browser.manage', payload)
|
||||||
.then(
|
.then(
|
||||||
ctx.guarded<BrowserManageResponse>(r => {
|
ctx.guarded<BrowserManageResponse>(r => {
|
||||||
|
r.messages?.forEach(message => ctx.transcript.sys(message))
|
||||||
|
|
||||||
if (action === 'status') {
|
if (action === 'status') {
|
||||||
return ctx.transcript.sys(
|
return ctx.transcript.sys(
|
||||||
r.connected
|
r.connected
|
||||||
@@ -124,13 +127,14 @@ export const opsCommands: SlashCommand[] = [
|
|||||||
|
|
||||||
if (action === 'connect') {
|
if (action === 'connect') {
|
||||||
if (r.connected) {
|
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')
|
ctx.transcript.sys('next browser tool call will use this CDP endpoint')
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.transcript.sys('browser connect failed')
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.transcript.sys('browser disconnected')
|
ctx.transcript.sys('browser disconnected')
|
||||||
|
|||||||
@@ -314,6 +314,7 @@ export interface ProcessStopResponse {
|
|||||||
|
|
||||||
export interface BrowserManageResponse {
|
export interface BrowserManageResponse {
|
||||||
connected?: boolean
|
connected?: boolean
|
||||||
|
messages?: string[]
|
||||||
url?: string
|
url?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user