Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# Changelog

## v0.5.0
- **`full` is the default again, and it embeds the dashboard inline.** Full mode iframes
agent-browser's dashboard at its own **local origin** (`http://<host>:<port>/`) — so on a local
setup (console + agent-browser on one machine) you get the real dashboard, feeds and all, right
in the panel. (v0.4.0 turned it into a new-tab launcher; this brings the inline embed back, the
right way — at the dashboard's origin, not through the impossible sub-path proxy.)
- **A clear error instead of a blank frame when it can't embed.** If the console is opened remotely
(a fleet member, a non-loopback host, or over https) the dashboard's `localhost` isn't reachable
from your browser — the panel detects that (loopback host + not fleet-proxied + not https) and
says so, pointing you at `panel_mode: minimal` (which still works everywhere). Start/Stop the
dashboard from the panel as before; a not-yet-running dashboard shows a Start prompt.

## v0.4.0
- **The Browser panel works out of the box now.** The default `panel_mode` is **`minimal`** — a
live screenshot + nav toolbar driven through the gated same-origin routes; it works everywhere
Expand Down
19 changes: 10 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,16 @@ point.
- **Workflows** — declarative browser recipes (browse-and-extract, fill-a-form, …).
- **Browser panel** — a console view (ADR 0026) for watching/driving the browser. Two
modes, set by `panel_mode`:
- **`minimal` (default)** — a **live screenshot** of the viewport + a nav toolbar + a
**Dashboard control** (start/stop/status), all through the **gated same-origin routes**.
Works everywhere (host and member), no dashboard daemon needed. The reliable mode.
- **`full`** — a **launcher** for agent-browser's own dashboard (viewport + activity/
console/network feeds). It's not embedded: that dashboard is a Next.js app with
**root-absolute asset paths** (no base-path), so it can't render under a sub-path
panel — it only loads at its **own origin**. Full mode opens it there
("Open dashboard ↗"), which works on a local/host setup. For a remote member, use
`minimal`.
- **`full` (default)** — **embeds agent-browser's dashboard inline** (viewport + activity/
console/network feeds) at its own **local origin** (`http://<host>:<port>/`). Best for a
**local setup** (console + agent-browser on one machine). Because the dashboard is a
Next.js app with root-absolute assets (no base-path), it only loads at its own origin —
so when the console is opened **remotely** (a fleet member, a non-loopback host, or over
https) its `localhost` isn't reachable from your browser, and the panel shows a **clear
error** (pointing you at `minimal`) rather than a blank frame.
- **`minimal`** — a **live screenshot** of the viewport + a nav toolbar, all through the
**gated same-origin routes**. Works everywhere (host and member), no dashboard daemon
needed — use it for a remote/member agent.

Either mode can **start the dashboard from the panel** (no terminal) — the Start/Stop
control hits the gated `POST /api/plugins/agent_browser/dashboard`.
Expand Down
100 changes: 65 additions & 35 deletions browser_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,71 +138,101 @@ async def _dash_control(body: dict = Body(...)):
return router


