Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 30 additions & 7 deletions cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
{
Expand All @@ -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) {
Expand All @@ -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;
}
Expand Down Expand Up @@ -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)]
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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::<u16>().ok())
.unwrap_or(4848);
run_dashboard_start(port, flags.json);
run_dashboard_start(host, port, flags.json);
return;
}
Some("stop") => {
Expand Down Expand Up @@ -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");
Expand Down
4 changes: 2 additions & 2 deletions cli/src/native/stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
24 changes: 20 additions & 4 deletions cli/src/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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" => {
Expand Down Expand Up @@ -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 <n>] Start the dashboard server (default port: 4848)
start [--port <n>] [--host <host>] Start the dashboard server (default port: 4848)
stop Stop the dashboard server
install Download and install the dashboard to ~/.agent-browser/dashboard/

Expand All @@ -2414,13 +2422,15 @@ browser sessions. All sessions automatically stream to the dashboard.

Options:
--port <n> Port for the dashboard server (default: 4848)
--host <host> Host interface to bind the dashboard server to (default: 127.0.0.1)

Global Options:
--json Output as JSON

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
"##
Expand Down Expand Up @@ -2652,10 +2662,9 @@ Examples:
"##
}

_ => return false,
_ => return None,
};
println!("{}", help.trim());
true
Some(help)
}

pub fn print_help() {
Expand Down Expand Up @@ -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 <host>"));
assert!(help.contains("default: 127.0.0.1"));
}

#[test]
fn test_format_storage_text_for_all_entries() {
let data = json!({
Expand Down
4 changes: 3 additions & 1 deletion packages/dashboard/src/components/viewport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -550,7 +550,9 @@ export function Viewport() {
</span>
{browserConnected && (
<span className="text-xs text-muted-foreground/60 font-mono">
ws://localhost:{streamPort}
{typeof window !== "undefined"
? `${window.location.protocol === "https:" ? "wss" : "ws"}://${window.location.hostname}:${streamPort}`
: `ws://127.0.0.1:${streamPort}`}
</span>
)}
<div className="ml-auto flex items-center gap-2">
Expand Down
21 changes: 18 additions & 3 deletions packages/dashboard/src/lib/exec.ts
Original file line number Diff line number Diff line change
@@ -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}`;
Comment on lines +8 to +13
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this going to always return ${protocol}//${hostname}:${DASHBOARD_PORT}?

}

export interface ExecResult {
success: boolean;
Expand All @@ -9,7 +22,7 @@ export interface ExecResult {

export async function execCommand(args: string[]): Promise<ExecResult> {
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 }),
Expand All @@ -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 }),
Expand All @@ -41,3 +54,5 @@ export async function killSession(session: string): Promise<{ success: boolean;
return { success: false };
}
}

export { getDashboardBaseUrl };
26 changes: 17 additions & 9 deletions packages/dashboard/src/store/sessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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}`;
}

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -256,3 +262,5 @@ export function useSessionsSync(pollInterval = 5000) {
};
}, [fetchSessions, pollInterval]);
}

export { getSessionsUrl, getSessionApiBase };