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
+4 -19
View File
@@ -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!(
+20 -44
View File
@@ -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>"#;
-22
View File
@@ -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 -15
View File
@@ -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;
}
}