# ── full mode: a LAUNCHER for agent-browser's dashboard (not an embed) ──────────
# The dashboard is a prebuilt Next.js app whose assets are root-absolute (/_next/…)
# with no base-path option, so it can't render under our sub-path panel — it only
# loads at its OWN origin. So full mode is a launcher: a Start/Stop control + an
# "Open dashboard ↗" link to that origin (reachable on a local/host setup). For a
# remote member, minimal mode is the one that works.
# ── full mode (default): embed agent-browser's dashboard at its OWN origin ──────
# The dashboard is a Next.js app with root-absolute assets (no base-path), so it only
# renders at its own origin (http://<host>:<port>/) — never under a sub-path proxy. Full
# mode therefore ASSUMES A LOCAL setup (console + agent-browser on one machine) and iframes
# that loopback origin directly. When the console is opened remotely (a fleet member, or a
# non-loopback host, or over https) that loopback dashboard isn't reachable from the
# operator's browser — we DETECT that and show a clear error instead of a blank frame.
_FULL_PAGE = r"""<!doctype html><html lang="en"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"><title>Browser</title>
<link id="dskit" rel="stylesheet" href="">
<script>
const BASE=location.pathname.split("/plugins/")[0];
document.getElementById("dskit").href=BASE+"/_ds/plugin-kit.css";
const DASH_PORT=__DASH_PORT__;
function dashOpenUrl(){ return "http://"+location.hostname+":"+DASH_PORT+"/"; }
const PORT=__DASH_PORT__;
const LOOPBACK=["localhost","127.0.0.1","[::1]","::1"].indexOf(location.hostname)>=0;
const LOCAL=BASE==="" && LOOPBACK; // host view (not fleet-proxied) + browser on the box
const MIXED=location.protocol==="https:"; // dashboard is http; an https page can't embed it
const DASH_URL="http://"+location.hostname+":"+PORT+"/";
</script>
<style>
html,body{margin:0;height:100%;background:var(--pl-color-bg);color:var(--pl-color-fg);
font-family:var(--pl-font-sans);font-size:13px}
.bar{height:38px;display:flex;align-items:center;gap:8px;padding:0 12px;
.bar{height:34px;display:flex;align-items:center;gap:8px;padding:0 12px;
border-bottom:var(--pl-border-width) solid var(--pl-color-border)}
.bar b{color:var(--pl-color-accent)}
.dot{width:7px;height:7px;border-radius:50%;display:inline-block;flex:none}
.dot.ok{background:#22c55e}.dot.off{background:var(--pl-color-fg-muted,#9aa0aa)}
.stage{height:calc(100% - 38px);display:flex;align-items:center;justify-content:center;
padding:24px;background:var(--pl-color-bg-inset);box-sizing:border-box}
.card{max-width:440px;text-align:center}
.stage{height:calc(100% - 34px);position:relative;background:var(--pl-color-bg-inset)}
iframe{display:none;width:100%;height:100%;border:0;background:var(--pl-color-bg)}
#msg{display:none;position:absolute;inset:0;align-items:center;justify-content:center;padding:24px;box-sizing:border-box}
.card{max-width:460px;text-align:center}
.card .t{font-size:15px;font-weight:600;margin-bottom:8px}
.card .d{color:var(--pl-color-fg-muted);line-height:1.6;margin-bottom:16px}
.card .tip{margin-top:16px;font-size:11.5px;color:var(--pl-color-fg-muted)}
.card code{background:var(--pl-color-bg-subtle,rgba(127,127,127,.14));padding:.1em .35em;border-radius:4px}
.card .d{color:var(--pl-color-fg-muted);line-height:1.6}
.card code{background:var(--pl-color-bg-subtle,rgba(127,127,127,.16));padding:.1em .35em;border-radius:4px}
</style></head><body>
<div class="bar"><b>Browser</b><span id="dash" title="agent-browser dashboard"></span></div>
<div class="stage"><div class="card">
<div class="t">agent-browser dashboard</div>
<div class="d">The full dashboard — live viewport plus activity, console, and network
feeds — runs in its own window. It can't embed in this panel (its assets load from the
page root, which a sub-path panel can't serve), so it opens in a new tab.</div>
<a id="openlink" class="pl-btn pl-btn--primary" href="" target="_blank" rel="noopener">Open dashboard ↗</a>
<div class="tip">Want the browser <em>inside</em> the console? Set
<code>panel_mode: minimal</code> — a live viewport you can drive right here.</div>
</div></div>
<div class="stage">
<iframe id="f" referrerpolicy="no-referrer" allow="clipboard-read; clipboard-write"></iframe>
<div id="msg"><div class="card"><div class="t" id="mt"></div><div class="d" id="md"></div></div></div>
</div>
<script type="module">
document.getElementById("openlink").href=dashOpenUrl();
const $=(id)=>document.getElementById(id);
let kit;
try { kit = await import(BASE + "/_ds/plugin-kit.js"); kit.initPluginView(); }
catch (e) { kit = { initPluginView(){}, apiFetch: (p, i) => fetch(BASE + p, i) }; }
// Dashboard control — start/stop the daemon from the panel (no terminal).
function renderDash(running){
const el=document.getElementById("dash"); if(running===null){ el.innerHTML=""; return; }
el.innerHTML = running

function showFrame(){ $("f").src=DASH_URL; $("f").style.display="block"; $("msg").style.display="none"; }
function showMsg(title, html){ $("mt").textContent=title; $("md").innerHTML=html;
$("msg").style.display="flex"; $("f").style.display="none"; }

function renderDash(state){
const el=$("dash");
if(!LOCAL || MIXED || state==null){ el.innerHTML=""; return; } // control only matters when embeddable
el.innerHTML = state
? '<span class="dot ok"></span> running <button class="pl-btn pl-btn--ghost pl-btn--sm" onclick="dashAct(\'stop\')">Stop</button>'
: '<span class="dot off"></span> stopped <button class="pl-btn pl-btn--sm" onclick="dashAct(\'start\')">Start dashboard</button>';
}
async function dashStatus(){
try{ const r=await kit.apiFetch("/api/plugins/agent_browser/dashboard"); const d=await r.json();
renderDash(!!d.running); }catch(_){ renderDash(null); }
async function dashRunning(){
try{ const r=await kit.apiFetch("/api/plugins/agent_browser/dashboard"); return !!(await r.json()).running; }
catch(_){ return null; }
}
async function decide(){
if(!LOCAL){
renderDash(null);
showMsg("Open the console locally to see the dashboard",
"The dashboard embeds agent-browser's <b>local</b> dashboard — it only loads on the same machine that runs it. "
+ (BASE ? "This panel is served through the fleet proxy" : "You're reaching the console at <code>"+location.hostname+"</code>")
+ ", so its <code>localhost:"+PORT+"</code> dashboard isn't reachable from your browser.<br><br>"
+ "Open the console at <code>http://localhost</code> on the host, or set <code>panel_mode: minimal</code> "
+ "for a screenshot viewport that works remotely.");
return;
}
if(MIXED){
renderDash(null);
showMsg("Can't embed the dashboard over https",
"The console is served over https, but agent-browser's dashboard is http — browsers block that mix. "
+ "Open the console at <code>http://localhost</code>, or set <code>panel_mode: minimal</code>.");
return;
}
const running = await dashRunning(); renderDash(running);
if(running){ showFrame(); }
else { showMsg("Dashboard not running",
"Click <b>Start dashboard</b> in the top bar to launch it and view it here "
+ "(or run <code>agent-browser dashboard start</code>)."); }
}
async function dashAct(action){
document.getElementById("dash").innerHTML='<span class="dot off"></span> …';
$("dash").innerHTML='<span class="dot off"></span> …';
try{ await kit.apiFetch("/api/plugins/agent_browser/dashboard",{method:"POST",
headers:{"Content-Type":"application/json"},body:JSON.stringify({action})}); }catch(_){}
setTimeout(dashStatus, 900);
setTimeout(decide, action==="start"?1500:700);
}
window.dashAct=dashAct;
dashStatus(); setInterval(dashStatus, 6000);
decide();
// While local + not yet embedded, re-check so a dashboard started elsewhere appears on its own.
setInterval(()=>{ if(LOCAL && !MIXED && $("f").style.display==="none") decide(); }, 6000);
</script></body></html>"""


