@@ -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 }}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Generated
+63
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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 && 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");
|
||||||
|
|||||||
@@ -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(())
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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!(
|
||||||
|
|||||||
@@ -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>"#;
|
|
||||||
|
|||||||
@@ -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,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
@@ -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:
|
||||||
|
|||||||
@@ -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,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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user