embed dashboard (#1169)

* embed dashboard

* docs

* fmt
This commit is contained in:
Chris Tate
2026-04-06 10:45:08 -05:00
committed by GitHub
parent 3896ed0d9d
commit 1f8757b215
15 changed files with 116 additions and 272 deletions
+5 -22
View File
@@ -121,6 +121,9 @@ jobs:
- name: Sync version - name: Sync version
run: pnpm run version:sync run: pnpm run version:sync
- name: Build dashboard
run: pnpm --filter dashboard build
- name: Setup Rust toolchain - name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
with: with:
@@ -288,26 +291,6 @@ jobs:
fi fi
echo "Found $BINARY_COUNT binaries" echo "Found $BINARY_COUNT binaries"
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build dashboard
run: pnpm --filter dashboard build
- name: Create dashboard.zip
run: cd packages/dashboard/out && zip -r ../../../dashboard.zip .
- name: Extract changelog entry - name: Extract changelog entry
run: | run: |
VERSION="${{ needs.check-release.outputs.version }}" VERSION="${{ needs.check-release.outputs.version }}"
@@ -327,13 +310,13 @@ jobs:
if gh release view "$TAG" &>/dev/null; then if gh release view "$TAG" &>/dev/null; then
echo "Release $TAG already exists, uploading assets..." echo "Release $TAG already exists, uploading assets..."
gh release upload "$TAG" bin/agent-browser-* dashboard.zip --clobber gh release upload "$TAG" bin/agent-browser-* --clobber
else else
echo "Creating release $TAG..." echo "Creating release $TAG..."
gh release create "$TAG" \ gh release create "$TAG" \
--title "$TAG" \ --title "$TAG" \
--notes-file /tmp/release-notes.md \ --notes-file /tmp/release-notes.md \
bin/agent-browser-* dashboard.zip bin/agent-browser-*
fi fi
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-3
View File
@@ -639,9 +639,6 @@ This is useful for multimodal AI models that can reason about visual layout, unl
Monitor agent-browser sessions in real time with a local web dashboard showing a live viewport and command activity feed. Monitor agent-browser sessions in real time with a local web dashboard showing a live viewport and command activity feed.
```bash ```bash
# Install the dashboard (one time)
agent-browser dashboard install
# Start the dashboard server (runs in background on port 4848) # Start the dashboard server (runs in background on port 4848)
agent-browser dashboard start agent-browser dashboard start
agent-browser dashboard start --port 8080 # Custom port agent-browser dashboard start --port 8080 # Custom port
+63
View File
@@ -59,6 +59,7 @@ dependencies = [
"image", "image",
"libc", "libc",
"reqwest", "reqwest",
"rust-embed",
"serde", "serde",
"serde_json", "serde_json",
"sha2", "sha2",
@@ -1738,6 +1739,40 @@ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[package]]
name = "rust-embed"
version = "8.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04113cb9355a377d83f06ef1f0a45b8ab8cd7d8b1288160717d66df5c7988d27"
dependencies = [
"rust-embed-impl",
"rust-embed-utils",
"walkdir",
]
[[package]]
name = "rust-embed-impl"
version = "8.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da0902e4c7c8e997159ab384e6d0fc91c221375f6894346ae107f47dd0f3ccaa"
dependencies = [
"proc-macro2",
"quote",
"rust-embed-utils",
"syn",
"walkdir",
]
[[package]]
name = "rust-embed-utils"
version = "8.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5bcdef0be6fe7f6fa333b1073c949729274b05f123a0ad7efcb8efd878e5c3b1"
dependencies = [
"sha2",
"walkdir",
]
[[package]] [[package]]
name = "rustc-hash" name = "rustc-hash"
version = "2.1.1" version = "2.1.1"
@@ -1791,6 +1826,15 @@ version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]] [[package]]
name = "semver" name = "semver"
version = "1.0.27" version = "1.0.27"
@@ -2340,6 +2384,16 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "walkdir"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
dependencies = [
"same-file",
"winapi-util",
]
[[package]] [[package]]
name = "want" name = "want"
version = "0.3.1" version = "0.3.1"
@@ -2523,6 +2577,15 @@ version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
[[package]]
name = "winapi-util"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys 0.61.2",
]
[[package]] [[package]]
name = "windows-core" name = "windows-core"
version = "0.62.2" version = "0.62.2"
+1
View File
@@ -34,6 +34,7 @@ hmac = "0.12"
hex = "0.4" hex = "0.4"
chrono = "0.4" chrono = "0.4"
urlencoding = "2" urlencoding = "2"
rust-embed = "8"
[target.'cfg(unix)'.dependencies] [target.'cfg(unix)'.dependencies]
libc = "0.2" libc = "0.2"
+17
View File
@@ -3,7 +3,24 @@ use std::env;
use std::fs; use std::fs;
use std::path::Path; use std::path::Path;
/// Ensure `packages/dashboard/out/` exists so `rust-embed` doesn't fail during
/// Rust-only dev builds where the dashboard hasn't been built. The placeholder
/// `index.html` is only written when the directory is completely absent.
fn ensure_dashboard_dir() {
let dashboard_out = Path::new("../packages/dashboard/out");
println!("cargo:rerun-if-changed=../packages/dashboard/out");
if !dashboard_out.join("index.html").exists() {
let _ = fs::create_dir_all(dashboard_out);
let _ = fs::write(
dashboard_out.join("index.html"),
"<!DOCTYPE html><html><body><p>Dashboard not built. Run: cd packages/dashboard &amp;&amp; pnpm build</p></body></html>\n",
);
}
}
fn main() { fn main() {
ensure_dashboard_dir();
let protocol_dir = Path::new("cdp-protocol"); let protocol_dir = Path::new("cdp-protocol");
let out_dir = env::var("OUT_DIR").unwrap(); let out_dir = env::var("OUT_DIR").unwrap();
let out_path = Path::new(&out_dir).join("cdp_generated.rs"); let out_path = Path::new(&out_dir).join("cdp_generated.rs");
-121
View File
@@ -772,83 +772,6 @@ fn package_exists_apt(pkg: &str) -> bool {
.unwrap_or(false) .unwrap_or(false)
} }
// ---------------------------------------------------------------------------
// Dashboard install
// ---------------------------------------------------------------------------
pub fn get_dashboard_dir() -> PathBuf {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".agent-browser")
.join("dashboard")
}
const DASHBOARD_VERSION: &str = env!("CARGO_PKG_VERSION");
fn dashboard_download_url() -> String {
format!(
"https://github.com/vercel-labs/agent-browser/releases/download/v{}/dashboard.zip",
DASHBOARD_VERSION
)
}
pub fn run_dashboard_install() {
println!("{}", color::cyan("Installing dashboard..."));
let dest = get_dashboard_dir();
if dest.join("index.html").exists() {
println!(
"{} Dashboard is already installed at {}",
color::success_indicator(),
dest.display()
);
return;
}
let url = dashboard_download_url();
println!(" Downloading dashboard v{}", DASHBOARD_VERSION);
println!(" {}", url);
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap_or_else(|e| {
eprintln!(
"{} Failed to create runtime: {}",
color::error_indicator(),
e
);
exit(1);
});
let bytes = match rt.block_on(download_bytes(&url)) {
Ok(b) => b,
Err(e) => {
eprintln!("{} {}", color::error_indicator(), e);
eprintln!(" The dashboard may not be available for this version yet.");
eprintln!(" You can build it locally: cd packages/dashboard && pnpm build");
exit(1);
}
};
match extract_dashboard_zip(bytes, &dest) {
Ok(()) => {
println!(
"{} Dashboard v{} installed successfully",
color::success_indicator(),
DASHBOARD_VERSION
);
println!(" Location: {}", dest.display());
}
Err(e) => {
let _ = fs::remove_dir_all(&dest);
eprintln!("{} {}", color::error_indicator(), e);
exit(1);
}
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@@ -1037,47 +960,3 @@ mod tests {
); );
} }
} }
fn extract_dashboard_zip(bytes: Vec<u8>, dest: &Path) -> Result<(), String> {
fs::create_dir_all(dest).map_err(|e| format!("Failed to create directory: {}", e))?;
let cursor = io::Cursor::new(bytes);
let mut archive =
zip::ZipArchive::new(cursor).map_err(|e| format!("Failed to read zip archive: {}", e))?;
for i in 0..archive.len() {
let mut file = archive
.by_index(i)
.map_err(|e| format!("Failed to read zip entry: {}", e))?;
let enclosed = match file.enclosed_name() {
Some(name) => name.to_owned(),
None => continue,
};
let rel_path = enclosed.to_string_lossy().to_string();
if rel_path.is_empty() || file.is_dir() {
if file.is_dir() {
let out_dir = dest.join(&rel_path);
let _ = fs::create_dir_all(&out_dir);
}
continue;
}
let out_path = dest.join(&rel_path);
if !out_path.starts_with(dest) {
continue;
}
if let Some(parent) = out_path.parent() {
fs::create_dir_all(parent)
.map_err(|e| format!("Failed to create parent dir {}: {}", parent.display(), e))?;
}
let mut out_file = fs::File::create(&out_path)
.map_err(|e| format!("Failed to create file {}: {}", out_path.display(), e))?;
io::copy(&mut file, &mut out_file)
.map_err(|e| format!("Failed to write {}: {}", out_path.display(), e))?;
}
Ok(())
}
-4
View File
@@ -654,10 +654,6 @@ fn main() {
// Handle dashboard subcommand // Handle dashboard subcommand
if clean.first().map(|s| s.as_str()) == Some("dashboard") { if clean.first().map(|s| s.as_str()) == Some("dashboard") {
match clean.get(1).map(|s| s.as_str()) { match clean.get(1).map(|s| s.as_str()) {
Some("install") => {
install::run_dashboard_install();
return;
}
Some("start") | None => { Some("start") | None => {
let port = clean let port = clean
.iter() .iter()
+4 -19
View File
@@ -1,16 +1,13 @@
use serde_json::{json, Value}; use serde_json::{json, Value};
use std::path::PathBuf;
use std::sync::Arc;
use tokio::io::AsyncWriteExt; use tokio::io::AsyncWriteExt;
use tokio::net::TcpListener; use tokio::net::TcpListener;
use crate::connection::get_socket_dir; use crate::connection::get_socket_dir;
use crate::install::get_dashboard_dir;
use super::chat::{chat_status_json, handle_chat_request, handle_models_request}; use super::chat::{chat_status_json, handle_chat_request, handle_models_request};
use super::discovery::discover_sessions; use super::discovery::discover_sessions;
use super::http::{serve_static_file, CORS_HEADERS, DASHBOARD_NOT_INSTALLED_HTML}; use super::http::{serve_embedded_file, CORS_HEADERS};
pub async fn run_dashboard_server(port: u16) { pub async fn run_dashboard_server(port: u16) {
let addr = format!("127.0.0.1:{}", port); let addr = format!("127.0.0.1:{}", port);
@@ -22,23 +19,17 @@ pub async fn run_dashboard_server(port: u16) {
} }
}; };
let dashboard_dir: Arc<PathBuf> = Arc::from(get_dashboard_dir());
loop { loop {
let Ok((stream, _addr)) = listener.accept().await else { let Ok((stream, _addr)) = listener.accept().await else {
break; break;
}; };
let dash_dir = dashboard_dir.clone();
tokio::spawn(async move { tokio::spawn(async move {
handle_dashboard_connection(stream, dash_dir).await; handle_dashboard_connection(stream).await;
}); });
} }
} }
async fn handle_dashboard_connection( async fn handle_dashboard_connection(mut stream: tokio::net::TcpStream) {
mut stream: tokio::net::TcpStream,
dashboard_dir: Arc<PathBuf>,
) {
use tokio::io::AsyncReadExt; use tokio::io::AsyncReadExt;
let mut buf = vec![0u8; 8192]; let mut buf = vec![0u8; 8192];
@@ -118,14 +109,8 @@ async fn handle_dashboard_connection(
"application/json; charset=utf-8", "application/json; charset=utf-8",
chat_status_json().into_bytes(), chat_status_json().into_bytes(),
) )
} else if dashboard_dir.join("index.html").exists() {
serve_static_file(&dashboard_dir, path)
} else { } else {
( serve_embedded_file(path)
"200 OK",
"text/html; charset=utf-8",
DASHBOARD_NOT_INSTALLED_HTML.as_bytes().to_vec(),
)
}; };
let response = format!( let response = format!(
+20 -44
View File
@@ -1,5 +1,5 @@
use rust_embed::Embed;
use serde_json::{json, Value}; use serde_json::{json, Value};
use std::path::Path;
use std::sync::Arc; use std::sync::Arc;
use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::io::{AsyncReadExt, AsyncWriteExt};
@@ -13,6 +13,10 @@ use super::chat::{chat_status_json, handle_chat_request, handle_models_request};
use super::dashboard::spawn_session; use super::dashboard::spawn_session;
use super::discovery::discover_sessions; use super::discovery::discover_sessions;
#[derive(Embed)]
#[folder = "../packages/dashboard/out/"]
struct DashboardAssets;
pub(super) const CORS_HEADERS: &str = "Access-Control-Allow-Origin: *\r\nAccess-Control-Allow-Methods: GET, POST, OPTIONS\r\nAccess-Control-Allow-Headers: Content-Type\r\n"; pub(super) const CORS_HEADERS: &str = "Access-Control-Allow-Origin: *\r\nAccess-Control-Allow-Methods: GET, POST, OPTIONS\r\nAccess-Control-Allow-Headers: Content-Type\r\n";
/// Build CORS headers that reflect the request origin only when it passes /// Build CORS headers that reflect the request origin only when it passes
@@ -42,7 +46,6 @@ fn parse_origin(peeked: &[u8]) -> Option<String> {
pub(super) async fn handle_http_request( pub(super) async fn handle_http_request(
mut stream: tokio::net::TcpStream, mut stream: tokio::net::TcpStream,
peeked: &[u8], peeked: &[u8],
dashboard_dir: Option<&Path>,
last_tabs: &Arc<RwLock<Vec<Value>>>, last_tabs: &Arc<RwLock<Vec<Value>>>,
last_engine: &Arc<RwLock<String>>, last_engine: &Arc<RwLock<String>>,
session_name: &str, session_name: &str,
@@ -163,14 +166,7 @@ pub(super) async fn handle_http_request(
chat_status_json().into_bytes(), chat_status_json().into_bytes(),
) )
} else { } else {
match dashboard_dir { serve_embedded_file(path)
Some(dir) => serve_static_file(dir, path),
None => (
"200 OK",
"text/html; charset=utf-8",
DASHBOARD_NOT_INSTALLED_HTML.as_bytes().to_vec(),
),
}
}; };
let response = format!( let response = format!(
@@ -282,25 +278,19 @@ pub(super) async fn relay_command_to_daemon(
Ok(response_line.trim().to_string()) Ok(response_line.trim().to_string())
} }
pub(super) fn serve_static_file( pub(super) fn serve_embedded_file(url_path: &str) -> (&'static str, &'static str, Vec<u8>) {
dir: &Path,
url_path: &str,
) -> (&'static str, &'static str, Vec<u8>) {
let clean = url_path.trim_start_matches('/'); let clean = url_path.trim_start_matches('/');
let file_path = if clean.is_empty() { let key = if clean.is_empty() {
dir.join("index.html") "index.html"
} else { } else {
let joined = dir.join(clean); clean
if joined.is_file() {
joined
} else {
dir.join("index.html")
}
}; };
match std::fs::read(&file_path) { let file = DashboardAssets::get(key).or_else(|| DashboardAssets::get("index.html"));
Ok(content) => {
let ext = file_path.extension().and_then(|e| e.to_str()).unwrap_or(""); match file {
Some(content) => {
let ext = key.rsplit('.').next().unwrap_or("");
let ct = match ext { let ct = match ext {
"html" => "text/html; charset=utf-8", "html" => "text/html; charset=utf-8",
"js" => "application/javascript; charset=utf-8", "js" => "application/javascript; charset=utf-8",
@@ -309,31 +299,17 @@ pub(super) fn serve_static_file(
"svg" => "image/svg+xml", "svg" => "image/svg+xml",
"png" => "image/png", "png" => "image/png",
"ico" => "image/x-icon", "ico" => "image/x-icon",
"woff2" => "font/woff2",
"woff" => "font/woff",
"txt" => "text/plain; charset=utf-8",
_ => "application/octet-stream", _ => "application/octet-stream",
}; };
("200 OK", ct, content) ("200 OK", ct, content.data.to_vec())
} }
Err(_) => ( None => (
"404 Not Found", "404 Not Found",
"text/html; charset=utf-8", "text/html; charset=utf-8",
b"<html><body><p>404 Not Found</p></body></html>".to_vec(), b"<html><body><p>404 Not Found</p></body></html>".to_vec(),
), ),
} }
} }
pub(super) const DASHBOARD_NOT_INSTALLED_HTML: &str = r#"<!DOCTYPE html>
<html lang="en">
<head><meta charset="utf-8"><title>agent-browser</title>
<style>
body { font-family: system-ui, sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #0a0a0a; color: #e5e5e5; }
.card { text-align: center; max-width: 400px; }
code { background: #262626; padding: 2px 8px; border-radius: 4px; font-size: 14px; }
</style>
</head>
<body>
<div class="card">
<h2>Dashboard not installed</h2>
<p>Run <code>agent-browser dashboard install</code> to download the dashboard.</p>
</div>
</body>
</html>"#;
-22
View File
@@ -9,7 +9,6 @@ pub use cdp_loop::{ack_screencast_frame, start_screencast, stop_screencast};
pub use dashboard::run_dashboard_server; pub use dashboard::run_dashboard_server;
use serde_json::{json, Value}; use serde_json::{json, Value};
use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
use tokio::net::TcpListener; use tokio::net::TcpListener;
@@ -55,7 +54,6 @@ pub struct StreamServer {
screencasting: Arc<Mutex<bool>>, screencasting: Arc<Mutex<bool>>,
viewport_width: Arc<Mutex<u32>>, viewport_width: Arc<Mutex<u32>>,
viewport_height: Arc<Mutex<u32>>, viewport_height: Arc<Mutex<u32>>,
dashboard_dir: Option<PathBuf>,
last_tabs: Arc<RwLock<Vec<Value>>>, last_tabs: Arc<RwLock<Vec<Value>>>,
last_engine: Arc<RwLock<String>>, last_engine: Arc<RwLock<String>>,
last_frame: Arc<RwLock<Option<String>>>, last_frame: Arc<RwLock<Option<String>>>,
@@ -91,16 +89,6 @@ impl StreamServer {
Self::start_inner(preferred_port, client_slot, session_id, allow_port_fallback).await Self::start_inner(preferred_port, client_slot, session_id, allow_port_fallback).await
} }
/// Resolve the dashboard directory if it exists.
fn resolve_dashboard_dir() -> Option<PathBuf> {
let dir = dirs::home_dir()?.join(".agent-browser").join("dashboard");
if dir.join("index.html").exists() {
Some(dir)
} else {
None
}
}
/// Notify the background CDP listener that the client has changed (browser launched/closed). /// Notify the background CDP listener that the client has changed (browser launched/closed).
pub fn notify_client_changed(&self) { pub fn notify_client_changed(&self) {
self.client_notify.notify_one(); self.client_notify.notify_one();
@@ -181,8 +169,6 @@ impl StreamServer {
.map_err(|e| format!("Failed to get stream address: {}", e))?; .map_err(|e| format!("Failed to get stream address: {}", e))?;
let port = actual_addr.port(); let port = actual_addr.port();
let dashboard_dir = Self::resolve_dashboard_dir();
let (frame_tx, _) = broadcast::channel::<String>(64); let (frame_tx, _) = broadcast::channel::<String>(64);
let client_count = Arc::new(Mutex::new(0usize)); let client_count = Arc::new(Mutex::new(0usize));
let client_notify = Arc::new(Notify::new()); let client_notify = Arc::new(Notify::new());
@@ -205,7 +191,6 @@ impl StreamServer {
let vw_clone = viewport_width.clone(); let vw_clone = viewport_width.clone();
let vh_clone = viewport_height.clone(); let vh_clone = viewport_height.clone();
let dashboard_dir_clone = dashboard_dir.clone();
let last_tabs_clone = last_tabs.clone(); let last_tabs_clone = last_tabs.clone();
let last_engine_clone = last_engine.clone(); let last_engine_clone = last_engine.clone();
let last_frame_clone = last_frame.clone(); let last_frame_clone = last_frame.clone();
@@ -223,7 +208,6 @@ impl StreamServer {
cdp_session_clone, cdp_session_clone,
vw_clone, vw_clone,
vh_clone, vh_clone,
dashboard_dir_clone,
last_tabs_clone, last_tabs_clone,
last_engine_clone, last_engine_clone,
last_frame_clone, last_frame_clone,
@@ -277,7 +261,6 @@ impl StreamServer {
screencasting, screencasting,
viewport_width, viewport_width,
viewport_height, viewport_height,
dashboard_dir,
last_tabs, last_tabs,
last_engine, last_engine,
last_frame, last_frame,
@@ -435,11 +418,6 @@ impl StreamServer {
}); });
let _ = self.frame_tx.send(msg.to_string()); let _ = self.frame_tx.send(msg.to_string());
} }
/// Whether the dashboard directory is available.
pub fn has_dashboard(&self) -> bool {
self.dashboard_dir.is_some()
}
} }
pub(crate) fn timestamp_ms() -> u64 { pub(crate) fn timestamp_ms() -> u64 {
+1 -15
View File
@@ -1,6 +1,5 @@
use serde_json::{json, Value}; use serde_json::{json, Value};
use std::net::SocketAddr; use std::net::SocketAddr;
use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
use futures_util::{SinkExt, StreamExt}; use futures_util::{SinkExt, StreamExt};
@@ -24,7 +23,6 @@ pub(super) async fn accept_loop(
cdp_session_id: Arc<RwLock<Option<String>>>, cdp_session_id: Arc<RwLock<Option<String>>>,
viewport_width: Arc<Mutex<u32>>, viewport_width: Arc<Mutex<u32>>,
viewport_height: Arc<Mutex<u32>>, viewport_height: Arc<Mutex<u32>>,
dashboard_dir: Option<PathBuf>,
last_tabs: Arc<RwLock<Vec<Value>>>, last_tabs: Arc<RwLock<Vec<Value>>>,
last_engine: Arc<RwLock<String>>, last_engine: Arc<RwLock<String>>,
last_frame: Arc<RwLock<Option<String>>>, last_frame: Arc<RwLock<Option<String>>>,
@@ -32,7 +30,6 @@ pub(super) async fn accept_loop(
mut shutdown_rx: watch::Receiver<bool>, mut shutdown_rx: watch::Receiver<bool>,
session_name: String, session_name: String,
) { ) {
let dashboard_dir = dashboard_dir.map(Arc::from);
let session_name: Arc<str> = Arc::from(session_name); let session_name: Arc<str> = Arc::from(session_name);
loop { loop {
tokio::select! { tokio::select! {
@@ -53,7 +50,6 @@ pub(super) async fn accept_loop(
let cdp_session_id = cdp_session_id.clone(); let cdp_session_id = cdp_session_id.clone();
let vw = viewport_width.clone(); let vw = viewport_width.clone();
let vh = viewport_height.clone(); let vh = viewport_height.clone();
let dd = dashboard_dir.clone();
let lt = last_tabs.clone(); let lt = last_tabs.clone();
let le = last_engine.clone(); let le = last_engine.clone();
let lf = last_frame.clone(); let lf = last_frame.clone();
@@ -73,7 +69,6 @@ pub(super) async fn accept_loop(
cdp_session_id, cdp_session_id,
vw, vw,
vh, vh,
dd,
lt, lt,
le, le,
lf, lf,
@@ -112,7 +107,6 @@ async fn handle_connection(
cdp_session_id: Arc<RwLock<Option<String>>>, cdp_session_id: Arc<RwLock<Option<String>>>,
viewport_width: Arc<Mutex<u32>>, viewport_width: Arc<Mutex<u32>>,
viewport_height: Arc<Mutex<u32>>, viewport_height: Arc<Mutex<u32>>,
dashboard_dir: Option<Arc<PathBuf>>,
last_tabs: Arc<RwLock<Vec<Value>>>, last_tabs: Arc<RwLock<Vec<Value>>>,
last_engine: Arc<RwLock<String>>, last_engine: Arc<RwLock<String>>,
last_frame: Arc<RwLock<Option<String>>>, last_frame: Arc<RwLock<Option<String>>>,
@@ -148,15 +142,7 @@ async fn handle_connection(
) )
.await; .await;
} else { } else {
handle_http_request( handle_http_request(stream, &buf[..n], &last_tabs, &last_engine, &session_name).await;
stream,
&buf[..n],
dashboard_dir.as_deref().map(|p| p.as_path()),
&last_tabs,
&last_engine,
&session_name,
)
.await;
} }
} }
+3 -4
View File
@@ -2399,15 +2399,15 @@ Examples:
r##" r##"
agent-browser dashboard - Observability dashboard agent-browser dashboard - Observability dashboard
Usage: agent-browser dashboard [start|stop|install] [options] Usage: agent-browser dashboard [start|stop] [options]
Manage the observability dashboard, a local web UI that shows live Manage the observability dashboard, a local web UI that shows live
browser viewports and command activity feeds for all sessions. browser viewports and command activity feeds for all sessions.
The dashboard is bundled into the binary and requires no separate install.
Subcommands: Subcommands:
start [--port <n>] Start the dashboard server (default port: 4848) start [--port <n>] Start the dashboard server (default port: 4848)
stop Stop the dashboard server stop Stop the dashboard server
install Download and install the dashboard to ~/.agent-browser/dashboard/
Running 'agent-browser dashboard' with no subcommand is equivalent to 'dashboard start'. Running 'agent-browser dashboard' with no subcommand is equivalent to 'dashboard start'.
@@ -2421,7 +2421,6 @@ Global Options:
--json Output as JSON --json Output as JSON
Examples: Examples:
agent-browser dashboard install
agent-browser dashboard start agent-browser dashboard start
agent-browser dashboard start --port 8080 agent-browser dashboard start --port 8080
agent-browser dashboard stop agent-browser dashboard stop
@@ -2842,7 +2841,7 @@ Setup:
install Install browser binaries install Install browser binaries
install --with-deps Also install system dependencies (Linux) install --with-deps Also install system dependencies (Linux)
upgrade Upgrade to the latest version upgrade Upgrade to the latest version
dashboard install Install the observability dashboard dashboard start Start the observability dashboard
profiles List available Chrome profiles profiles List available Chrome profiles
Snapshot Options: Snapshot Options:
-1
View File
@@ -343,7 +343,6 @@ agent-browser --profile Default open https://gmail.com # Reuse a profile's logi
agent-browser dashboard [start] # Start the dashboard server (default port: 4848) agent-browser dashboard [start] # Start the dashboard server (default port: 4848)
agent-browser dashboard start --port <n> # Start on a specific port agent-browser dashboard start --port <n> # Start on a specific port
agent-browser dashboard stop # Stop the dashboard server agent-browser dashboard stop # Stop the dashboard server
agent-browser dashboard install # Install the dashboard files
``` ```
## Chat ## Chat
+2 -14
View File
@@ -2,19 +2,9 @@
Monitor agent-browser sessions in real time with a local web dashboard showing a live browser viewport and command activity feed. Monitor agent-browser sessions in real time with a local web dashboard showing a live browser viewport and command activity feed.
## Install
Download the dashboard once:
```bash
agent-browser dashboard install
```
This downloads the dashboard to `~/.agent-browser/dashboard/` and is served directly by the daemon when streaming is enabled.
## Usage ## Usage
Start the dashboard server and open any session -- it appears automatically: The dashboard is bundled into the binary and requires no separate install. Start the server and open any session:
```bash ```bash
agent-browser dashboard start agent-browser dashboard start
@@ -132,9 +122,7 @@ The dashboard is a Next.js static export (`output: 'export'`) that produces plai
pnpm build:dashboard pnpm build:dashboard
``` ```
The built files are served by the daemon's stream server on the same port used for WebSocket connections. Plain HTTP requests serve the dashboard, while WebSocket upgrade requests are handled as before. The dashboard is embedded into the CLI binary at compile time using `rust-embed`. Plain HTTP requests serve the embedded dashboard assets, while WebSocket upgrade requests are handled as before.
When the dashboard is not installed, visiting the HTTP endpoint shows instructions to run `agent-browser dashboard install`.
## AI Chat ## AI Chat
-3
View File
@@ -789,9 +789,6 @@ Lightpanda does not support `--extension`, `--profile`, `--state`, or `--allow-f
The dashboard is a standalone background server that shows live browser viewports, command activity, and console output for all sessions. The dashboard is a standalone background server that shows live browser viewports, command activity, and console output for all sessions.
```bash ```bash
# Install the dashboard once
agent-browser dashboard install
# Start the dashboard server (background, port 4848) # Start the dashboard server (background, port 4848)
agent-browser dashboard start agent-browser dashboard start