@@ -121,6 +121,9 @@ jobs:
|
||||
- name: Sync version
|
||||
run: pnpm run version:sync
|
||||
|
||||
- name: Build dashboard
|
||||
run: pnpm --filter dashboard build
|
||||
|
||||
- name: Setup Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
@@ -288,26 +291,6 @@ jobs:
|
||||
fi
|
||||
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
|
||||
run: |
|
||||
VERSION="${{ needs.check-release.outputs.version }}"
|
||||
@@ -327,13 +310,13 @@ jobs:
|
||||
|
||||
if gh release view "$TAG" &>/dev/null; then
|
||||
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
|
||||
echo "Creating release $TAG..."
|
||||
gh release create "$TAG" \
|
||||
--title "$TAG" \
|
||||
--notes-file /tmp/release-notes.md \
|
||||
bin/agent-browser-* dashboard.zip
|
||||
bin/agent-browser-*
|
||||
fi
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -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.
|
||||
|
||||
```bash
|
||||
# Install the dashboard (one time)
|
||||
agent-browser dashboard install
|
||||
|
||||
# Start the dashboard server (runs in background on port 4848)
|
||||
agent-browser dashboard start
|
||||
agent-browser dashboard start --port 8080 # Custom port
|
||||
|
||||
Generated
+63
@@ -59,6 +59,7 @@ dependencies = [
|
||||
"image",
|
||||
"libc",
|
||||
"reqwest",
|
||||
"rust-embed",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
@@ -1738,6 +1739,40 @@ dependencies = [
|
||||
"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]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.1"
|
||||
@@ -1791,6 +1826,15 @@ version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "semver"
|
||||
version = "1.0.27"
|
||||
@@ -2340,6 +2384,16 @@ version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "want"
|
||||
version = "0.3.1"
|
||||
@@ -2523,6 +2577,15 @@ version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "windows-core"
|
||||
version = "0.62.2"
|
||||
|
||||
@@ -34,6 +34,7 @@ hmac = "0.12"
|
||||
hex = "0.4"
|
||||
chrono = "0.4"
|
||||
urlencoding = "2"
|
||||
rust-embed = "8"
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
libc = "0.2"
|
||||
|
||||
@@ -3,7 +3,24 @@ use std::env;
|
||||
use std::fs;
|
||||
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 && pnpm build</p></body></html>\n",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
ensure_dashboard_dir();
|
||||
|
||||
let protocol_dir = Path::new("cdp-protocol");
|
||||
let out_dir = env::var("OUT_DIR").unwrap();
|
||||
let out_path = Path::new(&out_dir).join("cdp_generated.rs");
|
||||
|
||||
@@ -772,83 +772,6 @@ fn package_exists_apt(pkg: &str) -> bool {
|
||||
.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)]
|
||||
mod tests {
|
||||
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(())
|
||||
}
|
||||
|
||||
@@ -654,10 +654,6 @@ fn main() {
|
||||
// Handle dashboard subcommand
|
||||
if clean.first().map(|s| s.as_str()) == Some("dashboard") {
|
||||
match clean.get(1).map(|s| s.as_str()) {
|
||||
Some("install") => {
|
||||
install::run_dashboard_install();
|
||||
return;
|
||||
}
|
||||
Some("start") | None => {
|
||||
let port = clean
|
||||
.iter()
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
use serde_json::{json, Value};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::net::TcpListener;
|
||||
|
||||
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::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) {
|
||||
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 {
|
||||
let Ok((stream, _addr)) = listener.accept().await else {
|
||||
break;
|
||||
};
|
||||
let dash_dir = dashboard_dir.clone();
|
||||
tokio::spawn(async move {
|
||||
handle_dashboard_connection(stream, dash_dir).await;
|
||||
handle_dashboard_connection(stream).await;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_dashboard_connection(
|
||||
mut stream: tokio::net::TcpStream,
|
||||
dashboard_dir: Arc<PathBuf>,
|
||||
) {
|
||||
async fn handle_dashboard_connection(mut stream: tokio::net::TcpStream) {
|
||||
use tokio::io::AsyncReadExt;
|
||||
|
||||
let mut buf = vec![0u8; 8192];
|
||||
@@ -118,14 +109,8 @@ async fn handle_dashboard_connection(
|
||||
"application/json; charset=utf-8",
|
||||
chat_status_json().into_bytes(),
|
||||
)
|
||||
} else if dashboard_dir.join("index.html").exists() {
|
||||
serve_static_file(&dashboard_dir, path)
|
||||
} else {
|
||||
(
|
||||
"200 OK",
|
||||
"text/html; charset=utf-8",
|
||||
DASHBOARD_NOT_INSTALLED_HTML.as_bytes().to_vec(),
|
||||
)
|
||||
serve_embedded_file(path)
|
||||
};
|
||||
|
||||
let response = format!(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use rust_embed::Embed;
|
||||
use serde_json::{json, Value};
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
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::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";
|
||||
|
||||
/// 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(
|
||||
mut stream: tokio::net::TcpStream,
|
||||
peeked: &[u8],
|
||||
dashboard_dir: Option<&Path>,
|
||||
last_tabs: &Arc<RwLock<Vec<Value>>>,
|
||||
last_engine: &Arc<RwLock<String>>,
|
||||
session_name: &str,
|
||||
@@ -163,14 +166,7 @@ pub(super) async fn handle_http_request(
|
||||
chat_status_json().into_bytes(),
|
||||
)
|
||||
} else {
|
||||
match dashboard_dir {
|
||||
Some(dir) => serve_static_file(dir, path),
|
||||
None => (
|
||||
"200 OK",
|
||||
"text/html; charset=utf-8",
|
||||
DASHBOARD_NOT_INSTALLED_HTML.as_bytes().to_vec(),
|
||||
),
|
||||
}
|
||||
serve_embedded_file(path)
|
||||
};
|
||||
|
||||
let response = format!(
|
||||
@@ -282,25 +278,19 @@ pub(super) async fn relay_command_to_daemon(
|
||||
Ok(response_line.trim().to_string())
|
||||
}
|
||||
|
||||
pub(super) fn serve_static_file(
|
||||
dir: &Path,
|
||||
url_path: &str,
|
||||
) -> (&'static str, &'static str, Vec<u8>) {
|
||||
pub(super) fn serve_embedded_file(url_path: &str) -> (&'static str, &'static str, Vec<u8>) {
|
||||
let clean = url_path.trim_start_matches('/');
|
||||
let file_path = if clean.is_empty() {
|
||||
dir.join("index.html")
|
||||
let key = if clean.is_empty() {
|
||||
"index.html"
|
||||
} else {
|
||||
let joined = dir.join(clean);
|
||||
if joined.is_file() {
|
||||
joined
|
||||
} else {
|
||||
dir.join("index.html")
|
||||
}
|
||||
clean
|
||||
};
|
||||
|
||||
match std::fs::read(&file_path) {
|
||||
Ok(content) => {
|
||||
let ext = file_path.extension().and_then(|e| e.to_str()).unwrap_or("");
|
||||
let file = DashboardAssets::get(key).or_else(|| DashboardAssets::get("index.html"));
|
||||
|
||||
match file {
|
||||
Some(content) => {
|
||||
let ext = key.rsplit('.').next().unwrap_or("");
|
||||
let ct = match ext {
|
||||
"html" => "text/html; charset=utf-8",
|
||||
"js" => "application/javascript; charset=utf-8",
|
||||
@@ -309,31 +299,17 @@ pub(super) fn serve_static_file(
|
||||
"svg" => "image/svg+xml",
|
||||
"png" => "image/png",
|
||||
"ico" => "image/x-icon",
|
||||
"woff2" => "font/woff2",
|
||||
"woff" => "font/woff",
|
||||
"txt" => "text/plain; charset=utf-8",
|
||||
_ => "application/octet-stream",
|
||||
};
|
||||
("200 OK", ct, content)
|
||||
("200 OK", ct, content.data.to_vec())
|
||||
}
|
||||
Err(_) => (
|
||||
None => (
|
||||
"404 Not Found",
|
||||
"text/html; charset=utf-8",
|
||||
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>"#;
|
||||
|
||||
@@ -9,7 +9,6 @@ pub use cdp_loop::{ack_screencast_frame, start_screencast, stop_screencast};
|
||||
pub use dashboard::run_dashboard_server;
|
||||
|
||||
use serde_json::{json, Value};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use tokio::net::TcpListener;
|
||||
@@ -55,7 +54,6 @@ pub struct StreamServer {
|
||||
screencasting: Arc<Mutex<bool>>,
|
||||
viewport_width: Arc<Mutex<u32>>,
|
||||
viewport_height: Arc<Mutex<u32>>,
|
||||
dashboard_dir: Option<PathBuf>,
|
||||
last_tabs: Arc<RwLock<Vec<Value>>>,
|
||||
last_engine: Arc<RwLock<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
|
||||
}
|
||||
|
||||
/// 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).
|
||||
pub fn notify_client_changed(&self) {
|
||||
self.client_notify.notify_one();
|
||||
@@ -181,8 +169,6 @@ impl StreamServer {
|
||||
.map_err(|e| format!("Failed to get stream address: {}", e))?;
|
||||
let port = actual_addr.port();
|
||||
|
||||
let dashboard_dir = Self::resolve_dashboard_dir();
|
||||
|
||||
let (frame_tx, _) = broadcast::channel::<String>(64);
|
||||
let client_count = Arc::new(Mutex::new(0usize));
|
||||
let client_notify = Arc::new(Notify::new());
|
||||
@@ -205,7 +191,6 @@ impl StreamServer {
|
||||
|
||||
let vw_clone = viewport_width.clone();
|
||||
let vh_clone = viewport_height.clone();
|
||||
let dashboard_dir_clone = dashboard_dir.clone();
|
||||
let last_tabs_clone = last_tabs.clone();
|
||||
let last_engine_clone = last_engine.clone();
|
||||
let last_frame_clone = last_frame.clone();
|
||||
@@ -223,7 +208,6 @@ impl StreamServer {
|
||||
cdp_session_clone,
|
||||
vw_clone,
|
||||
vh_clone,
|
||||
dashboard_dir_clone,
|
||||
last_tabs_clone,
|
||||
last_engine_clone,
|
||||
last_frame_clone,
|
||||
@@ -277,7 +261,6 @@ impl StreamServer {
|
||||
screencasting,
|
||||
viewport_width,
|
||||
viewport_height,
|
||||
dashboard_dir,
|
||||
last_tabs,
|
||||
last_engine,
|
||||
last_frame,
|
||||
@@ -435,11 +418,6 @@ impl StreamServer {
|
||||
});
|
||||
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 {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use serde_json::{json, Value};
|
||||
use std::net::SocketAddr;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
@@ -24,7 +23,6 @@ pub(super) async fn accept_loop(
|
||||
cdp_session_id: Arc<RwLock<Option<String>>>,
|
||||
viewport_width: Arc<Mutex<u32>>,
|
||||
viewport_height: Arc<Mutex<u32>>,
|
||||
dashboard_dir: Option<PathBuf>,
|
||||
last_tabs: Arc<RwLock<Vec<Value>>>,
|
||||
last_engine: Arc<RwLock<String>>,
|
||||
last_frame: Arc<RwLock<Option<String>>>,
|
||||
@@ -32,7 +30,6 @@ pub(super) async fn accept_loop(
|
||||
mut shutdown_rx: watch::Receiver<bool>,
|
||||
session_name: String,
|
||||
) {
|
||||
let dashboard_dir = dashboard_dir.map(Arc::from);
|
||||
let session_name: Arc<str> = Arc::from(session_name);
|
||||
loop {
|
||||
tokio::select! {
|
||||
@@ -53,7 +50,6 @@ pub(super) async fn accept_loop(
|
||||
let cdp_session_id = cdp_session_id.clone();
|
||||
let vw = viewport_width.clone();
|
||||
let vh = viewport_height.clone();
|
||||
let dd = dashboard_dir.clone();
|
||||
let lt = last_tabs.clone();
|
||||
let le = last_engine.clone();
|
||||
let lf = last_frame.clone();
|
||||
@@ -73,7 +69,6 @@ pub(super) async fn accept_loop(
|
||||
cdp_session_id,
|
||||
vw,
|
||||
vh,
|
||||
dd,
|
||||
lt,
|
||||
le,
|
||||
lf,
|
||||
@@ -112,7 +107,6 @@ async fn handle_connection(
|
||||
cdp_session_id: Arc<RwLock<Option<String>>>,
|
||||
viewport_width: Arc<Mutex<u32>>,
|
||||
viewport_height: Arc<Mutex<u32>>,
|
||||
dashboard_dir: Option<Arc<PathBuf>>,
|
||||
last_tabs: Arc<RwLock<Vec<Value>>>,
|
||||
last_engine: Arc<RwLock<String>>,
|
||||
last_frame: Arc<RwLock<Option<String>>>,
|
||||
@@ -148,15 +142,7 @@ async fn handle_connection(
|
||||
)
|
||||
.await;
|
||||
} else {
|
||||
handle_http_request(
|
||||
stream,
|
||||
&buf[..n],
|
||||
dashboard_dir.as_deref().map(|p| p.as_path()),
|
||||
&last_tabs,
|
||||
&last_engine,
|
||||
&session_name,
|
||||
)
|
||||
.await;
|
||||
handle_http_request(stream, &buf[..n], &last_tabs, &last_engine, &session_name).await;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-4
@@ -2399,15 +2399,15 @@ Examples:
|
||||
r##"
|
||||
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
|
||||
browser viewports and command activity feeds for all sessions.
|
||||
The dashboard is bundled into the binary and requires no separate install.
|
||||
|
||||
Subcommands:
|
||||
start [--port <n>] Start the dashboard server (default port: 4848)
|
||||
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'.
|
||||
|
||||
@@ -2421,7 +2421,6 @@ Global Options:
|
||||
--json Output as JSON
|
||||
|
||||
Examples:
|
||||
agent-browser dashboard install
|
||||
agent-browser dashboard start
|
||||
agent-browser dashboard start --port 8080
|
||||
agent-browser dashboard stop
|
||||
@@ -2842,7 +2841,7 @@ Setup:
|
||||
install Install browser binaries
|
||||
install --with-deps Also install system dependencies (Linux)
|
||||
upgrade Upgrade to the latest version
|
||||
dashboard install Install the observability dashboard
|
||||
dashboard start Start the observability dashboard
|
||||
profiles List available Chrome profiles
|
||||
|
||||
Snapshot Options:
|
||||
|
||||
@@ -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 --port <n> # Start on a specific port
|
||||
agent-browser dashboard stop # Stop the dashboard server
|
||||
agent-browser dashboard install # Install the dashboard files
|
||||
```
|
||||
|
||||
## Chat
|
||||
|
||||
@@ -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.
|
||||
|
||||
## 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
|
||||
|
||||
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
|
||||
agent-browser dashboard start
|
||||
@@ -132,9 +122,7 @@ The dashboard is a Next.js static export (`output: 'export'`) that produces plai
|
||||
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.
|
||||
|
||||
When the dashboard is not installed, visiting the HTTP endpoint shows instructions to run `agent-browser dashboard install`.
|
||||
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.
|
||||
|
||||
## AI Chat
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
```bash
|
||||
# Install the dashboard once
|
||||
agent-browser dashboard install
|
||||
|
||||
# Start the dashboard server (background, port 4848)
|
||||
agent-browser dashboard start
|
||||
|
||||
|
||||
Reference in New Issue
Block a user