diff --git a/cli/src/main.rs b/cli/src/main.rs index e046b8330..ebd21a649 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -202,6 +202,10 @@ fn get_dashboard_pid_path() -> std::path::PathBuf { get_socket_dir().join("dashboard.pid") } +fn format_dashboard_url(host: &str, port: u16) -> String { + format!("http://{}:{}", host, port) +} + fn is_pid_alive(pid: u32) -> bool { #[cfg(unix)] { @@ -221,8 +225,9 @@ fn is_pid_alive(pid: u32) -> bool { } } -fn run_dashboard_start(port: u16, json_mode: bool) { +fn run_dashboard_start(host: &str, port: u16, json_mode: bool) { let pid_path = get_dashboard_pid_path(); + let dashboard_url = format_dashboard_url(host, port); // Check if already running if let Ok(pid_str) = fs::read_to_string(&pid_path) { @@ -231,10 +236,10 @@ fn run_dashboard_start(port: u16, json_mode: bool) { if json_mode { print_json_value(json!({ "success": true, - "data": { "port": port, "pid": pid, "already_running": true }, + "data": { "host": host, "port": port, "pid": pid, "already_running": true }, })); } else { - println!("Dashboard already running at http://localhost:{}", port); + println!("Dashboard already running at {}", dashboard_url); } return; } @@ -265,6 +270,7 @@ fn run_dashboard_start(port: u16, json_mode: bool) { let mut cmd = std::process::Command::new(&exe_path); cmd.env("AGENT_BROWSER_DASHBOARD", "1") + .env("AGENT_BROWSER_DASHBOARD_HOST", host) .env("AGENT_BROWSER_DASHBOARD_PORT", port.to_string()); #[cfg(unix)] @@ -299,10 +305,10 @@ fn run_dashboard_start(port: u16, json_mode: bool) { if json_mode { print_json_value(json!({ "success": true, - "data": { "port": port, "pid": pid }, + "data": { "host": host, "port": port, "pid": pid }, })); } else { - println!("Dashboard started at http://localhost:{}", port); + println!("Dashboard started at {}", dashboard_url); } } Err(e) => { @@ -501,12 +507,14 @@ fn main() { // Standalone dashboard server mode if env::var("AGENT_BROWSER_DASHBOARD").is_ok() { + let host = + env::var("AGENT_BROWSER_DASHBOARD_HOST").unwrap_or_else(|_| "127.0.0.1".to_string()); let port: u16 = env::var("AGENT_BROWSER_DASHBOARD_PORT") .ok() .and_then(|s| s.parse().ok()) .unwrap_or(4848); let rt = tokio::runtime::Runtime::new().expect("Failed to create tokio runtime"); - rt.block_on(native::stream::run_dashboard_server(port)); + rt.block_on(native::stream::run_dashboard_server(&host, port)); return; } @@ -558,13 +566,20 @@ fn main() { return; } Some("start") | None => { + let host = clean + .iter() + .position(|a| a == "--host") + .or_else(|| clean.iter().position(|a| a == "--hostname")) + .and_then(|i| clean.get(i + 1)) + .map(|s| s.as_str()) + .unwrap_or("127.0.0.1"); let port = clean .iter() .position(|a| a == "--port") .and_then(|i| clean.get(i + 1)) .and_then(|s| s.parse::().ok()) .unwrap_or(4848); - run_dashboard_start(port, flags.json); + run_dashboard_start(host, port, flags.json); return; } Some("stop") => { @@ -1376,6 +1391,14 @@ fn run_batch(flags: &Flags, bail: bool) { mod tests { use super::*; + #[test] + fn test_format_dashboard_url_uses_host_and_port() { + assert_eq!( + format_dashboard_url("127.0.0.1", 4848), + "http://127.0.0.1:4848" + ); + } + #[test] fn test_parse_proxy_simple() { let result = parse_proxy("http://proxy.com:8080"); diff --git a/cli/src/native/stream.rs b/cli/src/native/stream.rs index cbc6f52c9..ba3b0c791 100644 --- a/cli/src/native/stream.rs +++ b/cli/src/native/stream.rs @@ -1479,8 +1479,8 @@ pub async fn ack_screencast_frame( /// Standalone dashboard HTTP server (no browser, no WebSocket streaming). /// Serves static files and `/api/sessions` for session discovery. -pub async fn run_dashboard_server(port: u16) { - let addr = format!("127.0.0.1:{}", port); +pub async fn run_dashboard_server(host: &str, port: u16) { + let addr = format!("{}:{}", host, port); let listener = match TcpListener::bind(&addr).await { Ok(l) => l, Err(e) => { diff --git a/cli/src/output.rs b/cli/src/output.rs index e357cff66..c38296307 100644 --- a/cli/src/output.rs +++ b/cli/src/output.rs @@ -969,6 +969,14 @@ fn print_warning(resp: &Response) { /// Print command-specific help. Returns true if help was printed, false if command unknown. pub fn print_command_help(command: &str) -> bool { + let Some(help) = command_help(command) else { + return false; + }; + println!("{}", help.trim()); + true +} + +fn command_help(command: &str) -> Option<&'static str> { let help = match command { // === Navigation === "open" | "goto" | "navigate" => { @@ -2403,7 +2411,7 @@ Manage the observability dashboard, a local web UI that shows live browser viewports and command activity feeds for all sessions. Subcommands: - start [--port ] Start the dashboard server (default port: 4848) + start [--port ] [--host ] Start the dashboard server (default port: 4848) stop Stop the dashboard server install Download and install the dashboard to ~/.agent-browser/dashboard/ @@ -2414,6 +2422,7 @@ browser sessions. All sessions automatically stream to the dashboard. Options: --port Port for the dashboard server (default: 4848) + --host Host interface to bind the dashboard server to (default: 127.0.0.1) Global Options: --json Output as JSON @@ -2421,6 +2430,7 @@ Global Options: Examples: agent-browser dashboard install agent-browser dashboard start + agent-browser dashboard start --host 0.0.0.0 agent-browser dashboard start --port 8080 agent-browser dashboard stop "## @@ -2652,10 +2662,9 @@ Examples: "## } - _ => return false, + _ => return None, }; - println!("{}", help.trim()); - true + Some(help) } pub fn print_help() { @@ -3042,6 +3051,13 @@ mod tests { assert_eq!(rendered, "Streaming disabled"); } + #[test] + fn test_dashboard_help_mentions_host_option() { + let help = super::command_help("dashboard").expect("dashboard help should exist"); + assert!(help.contains("--host ")); + assert!(help.contains("default: 127.0.0.1")); + } + #[test] fn test_format_storage_text_for_all_entries() { let data = json!({ diff --git a/packages/dashboard/src/components/viewport.tsx b/packages/dashboard/src/components/viewport.tsx index 9ee31ff76..78702f577 100644 --- a/packages/dashboard/src/components/viewport.tsx +++ b/packages/dashboard/src/components/viewport.tsx @@ -550,7 +550,9 @@ export function Viewport() { {browserConnected && ( - ws://localhost:{streamPort} + {typeof window !== "undefined" + ? `${window.location.protocol === "https:" ? "wss" : "ws"}://${window.location.hostname}:${streamPort}` + : `ws://127.0.0.1:${streamPort}`} )}
diff --git a/packages/dashboard/src/lib/exec.ts b/packages/dashboard/src/lib/exec.ts index abccc4458..22c9ed780 100644 --- a/packages/dashboard/src/lib/exec.ts +++ b/packages/dashboard/src/lib/exec.ts @@ -1,4 +1,17 @@ -const DASHBOARD_PORT = 4848; +export const DASHBOARD_PORT = 4848; + +function getDashboardBaseUrl(): string { + if (typeof window === "undefined") { + return `http://127.0.0.1:${DASHBOARD_PORT}`; + } + + const { protocol, hostname, port } = window.location; + if (!port || port === String(DASHBOARD_PORT)) { + return `${protocol}//${hostname}:${DASHBOARD_PORT}`; + } + + return `${protocol}//${hostname}:${DASHBOARD_PORT}`; +} export interface ExecResult { success: boolean; @@ -9,7 +22,7 @@ export interface ExecResult { export async function execCommand(args: string[]): Promise { try { - const resp = await fetch(`http://localhost:${DASHBOARD_PORT}/api/exec`, { + const resp = await fetch(`${getDashboardBaseUrl()}/api/exec`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ args }), @@ -31,7 +44,7 @@ export function sessionArgs(session: string, ...args: string[]): string[] { export async function killSession(session: string): Promise<{ success: boolean; killed_pid?: number }> { try { - const resp = await fetch(`http://localhost:${DASHBOARD_PORT}/api/kill`, { + const resp = await fetch(`${getDashboardBaseUrl()}/api/kill`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ session }), @@ -41,3 +54,5 @@ export async function killSession(session: string): Promise<{ success: boolean; return { success: false }; } } + +export { getDashboardBaseUrl }; diff --git a/packages/dashboard/src/store/sessions.ts b/packages/dashboard/src/store/sessions.ts index 76f75bc34..da45b3b14 100644 --- a/packages/dashboard/src/store/sessions.ts +++ b/packages/dashboard/src/store/sessions.ts @@ -7,6 +7,7 @@ import type { SessionInfo } from "@/types"; import { execCommand, killSession, sessionArgs } from "@/lib/exec"; import { tabCacheAtom, engineCacheAtom } from "@/store/tabs"; import { streamTabsAtom, streamEngineAtom } from "@/store/stream"; +import { DASHBOARD_PORT, getDashboardBaseUrl } from "@/lib/exec"; function getPort(): number { if (typeof window === "undefined") return 9223; @@ -15,16 +16,21 @@ function getPort(): number { return p ? parseInt(p, 10) || 9223 : 9223; } -const DASHBOARD_PORT = 4848; - function getSessionsUrl(): string { - if (typeof window !== "undefined") { - const origin = window.location.origin; - if (origin.includes(`:${DASHBOARD_PORT}`)) { - return "/api/sessions"; - } + if (typeof window !== "undefined" && window.location.port === String(DASHBOARD_PORT)) { + return "/api/sessions"; + } + + return `${getDashboardBaseUrl()}/api/sessions`; +} + +function getSessionApiBase(port: number): string { + if (typeof window === "undefined") { + return `http://127.0.0.1:${port}`; } - return `http://localhost:${DASHBOARD_PORT}/api/sessions`; + + const { protocol, hostname } = window.location; + return `${protocol}//${hostname}:${port}`; } // --------------------------------------------------------------------------- @@ -223,7 +229,7 @@ export function useSessionsSync(pollInterval = 5000) { for (const s of data) { try { const tabsResp = await fetch( - `http://localhost:${s.port}/api/tabs`, + `${getSessionApiBase(s.port)}/api/tabs`, ).catch(() => null); if (tabsResp?.ok) { const tabs = await tabsResp.json(); @@ -256,3 +262,5 @@ export function useSessionsSync(pollInterval = 5000) { }; }, [fetchSessions, pollInterval]); } + +export { getSessionsUrl, getSessionApiBase };