Skip to content
Merged
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
20 changes: 18 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,9 @@ agent-browser pdf <path> # Save as PDF
agent-browser snapshot # Accessibility tree with refs (best for AI)
agent-browser eval <js> # Run JavaScript (-b for base64, --stdin for piped input)
agent-browser connect <port> # Connect to browser via CDP
agent-browser stream enable [--port <port>] # Start runtime WebSocket streaming
agent-browser stream status # Show runtime streaming state and bound port
agent-browser stream disable # Stop runtime WebSocket streaming
agent-browser close # Close browser (aliases: quit, exit)
```

Expand Down Expand Up @@ -925,13 +928,26 @@ Stream the browser viewport via WebSocket for live preview or "pair browsing" wh

### Enable Streaming

Set the `AGENT_BROWSER_STREAM_PORT` environment variable:
For an already-running session, enable streaming at runtime:

```bash
agent-browser stream enable
agent-browser stream status
agent-browser stream disable
```

`stream enable` binds an available localhost port automatically unless you pass `--port <port>`.
Use `stream status` to inspect whether streaming is enabled, which port is active, whether a browser is attached, and whether screencasting is active.

If you want streaming to be available immediately when the daemon starts, set `AGENT_BROWSER_STREAM_PORT` before the first command in that session:

```bash
AGENT_BROWSER_STREAM_PORT=9223 agent-browser open example.com
```

This starts a WebSocket server on the specified port that streams the browser viewport and accepts input events.
The environment variable only affects daemon startup. For sessions that are already running, use `agent-browser stream enable` instead.

Once enabled, the WebSocket server streams the browser viewport and accepts input events.

### WebSocket Protocol

Expand Down
96 changes: 96 additions & 0 deletions cli/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -796,6 +796,62 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result<Value, ParseError
}
}

// === Runtime stream control ===
"stream" => match rest.first().copied() {
Some("enable") => {
let mut cmd = json!({ "id": id, "action": "stream_enable" });
let mut i = 1;
while i < rest.len() {
match rest[i] {
"--port" => {
let value =
rest.get(i + 1)
.ok_or_else(|| ParseError::MissingArguments {
context: "stream enable --port".to_string(),
usage: "stream enable [--port <port>]",
})?;
let port =
value.parse::<u32>().map_err(|_| ParseError::InvalidValue {
message: format!(
"Invalid port: '{}' is not a valid integer",
value
),
usage: "stream enable [--port <port>]",
})?;
if port > u16::MAX as u32 {
return Err(ParseError::InvalidValue {
message: format!(
"Invalid port: {} is out of range (valid range: 0-65535)",
port
),
usage: "stream enable [--port <port>]",
});
}
cmd["port"] = json!(port);
i += 2;
}
flag => {
return Err(ParseError::InvalidValue {
message: format!("Unknown flag for stream enable: {}", flag),
usage: "stream enable [--port <port>]",
});
}
}
}
Ok(cmd)
}
Some("disable") => Ok(json!({ "id": id, "action": "stream_disable" })),
Some("status") => Ok(json!({ "id": id, "action": "stream_status" })),
Some(sub) => Err(ParseError::UnknownSubcommand {
subcommand: sub.to_string(),
valid_options: &["enable", "disable", "status"],
}),
None => Err(ParseError::MissingArguments {
context: "stream".to_string(),
usage: "stream <enable|disable|status>",
}),
},

// === Get ===
"get" => parse_get(&rest, &id),

Expand Down Expand Up @@ -3617,6 +3673,46 @@ mod tests {
assert_eq!(cmd["cdpPort"], 1);
}

// === Runtime stream control tests ===

#[test]
fn test_stream_enable_auto_port() {
let cmd = parse_command(&args("stream enable"), &default_flags()).unwrap();
assert_eq!(cmd["action"], "stream_enable");
assert!(cmd.get("port").is_none());
}

#[test]
fn test_stream_enable_with_port() {
let cmd = parse_command(&args("stream enable --port 9223"), &default_flags()).unwrap();
assert_eq!(cmd["action"], "stream_enable");
assert_eq!(cmd["port"], 9223);
}

#[test]
fn test_stream_status() {
let cmd = parse_command(&args("stream status"), &default_flags()).unwrap();
assert_eq!(cmd["action"], "stream_status");
}

#[test]
fn test_stream_disable() {
let cmd = parse_command(&args("stream disable"), &default_flags()).unwrap();
assert_eq!(cmd["action"], "stream_disable");
}

#[test]
fn test_stream_enable_invalid_port() {
let result = parse_command(&args("stream enable --port abc"), &default_flags());
assert!(matches!(result, Err(ParseError::InvalidValue { .. })));
}

#[test]
fn test_stream_missing_subcommand() {
let result = parse_command(&args("stream"), &default_flags());
assert!(matches!(result, Err(ParseError::MissingArguments { .. })));
}

// === Trace Tests ===

#[test]
Expand Down
2 changes: 2 additions & 0 deletions cli/src/connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,8 @@ fn get_pid_path(session: &str) -> PathBuf {
fn cleanup_stale_files(session: &str) {
let pid_path = get_pid_path(session);
let _ = fs::remove_file(&pid_path);
let stream_path = get_socket_dir().join(format!("{}.stream", session));
let _ = fs::remove_file(&stream_path);

#[cfg(unix)]
{
Expand Down
Loading