Skip to content

Commit f7fe151

Browse files
mabry1985claude
andauthored
feat: full mode embeds the local dashboard inline (default); clear error when not local (v0.5.0) (#12)
Per feedback (the minimal screenshot mode isn't wanted): full mode is the default again and now embeds agent-browser's dashboard INLINE — but correctly, at its own local origin (http://<host>:<port>/), not through the impossible sub-path proxy. On a local setup (console + agent-browser on one machine) you get the real dashboard, feeds and all, in the panel. Verified e2e against the live binary (iframe → 127.0.0.1:4848, dashboard loaded — title + tabs, status "running", no error). "Assume local, error if not": full mode detects whether the dashboard is reachable from the operator's browser — LOCAL = loopback host (localhost/127.0.0.1) AND not fleet-proxied (base=="") AND not https. If not local (a member, a non-loopback host, or an https console where the http dashboard is mixed-content-blocked), it shows a clear, specific error pointing at panel_mode: minimal instead of a blank frame. Local-but-stopped shows a Start prompt; the Start/Stop control is in the bar. minimal mode stays as the remote/everywhere option. 19 tests (full-mode test now asserts the embed + the error path), ruff clean. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 3f69efd commit f7fe151

6 files changed

Lines changed: 109 additions & 64 deletions

File tree

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
# Changelog
22

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

README.md

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

5253
Either mode can **start the dashboard from the panel** (no terminal) — the Start/Stop
5354
control hits the gated `POST /api/plugins/agent_browser/dashboard`.

browser_panel.py

Lines changed: 65 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -138,71 +138,101 @@ async def _dash_control(body: dict = Body(...)):
138138
return router
139139

140140

141-
# ── full mode: a LAUNCHER for agent-browser's dashboard (not an embed) ──────────
142-
# The dashboard is a prebuilt Next.js app whose assets are root-absolute (/_next/…)
143-
# with no base-path option, so it can't render under our sub-path panel — it only
144-
# loads at its OWN origin. So full mode is a launcher: a Start/Stop control + an
145-
# "Open dashboard ↗" link to that origin (reachable on a local/host setup). For a
146-
# remote member, minimal mode is the one that works.
141+
# ── full mode (default): embed agent-browser's dashboard at its OWN origin ──────
142+
# The dashboard is a Next.js app with root-absolute assets (no base-path), so it only
143+
# renders at its own origin (http://<host>:<port>/) — never under a sub-path proxy. Full
144+
# mode therefore ASSUMES A LOCAL setup (console + agent-browser on one machine) and iframes
145+
# that loopback origin directly. When the console is opened remotely (a fleet member, or a
146+
# non-loopback host, or over https) that loopback dashboard isn't reachable from the
147+
# operator's browser — we DETECT that and show a clear error instead of a blank frame.
147148
_FULL_PAGE = r"""<!doctype html><html lang="en"><head><meta charset="utf-8">
148149
<meta name="viewport" content="width=device-width, initial-scale=1"><title>Browser</title>
149150
<link id="dskit" rel="stylesheet" href="">
150151
<script>
151152
const BASE=location.pathname.split("/plugins/")[0];
152153
document.getElementById("dskit").href=BASE+"/_ds/plugin-kit.css";
153-
const DASH_PORT=__DASH_PORT__;
154-
function dashOpenUrl(){ return "http://"+location.hostname+":"+DASH_PORT+"/"; }
154+
const PORT=__DASH_PORT__;
155+
const LOOPBACK=["localhost","127.0.0.1","[::1]","::1"].indexOf(location.hostname)>=0;
156+
const LOCAL=BASE==="" && LOOPBACK; // host view (not fleet-proxied) + browser on the box
157+
const MIXED=location.protocol==="https:"; // dashboard is http; an https page can't embed it
158+
const DASH_URL="http://"+location.hostname+":"+PORT+"/";
155159
</script>
156160
<style>
157161
html,body{margin:0;height:100%;background:var(--pl-color-bg);color:var(--pl-color-fg);
158162
font-family:var(--pl-font-sans);font-size:13px}
159-
.bar{height:38px;display:flex;align-items:center;gap:8px;padding:0 12px;
163+
.bar{height:34px;display:flex;align-items:center;gap:8px;padding:0 12px;
160164
border-bottom:var(--pl-border-width) solid var(--pl-color-border)}
161165
.bar b{color:var(--pl-color-accent)}
162166
.dot{width:7px;height:7px;border-radius:50%;display:inline-block;flex:none}
163167
.dot.ok{background:#22c55e}.dot.off{background:var(--pl-color-fg-muted,#9aa0aa)}
164-
.stage{height:calc(100% - 38px);display:flex;align-items:center;justify-content:center;
165-
padding:24px;background:var(--pl-color-bg-inset);box-sizing:border-box}
166-
.card{max-width:440px;text-align:center}
168+
.stage{height:calc(100% - 34px);position:relative;background:var(--pl-color-bg-inset)}
169+
iframe{display:none;width:100%;height:100%;border:0;background:var(--pl-color-bg)}
170+
#msg{display:none;position:absolute;inset:0;align-items:center;justify-content:center;padding:24px;box-sizing:border-box}
171+
.card{max-width:460px;text-align:center}
167172
.card .t{font-size:15px;font-weight:600;margin-bottom:8px}
168-
.card .d{color:var(--pl-color-fg-muted);line-height:1.6;margin-bottom:16px}
169-
.card .tip{margin-top:16px;font-size:11.5px;color:var(--pl-color-fg-muted)}
170-
.card code{background:var(--pl-color-bg-subtle,rgba(127,127,127,.14));padding:.1em .35em;border-radius:4px}
173+
.card .d{color:var(--pl-color-fg-muted);line-height:1.6}
174+
.card code{background:var(--pl-color-bg-subtle,rgba(127,127,127,.16));padding:.1em .35em;border-radius:4px}
171175
</style></head><body>
172176
<div class="bar"><b>Browser</b><span id="dash" title="agent-browser dashboard"></span></div>
173-
<div class="stage"><div class="card">
174-
<div class="t">agent-browser dashboard</div>
175-
<div class="d">The full dashboard — live viewport plus activity, console, and network
176-
feeds — runs in its own window. It can't embed in this panel (its assets load from the
177-
page root, which a sub-path panel can't serve), so it opens in a new tab.</div>
178-
<a id="openlink" class="pl-btn pl-btn--primary" href="" target="_blank" rel="noopener">Open dashboard ↗</a>
179-
<div class="tip">Want the browser <em>inside</em> the console? Set
180-
<code>panel_mode: minimal</code> — a live viewport you can drive right here.</div>
181-
</div></div>
177+
<div class="stage">
178+
<iframe id="f" referrerpolicy="no-referrer" allow="clipboard-read; clipboard-write"></iframe>
179+
<div id="msg"><div class="card"><div class="t" id="mt"></div><div class="d" id="md"></div></div></div>
180+
</div>
182181
<script type="module">
183-
document.getElementById("openlink").href=dashOpenUrl();
182+
const $=(id)=>document.getElementById(id);
184183
let kit;
185184
try { kit = await import(BASE + "/_ds/plugin-kit.js"); kit.initPluginView(); }
186185
catch (e) { kit = { initPluginView(){}, apiFetch: (p, i) => fetch(BASE + p, i) }; }
187-
// Dashboard control — start/stop the daemon from the panel (no terminal).
188-
function renderDash(running){
189-
const el=document.getElementById("dash"); if(running===null){ el.innerHTML=""; return; }
190-
el.innerHTML = running
186+
187+
function showFrame(){ $("f").src=DASH_URL; $("f").style.display="block"; $("msg").style.display="none"; }
188+
function showMsg(title, html){ $("mt").textContent=title; $("md").innerHTML=html;
189+
$("msg").style.display="flex"; $("f").style.display="none"; }
190+
191+
function renderDash(state){
192+
const el=$("dash");
193+
if(!LOCAL || MIXED || state==null){ el.innerHTML=""; return; } // control only matters when embeddable
194+
el.innerHTML = state
191195
? '<span class="dot ok"></span> running <button class="pl-btn pl-btn--ghost pl-btn--sm" onclick="dashAct(\'stop\')">Stop</button>'
192196
: '<span class="dot off"></span> stopped <button class="pl-btn pl-btn--sm" onclick="dashAct(\'start\')">Start dashboard</button>';
193197
}
194-
async function dashStatus(){
195-
try{ const r=await kit.apiFetch("/api/plugins/agent_browser/dashboard"); const d=await r.json();
196-
renderDash(!!d.running); }catch(_){ renderDash(null); }
198+
async function dashRunning(){
199+
try{ const r=await kit.apiFetch("/api/plugins/agent_browser/dashboard"); return !!(await r.json()).running; }
200+
catch(_){ return null; }
201+
}
202+
async function decide(){
203+
if(!LOCAL){
204+
renderDash(null);
205+
showMsg("Open the console locally to see the dashboard",
206+
"The dashboard embeds agent-browser's <b>local</b> dashboard — it only loads on the same machine that runs it. "
207+
+ (BASE ? "This panel is served through the fleet proxy" : "You're reaching the console at <code>"+location.hostname+"</code>")
208+
+ ", so its <code>localhost:"+PORT+"</code> dashboard isn't reachable from your browser.<br><br>"
209+
+ "Open the console at <code>http://localhost</code> on the host, or set <code>panel_mode: minimal</code> "
210+
+ "for a screenshot viewport that works remotely.");
211+
return;
212+
}
213+
if(MIXED){
214+
renderDash(null);
215+
showMsg("Can't embed the dashboard over https",
216+
"The console is served over https, but agent-browser's dashboard is http — browsers block that mix. "
217+
+ "Open the console at <code>http://localhost</code>, or set <code>panel_mode: minimal</code>.");
218+
return;
219+
}
220+
const running = await dashRunning(); renderDash(running);
221+
if(running){ showFrame(); }
222+
else { showMsg("Dashboard not running",
223+
"Click <b>Start dashboard</b> in the top bar to launch it and view it here "
224+
+ "(or run <code>agent-browser dashboard start</code>)."); }
197225
}
198226
async function dashAct(action){
199-
document.getElementById("dash").innerHTML='<span class="dot off"></span> …';
227+
$("dash").innerHTML='<span class="dot off"></span> …';
200228
try{ await kit.apiFetch("/api/plugins/agent_browser/dashboard",{method:"POST",
201229
headers:{"Content-Type":"application/json"},body:JSON.stringify({action})}); }catch(_){}
202-
setTimeout(dashStatus, 900);
230+
setTimeout(decide, action==="start"?1500:700);
203231
}
204232
window.dashAct=dashAct;
205-
dashStatus(); setInterval(dashStatus, 6000);
233+
decide();
234+
// While local + not yet embedded, re-check so a dashboard started elsewhere appears on its own.
235+
setInterval(()=>{ if(LOCAL && !MIXED && $("f").style.display==="none") decide(); }, 6000);
206236
</script></body></html>"""
207237

208238

protoagent.plugin.yaml

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

4546
# Editable in Settings ▸ Plugins (ADR 0019) — the operator knobs above as UI fields.
4647
settings:
47-
- { 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." }
48+
- { 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." }
4849
- { key: headed, label: "Headed browser", type: bool, description: "Show a real browser window instead of running headless." }
4950
- { key: allowed_domains, label: "Allowed domains", type: string, description: "Comma-separated navigable-domain allowlist (e.g. example.com,*.foo.com). Blank = unrestricted." }
5051
- { key: confirm_actions, label: "Confirm actions", type: string, description: "Comma-separated action categories that require confirmation before the agent runs them." }

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "agent-browser-plugin"
3-
version = "0.4.0"
3+
version = "0.5.0"
44
description = "Browser-automation plugin for protoAgent, backed by vercel-labs/agent-browser (tools + skill + workflows + a live browser panel)."
55
requires-python = ">=3.11"
66

tests/test_agent_browser.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ def _app(cfg=None):
177177
return app
178178

179179

180-
def test_panel_page_full_mode_is_a_launcher():
180+
def test_panel_page_full_mode_embeds_local_or_errors():
181181
from fastapi.testclient import TestClient
182182

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

198199

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

212213

213-
def test_default_panel_mode_is_minimal():
214+
def test_default_panel_mode_is_full():
214215
import yaml
215216

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

221222

222223
def test_dashboard_control_endpoint(monkeypatch):

0 commit comments

Comments
 (0)