Expand Down
21 changes: 11 additions & 10 deletions protoagent.plugin.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
id: agent_browser
name: Agent Browser
version: 0.4.0
version: 0.5.0
description: >-
Browser automation for protoAgent, backed by **agent-browser** (vercel-labs) — a
fast native-Rust CLI/daemon that drives Chrome over CDP with accessibility-tree
Expand All @@ -23,14 +23,15 @@ config:
binary: agent-browser # the agent-browser CLI on PATH (override for a pinned path)
dashboard_port: 4848 # agent-browser dashboard start --port; full-mode panel iframes this
timeout_s: 60 # per-command subprocess timeout
panel_mode: minimal # Browser panel layout. `minimal` (default): viewport-only — a live
# screenshot + a nav toolbar + a Dashboard control, driven through the
# gated same-origin routes. Works everywhere (host + member), no daemon
# needed. `full`: iframe agent-browser's own dashboard (viewport + feeds)
# — only renders when the dashboard is reachable at its OWN origin from
# your browser (a local/host setup); it can't be embedded through a
# sub-path reverse proxy because its Next.js assets are root-absolute.
# Use the panel's "Open ↗" to view the dashboard directly.
panel_mode: full # Browser panel layout. `full` (default): embed agent-browser's own
# dashboard (viewport + activity/console/network feeds) at its local
# origin — best for a LOCAL setup (console + agent-browser on one box).
# When the console is opened remotely (a fleet member, a non-loopback
# host, or over https) the dashboard's localhost isn't reachable from
# your browser, so the panel shows a clear error — use `minimal` there.
# `minimal`: a screenshot viewport + nav toolbar driven through the
# gated same-origin routes; works everywhere, no daemon needed. Both
# modes can start/stop the dashboard from the panel.
# Runtime / launch options — a curated set passed to `agent-browser open` so you can
# shape and lock down the browser the agent spins up. Blank/0/false = CLI default.
headed: false # show a real browser window instead of headless
Expand All @@ -44,7 +45,7 @@ config:

# Editable in Settings ▸ Plugins (ADR 0019) — the operator knobs above as UI fields.
settings:
- { key: panel_mode, label: "Browser panel mode", type: select, options: [minimal, full], description: "minimal (recommended): a live screenshot + nav toolbar + Dashboard control, via gated same-origin routes — works everywhere. full: iframe agent-browser's dashboard (viewport + feeds) — only renders when the dashboard is reachable at its own origin from your browser (local/host); it can't embed through a sub-path proxy. Use the panel's Open ↗ either way." }
- { key: panel_mode, label: "Browser panel mode", type: select, options: [full, minimal], description: "full (default): embed agent-browser's dashboard (viewport + activity/console/network feeds) at its local origin — for a local setup (console + agent-browser on one machine). Opened remotely (a fleet member / non-loopback host / https), it shows a clear error instead of a blank frame. minimal: a screenshot viewport + nav toolbar via gated same-origin routes — works everywhere, no daemon. Both can start/stop the dashboard from the panel." }
- { key: headed, label: "Headed browser", type: bool, description: "Show a real browser window instead of running headless." }
- { key: allowed_domains, label: "Allowed domains", type: string, description: "Comma-separated navigable-domain allowlist (e.g. example.com,*.foo.com). Blank = unrestricted." }
- { key: confirm_actions, label: "Confirm actions", type: string, description: "Comma-separated action categories that require confirmation before the agent runs them." }
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "agent-browser-plugin"
version = "0.4.0"
version = "0.5.0"
description = "Browser-automation plugin for protoAgent, backed by vercel-labs/agent-browser (tools + skill + workflows + a live browser panel)."
requires-python = ">=3.11"

Expand Down
19 changes: 10 additions & 9 deletions tests/test_agent_browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ def _app(cfg=None):
return app


def test_panel_page_full_mode_is_a_launcher():
def test_panel_page_full_mode_embeds_local_or_errors():
from fastapi.testclient import TestClient

c = TestClient(_app({"panel_mode": "full", "dashboard_port": 4955}))
Expand All @@ -186,14 +186,15 @@ def test_panel_page_full_mode_is_a_launcher():
html = r.text
assert "/_ds/plugin-kit.css" in html # DS kit
assert 'location.pathname.split("/plugins/")[0]' in html # slug-aware base
assert "Open dashboard" in html # a launcher to the dashboard's own origin (not an embed)
assert 'id="f"' in html # the dashboard iframe
assert '"http://"+location.hostname+":"+PORT' in html # embed the dashboard's OWN local origin
assert "LOOPBACK" in html # local detection (loopback host + not fleet-proxied)
assert "Open the console locally" in html # the clear error shown when NOT local
assert "/api/plugins/agent_browser/dashboard" in html # the start/stop control
assert "panel_mode: minimal" in html # points users at the mode that embeds inline
# full mode no longer embeds a sub-path proxy; the port placeholder is interpolated.
assert "panel_mode: minimal" in html # the remote alternative the error points at
# the dead sub-path proxy is gone; the port placeholder is interpolated.
assert "/panel/dash" not in html
assert "__DASH_PORT__" not in html and "4955" in html
# the "open" link is built from location.hostname, never a hardcoded origin.
assert "http://localhost" not in html and "http://127.0.0.1" not in html


def test_panel_page_minimal_mode_uses_gated_data_routes():
Expand All @@ -210,13 +211,13 @@ def test_panel_page_minimal_mode_uses_gated_data_routes():
assert "__DASH_PORT__" not in html and "4933" in html


def test_default_panel_mode_is_minimal():
def test_default_panel_mode_is_full():
import yaml

m = yaml.safe_load((ROOT / "protoagent.plugin.yaml").read_text())
assert m["config"]["panel_mode"] == "minimal" # the reliable mode is the default now
assert m["config"]["panel_mode"] == "full" # embed the dashboard by default (local setup)
by_key = {f["key"]: f for f in m["settings"]}
assert by_key["panel_mode"]["options"][0] == "minimal" # recommended first
assert by_key["panel_mode"]["options"][0] == "full" # default listed first


def test_dashboard_control_endpoint(monkeypatch):
Expand Down
Loading