Consistent Tab IDs & Global Tag Targeting (#892)

Introduces stable per-tab IDs and a global `--tab <id>` flag for scoping individual commands to a specific tab.

Breaking change: response payloads for `tab_list`, `tab_new`, `tab_switch`, `tab_close`, and `window_new` now use `tabId` instead of `index`. `tab_close` returns `{tabId, closed: true}` instead of `{closed, activeIndex}`. `agent-browser tab <unknown>` now errors instead of silently listing tabs.

Follow-up PR to land immediately after this fixes a compile error on the provider direct-page path, clears per-tab daemon state around scoped switches, and implements active-tab restoration so `--tab N` is non-intrusive as intended.
This commit is contained in:
Daniel Hails
2026-04-16 12:02:55 -05:00
committed by GitHub
parent c691b269cb
commit 67dc631977
7 changed files with 618 additions and 45 deletions
+64 -20
View File
@@ -1010,28 +1010,35 @@ fn parse_command_inner(args: &[String], flags: &Flags) -> Result<Value, ParseErr
} }
// === Tabs === // === Tabs ===
"tab" => match rest.first().copied() { "tab" => {
Some("new") => { const VALID: &[&str] = &["list", "new", "close", "<id>"];
let mut cmd = json!({ "id": id, "action": "tab_new" }); match rest.first().copied() {
if let Some(url) = rest.get(1) { Some("new") => {
cmd["url"] = json!(url); let mut cmd = json!({ "id": id, "action": "tab_new" });
if let Some(url) = rest.get(1) {
cmd["url"] = json!(url);
}
Ok(cmd)
} }
Ok(cmd) Some("list") => Ok(json!({ "id": id, "action": "tab_list" })),
} Some("close") => {
Some("list") => Ok(json!({ "id": id, "action": "tab_list" })), let mut cmd = json!({ "id": id, "action": "tab_close" });
Some("close") => { if let Some(tab_id) = rest.get(1).and_then(|s| s.parse::<i32>().ok()) {
let mut cmd = json!({ "id": id, "action": "tab_close" }); cmd["tabId"] = json!(tab_id);
if let Some(index) = rest.get(1).and_then(|s| s.parse::<i32>().ok()) { }
cmd["index"] = json!(index); Ok(cmd)
} }
Ok(cmd) Some(n) if n.parse::<i32>().is_ok() => {
let tab_id = n.parse::<i32>().expect("already checked parse succeeds");
Ok(json!({ "id": id, "action": "tab_switch", "tabId": tab_id }))
}
Some(sub) => Err(ParseError::UnknownSubcommand {
subcommand: sub.to_string(),
valid_options: VALID,
}),
None => Ok(json!({ "id": id, "action": "tab_list" })),
} }
Some(n) if n.parse::<i32>().is_ok() => { }
let index = n.parse::<i32>().expect("already checked parse succeeds");
Ok(json!({ "id": id, "action": "tab_switch", "index": index }))
}
_ => Ok(json!({ "id": id, "action": "tab_list" })),
},
// === Window === // === Window ===
"window" => { "window" => {
@@ -2335,6 +2342,7 @@ mod tests {
fn default_flags() -> Flags { fn default_flags() -> Flags {
Flags { Flags {
session: "test".to_string(), session: "test".to_string(),
tab: None,
json: false, json: false,
headed: false, headed: false,
debug: false, debug: false,
@@ -2847,7 +2855,7 @@ mod tests {
fn test_tab_switch() { fn test_tab_switch() {
let cmd = parse_command(&args("tab 2"), &default_flags()).unwrap(); let cmd = parse_command(&args("tab 2"), &default_flags()).unwrap();
assert_eq!(cmd["action"], "tab_switch"); assert_eq!(cmd["action"], "tab_switch");
assert_eq!(cmd["index"], 2); assert_eq!(cmd["tabId"], 2);
} }
#[test] #[test]
@@ -2856,6 +2864,42 @@ mod tests {
assert_eq!(cmd["action"], "tab_close"); assert_eq!(cmd["action"], "tab_close");
} }
#[test]
fn test_tab_close_with_id() {
let cmd = parse_command(&args("tab close 2"), &default_flags()).unwrap();
assert_eq!(cmd["action"], "tab_close");
assert_eq!(cmd["tabId"], 2);
}
#[test]
fn test_tab_switch_sends_tab_id() {
let cmd = parse_command(&args("tab 2"), &default_flags()).unwrap();
assert_eq!(cmd["tabId"], 2);
assert!(cmd.get("index").is_none());
}
#[test]
fn test_tab_close_sends_tab_id() {
let cmd = parse_command(&args("tab close 3"), &default_flags()).unwrap();
assert_eq!(cmd["tabId"], 3);
assert!(cmd.get("index").is_none());
}
#[test]
fn test_tab_no_args_defaults_to_list() {
let cmd = parse_command(&args("tab"), &default_flags()).unwrap();
assert_eq!(cmd["action"], "tab_list");
}
#[test]
fn test_tab_unknown_subcommand_errors() {
let result = parse_command(&args("tab select 3"), &default_flags());
assert!(
result.is_err(),
"tab select should error, not silently fall through to tab_list"
);
}
// === Network === // === Network ===
#[test] #[test]
+33
View File
@@ -57,6 +57,7 @@ pub struct Config {
pub json: Option<bool>, pub json: Option<bool>,
pub debug: Option<bool>, pub debug: Option<bool>,
pub session: Option<String>, pub session: Option<String>,
pub tab: Option<u32>,
pub session_name: Option<String>, pub session_name: Option<String>,
pub executable_path: Option<String>, pub executable_path: Option<String>,
pub extensions: Option<Vec<String>>, pub extensions: Option<Vec<String>>,
@@ -98,6 +99,7 @@ impl Config {
json: other.json.or(self.json), json: other.json.or(self.json),
debug: other.debug.or(self.debug), debug: other.debug.or(self.debug),
session: other.session.or(self.session), session: other.session.or(self.session),
tab: other.tab.or(self.tab),
session_name: other.session_name.or(self.session_name), session_name: other.session_name.or(self.session_name),
executable_path: other.executable_path.or(self.executable_path), executable_path: other.executable_path.or(self.executable_path),
extensions: match (self.extensions, other.extensions) { extensions: match (self.extensions, other.extensions) {
@@ -196,6 +198,7 @@ fn parse_bool_arg(args: &[String], i: usize) -> (bool, bool) {
fn extract_config_path(args: &[String]) -> Option<Option<String>> { fn extract_config_path(args: &[String]) -> Option<Option<String>> {
const FLAGS_WITH_VALUE: &[&str] = &[ const FLAGS_WITH_VALUE: &[&str] = &[
"--session", "--session",
"--tab",
"--headers", "--headers",
"--executable-path", "--executable-path",
"--cdp", "--cdp",
@@ -273,6 +276,7 @@ pub struct Flags {
pub headed: bool, pub headed: bool,
pub debug: bool, pub debug: bool,
pub session: String, pub session: String,
pub tab: Option<u32>,
pub headers: Option<String>, pub headers: Option<String>,
pub executable_path: Option<String>, pub executable_path: Option<String>,
pub cdp: Option<String>, pub cdp: Option<String>,
@@ -355,6 +359,7 @@ pub fn parse_flags(args: &[String]) -> Flags {
.ok() .ok()
.or(config.session) .or(config.session)
.unwrap_or_else(|| "default".to_string()), .unwrap_or_else(|| "default".to_string()),
tab: config.tab,
headers: config.headers, headers: config.headers,
executable_path: env::var("AGENT_BROWSER_EXECUTABLE_PATH") executable_path: env::var("AGENT_BROWSER_EXECUTABLE_PATH")
.ok() .ok()
@@ -492,6 +497,12 @@ pub fn parse_flags(args: &[String]) -> Flags {
i += 1; i += 1;
} }
} }
"--tab" => {
if let Some(s) = args.get(i + 1) {
flags.tab = s.parse::<u32>().ok();
i += 1;
}
}
"--idle-timeout" => { "--idle-timeout" => {
if let Some(s) = args.get(i + 1) { if let Some(s) = args.get(i + 1) {
match parse_idle_timeout(s) { match parse_idle_timeout(s) {
@@ -775,6 +786,7 @@ pub fn clean_args(args: &[String]) -> Vec<String> {
// Global flags that always take a value (need to skip the next arg too) // Global flags that always take a value (need to skip the next arg too)
const GLOBAL_FLAGS_WITH_VALUE: &[&str] = &[ const GLOBAL_FLAGS_WITH_VALUE: &[&str] = &[
"--session", "--session",
"--tab",
"--headers", "--headers",
"--executable-path", "--executable-path",
"--cdp", "--cdp",
@@ -1441,4 +1453,25 @@ mod tests {
let clean = clean_args(&input); let clean = clean_args(&input);
assert_eq!(clean, vec!["open", "example.com"]); assert_eq!(clean, vec!["open", "example.com"]);
} }
// === Tab flag tests ===
#[test]
fn test_parse_tab_flag() {
let flags = parse_flags(&args("--tab 4 snapshot"));
assert_eq!(flags.tab, Some(4));
}
#[test]
fn test_clean_args_removes_tab_flag() {
let cleaned = clean_args(&args("--tab 4 snapshot"));
assert_eq!(cleaned, vec!["snapshot"]);
}
#[test]
fn test_parse_tab_config() {
let json = r#"{"tab": 4}"#;
let config: Config = serde_json::from_str(json).unwrap();
assert_eq!(config.tab, Some(4));
}
} }
+6
View File
@@ -738,6 +738,12 @@ fn main() {
} }
}; };
if let Some(tab_id) = flags.tab {
if cmd.get("tabId").is_none() {
cmd["tabId"] = json!(tab_id);
}
}
// Handle --password-stdin for auth save // Handle --password-stdin for auth save
if cmd.get("action").and_then(|v| v.as_str()) == Some("auth_save") { if cmd.get("action").and_then(|v| v.as_str()) == Some("auth_save") {
if cmd.get("password").is_some() { if cmd.get("password").is_some() {
+27 -10
View File
@@ -662,7 +662,9 @@ impl DaemonState {
.await; .await;
} }
let tab_id = mgr.assign_tab_id();
mgr.add_page(super::browser::PageInfo { mgr.add_page(super::browser::PageInfo {
tab_id,
target_id: te.target_info.target_id.clone(), target_id: te.target_info.target_id.clone(),
session_id: attach.session_id, session_id: attach.session_id,
url: te.target_info.url.clone(), url: te.target_info.url.clone(),
@@ -1273,6 +1275,20 @@ pub async fn execute_command(cmd: &Value, state: &mut DaemonState) -> Value {
); );
} }
// Pre-dispatch: if tabId is set on a non-tab command, switch to that tab first
if !matches!(
action,
"tab_list" | "tab_new" | "tab_switch" | "tab_close" | "launch" | "close"
) {
if let Some(tab_id) = cmd.get("tabId").and_then(|v| v.as_u64()) {
if let Some(ref mut mgr) = state.browser {
if let Err(e) = mgr.tab_switch_by_id(tab_id as u32).await {
return error_response(&id, &e);
}
}
}
}
let result = match action { let result = match action {
"launch" => handle_launch(cmd, state).await, "launch" => handle_launch(cmd, state).await,
"navigate" => handle_navigate(cmd, state).await, "navigate" => handle_navigate(cmd, state).await,
@@ -3647,14 +3663,14 @@ async fn handle_tab_new(cmd: &Value, state: &mut DaemonState) -> Result<Value, S
async fn handle_tab_switch(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> { async fn handle_tab_switch(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {
let mgr = state.browser.as_mut().ok_or("Browser not launched")?; let mgr = state.browser.as_mut().ok_or("Browser not launched")?;
let index = cmd let tab_id = cmd
.get("index") .get("tabId")
.and_then(|v| v.as_u64()) .and_then(|v| v.as_u64())
.ok_or("Missing 'index' parameter")? as usize; .ok_or("Missing 'tabId' parameter")? as u32;
state.ref_map.clear(); state.ref_map.clear();
state.iframe_sessions.clear(); state.iframe_sessions.clear();
state.active_frame_id = None; state.active_frame_id = None;
let result = mgr.tab_switch(index).await?; let result = mgr.tab_switch_by_id(tab_id).await?;
if let Some(ref server) = state.stream_server { if let Some(ref server) = state.stream_server {
if let Ok(dims) = mgr if let Ok(dims) = mgr
@@ -3679,14 +3695,11 @@ async fn handle_tab_switch(cmd: &Value, state: &mut DaemonState) -> Result<Value
async fn handle_tab_close(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> { async fn handle_tab_close(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {
let mgr = state.browser.as_mut().ok_or("Browser not launched")?; let mgr = state.browser.as_mut().ok_or("Browser not launched")?;
let index = cmd let tab_id = cmd.get("tabId").and_then(|v| v.as_u64()).map(|i| i as u32);
.get("index")
.and_then(|v| v.as_u64())
.map(|i| i as usize);
state.ref_map.clear(); state.ref_map.clear();
state.iframe_sessions.clear(); state.iframe_sessions.clear();
state.active_frame_id = None; state.active_frame_id = None;
mgr.tab_close(index).await mgr.tab_close_by_id(tab_id).await
} }
async fn handle_viewport(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> { async fn handle_viewport(cmd: &Value, state: &mut DaemonState) -> Result<Value, String> {
@@ -4065,7 +4078,9 @@ async fn handle_recording_start(cmd: &Value, state: &mut DaemonState) -> Result<
} }
// Add page and switch to it // Add page and switch to it
let tab_id = mgr.assign_tab_id();
mgr.add_page(super::browser::PageInfo { mgr.add_page(super::browser::PageInfo {
tab_id,
target_id: create_result.target_id, target_id: create_result.target_id,
session_id: new_session_id.clone(), session_id: new_session_id.clone(),
url: nav_url.clone(), url: nav_url.clone(),
@@ -5976,7 +5991,9 @@ async fn handle_window_new(cmd: &Value, state: &mut DaemonState) -> Result<Value
) )
.await?; .await?;
let tab_id = mgr.assign_tab_id();
mgr.add_page(super::browser::PageInfo { mgr.add_page(super::browser::PageInfo {
tab_id,
target_id: create_result.target_id, target_id: create_result.target_id,
session_id: attach.session_id, session_id: attach.session_id,
url: "about:blank".to_string(), url: "about:blank".to_string(),
@@ -6004,7 +6021,7 @@ async fn handle_window_new(cmd: &Value, state: &mut DaemonState) -> Result<Value
let total = mgr.page_count(); let total = mgr.page_count();
state.ref_map.clear(); state.ref_map.clear();
Ok(json!({ "index": total - 1, "total": total })) Ok(json!({ "tabId": tab_id, "total": total }))
} }
async fn handle_diff_screenshot(cmd: &Value, state: &DaemonState) -> Result<Value, String> { async fn handle_diff_screenshot(cmd: &Value, state: &DaemonState) -> Result<Value, String> {
+51 -4
View File
@@ -158,6 +158,7 @@ pub fn to_ai_friendly_error(error: &str) -> String {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct PageInfo { pub struct PageInfo {
pub tab_id: u32,
pub target_id: String, pub target_id: String,
pub session_id: String, pub session_id: String,
pub url: String, pub url: String,
@@ -226,6 +227,7 @@ pub struct BrowserManager {
pub ignore_https_errors: bool, pub ignore_https_errors: bool,
/// Origins visited during this session, used by save_state to collect cross-origin localStorage. /// Origins visited during this session, used by save_state to collect cross-origin localStorage.
visited_origins: HashSet<String>, visited_origins: HashSet<String>,
next_tab_id: u32,
} }
const LIGHTPANDA_CDP_CONNECT_TIMEOUT: Duration = Duration::from_secs(5); const LIGHTPANDA_CDP_CONNECT_TIMEOUT: Duration = Duration::from_secs(5);
@@ -297,6 +299,7 @@ impl BrowserManager {
download_path: download_path.clone(), download_path: download_path.clone(),
ignore_https_errors, ignore_https_errors,
visited_origins: HashSet::new(), visited_origins: HashSet::new(),
next_tab_id: 1,
}; };
manager.discover_and_attach_targets().await?; manager.discover_and_attach_targets().await?;
manager manager
@@ -385,6 +388,7 @@ impl BrowserManager {
download_path: None, download_path: None,
ignore_https_errors: false, ignore_https_errors: false,
visited_origins: HashSet::new(), visited_origins: HashSet::new(),
next_tab_id: 1,
}; };
if direct_page { if direct_page {
@@ -453,7 +457,10 @@ impl BrowserManager {
) )
.await?; .await?;
let tab_id = self.next_tab_id;
self.next_tab_id += 1;
self.pages.push(PageInfo { self.pages.push(PageInfo {
tab_id,
target_id: result.target_id, target_id: result.target_id,
session_id: attach_result.session_id.clone(), session_id: attach_result.session_id.clone(),
url: "about:blank".to_string(), url: "about:blank".to_string(),
@@ -476,7 +483,10 @@ impl BrowserManager {
) )
.await?; .await?;
let tab_id = self.next_tab_id;
self.next_tab_id += 1;
self.pages.push(PageInfo { self.pages.push(PageInfo {
tab_id,
target_id: target.target_id.clone(), target_id: target.target_id.clone(),
session_id: attach_result.session_id.clone(), session_id: attach_result.session_id.clone(),
url: target.url.clone(), url: target.url.clone(),
@@ -821,7 +831,10 @@ impl BrowserManager {
) )
.await?; .await?;
let tab_id = self.next_tab_id;
self.next_tab_id += 1;
self.pages.push(PageInfo { self.pages.push(PageInfo {
tab_id,
target_id: result.target_id, target_id: result.target_id,
session_id: attach_result.session_id.clone(), session_id: attach_result.session_id.clone(),
url: "about:blank".to_string(), url: "about:blank".to_string(),
@@ -864,7 +877,7 @@ impl BrowserManager {
.enumerate() .enumerate()
.map(|(i, p)| { .map(|(i, p)| {
json!({ json!({
"index": i, "tabId": p.tab_id,
"title": p.title, "title": p.title,
"url": p.url, "url": p.url,
"type": p.target_type, "type": p.target_type,
@@ -902,8 +915,11 @@ impl BrowserManager {
self.enable_domains(&attach.session_id).await?; self.enable_domains(&attach.session_id).await?;
let tab_id = self.next_tab_id;
self.next_tab_id += 1;
let index = self.pages.len(); let index = self.pages.len();
self.pages.push(PageInfo { self.pages.push(PageInfo {
tab_id,
target_id: result.target_id, target_id: result.target_id,
session_id: attach.session_id, session_id: attach.session_id,
url: target_url.to_string(), url: target_url.to_string(),
@@ -912,7 +928,7 @@ impl BrowserManager {
}); });
self.active_page_index = index; self.active_page_index = index;
Ok(json!({ "index": index, "url": target_url })) Ok(json!({ "tabId": tab_id, "url": target_url, "total": self.pages.len() }))
} }
pub async fn tab_switch(&mut self, index: usize) -> Result<Value, String> { pub async fn tab_switch(&mut self, index: usize) -> Result<Value, String> {
@@ -942,7 +958,8 @@ impl BrowserManager {
page.title = title.clone(); page.title = title.clone();
} }
Ok(json!({ "index": index, "url": url, "title": title })) let tab_id = self.pages[index].tab_id;
Ok(json!({ "tabId": tab_id, "url": url, "title": title }))
} }
pub async fn tab_close(&mut self, index: Option<usize>) -> Result<Value, String> { pub async fn tab_close(&mut self, index: Option<usize>) -> Result<Value, String> {
@@ -958,6 +975,7 @@ impl BrowserManager {
let page = self.pages.remove(target_index); let page = self.pages.remove(target_index);
self.update_active_page_after_removal(target_index); self.update_active_page_after_removal(target_index);
let closed_tab_id = page.tab_id;
let _ = self let _ = self
.client .client
.send_command_typed::<_, Value>( .send_command_typed::<_, Value>(
@@ -972,7 +990,7 @@ impl BrowserManager {
let session_id = self.pages[self.active_page_index].session_id.clone(); let session_id = self.pages[self.active_page_index].session_id.clone();
self.enable_domains(&session_id).await?; self.enable_domains(&session_id).await?;
Ok(json!({ "closed": target_index, "activeIndex": self.active_page_index })) Ok(json!({ "tabId": closed_tab_id, "closed": true }))
} }
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
@@ -1213,6 +1231,34 @@ impl BrowserManager {
.to_string()) .to_string())
} }
pub async fn tab_switch_by_id(&mut self, tab_id: u32) -> Result<Value, String> {
let index = self
.pages
.iter()
.position(|p| p.tab_id == tab_id)
.ok_or_else(|| format!("Tab ID {} not found", tab_id))?;
self.tab_switch(index).await
}
pub async fn tab_close_by_id(&mut self, tab_id: Option<u32>) -> Result<Value, String> {
let index = match tab_id {
Some(id) => Some(
self.pages
.iter()
.position(|p| p.tab_id == id)
.ok_or_else(|| format!("Tab ID {} not found", id))?,
),
None => None,
};
self.tab_close(index).await
}
pub fn assign_tab_id(&mut self) -> u32 {
let id = self.next_tab_id;
self.next_tab_id += 1;
id
}
pub fn add_page(&mut self, page: PageInfo) { pub fn add_page(&mut self, page: PageInfo) {
let index = self.pages.len(); let index = self.pages.len();
self.pages.push(page); self.pages.push(page);
@@ -1393,6 +1439,7 @@ async fn initialize_lightpanda_manager(
download_path: None, download_path: None,
ignore_https_errors: false, ignore_https_errors: false,
visited_origins: HashSet::new(), visited_origins: HashSet::new(),
next_tab_id: 1,
}; };
match discover_and_attach_lightpanda_targets(&mut manager, deadline).await { match discover_and_attach_lightpanda_targets(&mut manager, deadline).await {
+332 -5
View File
@@ -905,12 +905,13 @@ async fn e2e_tabs() {
.await; .await;
assert_success(&resp); assert_success(&resp);
// Tab list should show 1 tab // Tab list should show 1 tab with tabId 1
let resp = execute_command(&json!({ "id": "3", "action": "tab_list" }), &mut state).await; let resp = execute_command(&json!({ "id": "3", "action": "tab_list" }), &mut state).await;
assert_success(&resp); assert_success(&resp);
let tabs = get_data(&resp)["tabs"].as_array().unwrap(); let tabs = get_data(&resp)["tabs"].as_array().unwrap();
assert_eq!(tabs.len(), 1); assert_eq!(tabs.len(), 1);
assert_eq!(tabs[0]["active"], true); assert_eq!(tabs[0]["active"], true);
assert_eq!(tabs[0]["tabId"], 1, "First tab should have tabId 1");
// Open new tab // Open new tab
let resp = execute_command( let resp = execute_command(
@@ -919,18 +920,21 @@ async fn e2e_tabs() {
) )
.await; .await;
assert_success(&resp); assert_success(&resp);
assert_eq!(get_data(&resp)["index"], 1); assert_eq!(get_data(&resp)["tabId"], 2, "New tab should have tabId 2");
assert_eq!(get_data(&resp)["total"], 2);
// Tab list should show 2 tabs // Tab list should show 2 tabs with distinct, incrementing tabIds
let resp = execute_command(&json!({ "id": "5", "action": "tab_list" }), &mut state).await; let resp = execute_command(&json!({ "id": "5", "action": "tab_list" }), &mut state).await;
assert_success(&resp); assert_success(&resp);
let tabs = get_data(&resp)["tabs"].as_array().unwrap(); let tabs = get_data(&resp)["tabs"].as_array().unwrap();
assert_eq!(tabs.len(), 2); assert_eq!(tabs.len(), 2);
assert_eq!(tabs[1]["active"], true); assert_eq!(tabs[1]["active"], true);
assert_eq!(tabs[0]["tabId"], 1, "First tab should keep tabId 1");
assert_eq!(tabs[1]["tabId"], 2, "Second tab should have tabId 2");
// Switch to first tab // Switch to first tab
let resp = execute_command( let resp = execute_command(
&json!({ "id": "6", "action": "tab_switch", "index": 0 }), &json!({ "id": "6", "action": "tab_switch", "tabId": 1 }),
&mut state, &mut state,
) )
.await; .await;
@@ -946,7 +950,7 @@ async fn e2e_tabs() {
// Close second tab // Close second tab
let resp = execute_command( let resp = execute_command(
&json!({ "id": "8", "action": "tab_close", "index": 1 }), &json!({ "id": "8", "action": "tab_close", "tabId": 2 }),
&mut state, &mut state,
) )
.await; .await;
@@ -962,6 +966,329 @@ async fn e2e_tabs() {
assert_success(&resp); assert_success(&resp);
} }
#[tokio::test]
#[ignore]
async fn e2e_tab_ids_not_reused() {
let mut state = DaemonState::new();
let resp = execute_command(
&json!({ "id": "1", "action": "launch", "headless": true }),
&mut state,
)
.await;
assert_success(&resp);
// First tab gets tabId 1
let resp = execute_command(&json!({ "id": "2", "action": "tab_list" }), &mut state).await;
assert_success(&resp);
let tabs = get_data(&resp)["tabs"].as_array().unwrap();
assert_eq!(tabs[0]["tabId"], 1);
// Open tab 2 and tab 3
let resp = execute_command(
&json!({ "id": "3", "action": "tab_new", "url": "data:text/html,<h1>Tab 2</h1>" }),
&mut state,
)
.await;
assert_success(&resp);
assert_eq!(get_data(&resp)["tabId"], 2);
let resp = execute_command(
&json!({ "id": "4", "action": "tab_new", "url": "data:text/html,<h1>Tab 3</h1>" }),
&mut state,
)
.await;
assert_success(&resp);
assert_eq!(get_data(&resp)["tabId"], 3);
// Close tab 2
let resp = execute_command(
&json!({ "id": "5", "action": "tab_close", "tabId": 2 }),
&mut state,
)
.await;
assert_success(&resp);
// Open a new tab — should get tabId 4, NOT 2
let resp = execute_command(
&json!({ "id": "6", "action": "tab_new", "url": "data:text/html,<h1>Tab 4</h1>" }),
&mut state,
)
.await;
assert_success(&resp);
assert_eq!(
get_data(&resp)["tabId"],
4,
"Tab IDs must not be reused after closing"
);
// Verify final state: tabs 1, 3, 4
let resp = execute_command(&json!({ "id": "7", "action": "tab_list" }), &mut state).await;
assert_success(&resp);
let tabs = get_data(&resp)["tabs"].as_array().unwrap();
assert_eq!(tabs.len(), 3);
let ids: Vec<i64> = tabs.iter().map(|t| t["tabId"].as_i64().unwrap()).collect();
assert_eq!(ids, vec![1, 3, 4]);
let resp = execute_command(&json!({ "id": "99", "action": "close" }), &mut state).await;
assert_success(&resp);
}
#[tokio::test]
#[ignore]
async fn e2e_tab_global_targeting() {
let mut state = DaemonState::new();
let resp = execute_command(
&json!({ "id": "1", "action": "launch", "headless": true }),
&mut state,
)
.await;
assert_success(&resp);
// Navigate tab 1
let resp = execute_command(
&json!({ "id": "2", "action": "navigate", "url": "data:text/html,<h1>Page A</h1>" }),
&mut state,
)
.await;
assert_success(&resp);
// Open tab 2 (becomes active)
let resp = execute_command(
&json!({ "id": "3", "action": "tab_new", "url": "data:text/html,<h1>Page B</h1>" }),
&mut state,
)
.await;
assert_success(&resp);
assert_eq!(get_data(&resp)["tabId"], 2);
// Use tabId to evaluate on tab 1 while tab 2 is active
// (simulates --tab 1 evaluate ...)
let resp = execute_command(
&json!({ "id": "4", "action": "evaluate", "tabId": 1, "script": "document.querySelector('h1').textContent" }),
&mut state,
)
.await;
assert_success(&resp);
assert_eq!(
get_data(&resp)["result"],
"Page A",
"tabId should target tab 1 even though tab 2 was active"
);
// Verify tab 2 content is still accessible
let resp = execute_command(
&json!({ "id": "5", "action": "evaluate", "tabId": 2, "script": "document.querySelector('h1').textContent" }),
&mut state,
)
.await;
assert_success(&resp);
assert_eq!(get_data(&resp)["result"], "Page B");
// Without tabId, should use the current active tab (now tab 1 from the switch)
let resp = execute_command(
&json!({ "id": "6", "action": "evaluate", "script": "document.querySelector('h1').textContent" }),
&mut state,
)
.await;
assert_success(&resp);
// After targeting tab 1 then tab 2, active tab is now tab 2
assert_eq!(get_data(&resp)["result"], "Page B");
let resp = execute_command(&json!({ "id": "99", "action": "close" }), &mut state).await;
assert_success(&resp);
}
#[tokio::test]
#[ignore]
async fn e2e_tab_global_targeting_snapshot() {
let mut state = DaemonState::new();
let resp = execute_command(
&json!({ "id": "1", "action": "launch", "headless": true }),
&mut state,
)
.await;
assert_success(&resp);
// Navigate tab 1
let resp = execute_command(
&json!({ "id": "2", "action": "navigate", "url": "data:text/html,<h1>Page A</h1>" }),
&mut state,
)
.await;
assert_success(&resp);
// Open tab 2 (becomes active)
let resp = execute_command(
&json!({ "id": "3", "action": "tab_new", "url": "data:text/html,<h1>Page B</h1>" }),
&mut state,
)
.await;
assert_success(&resp);
assert_eq!(get_data(&resp)["tabId"], 2);
// Snapshot tab 1 via tabId while tab 2 is active
let resp = execute_command(
&json!({ "id": "4", "action": "snapshot", "tabId": 1 }),
&mut state,
)
.await;
assert_success(&resp);
let snapshot = get_data(&resp)["snapshot"].as_str().unwrap();
assert!(
snapshot.contains("Page A"),
"Snapshot with tabId=1 should contain 'Page A', got: {}",
snapshot
);
assert!(
!snapshot.contains("Page B"),
"Snapshot with tabId=1 should NOT contain 'Page B', got: {}",
snapshot
);
// Snapshot tab 2 via tabId
let resp = execute_command(
&json!({ "id": "5", "action": "snapshot", "tabId": 2 }),
&mut state,
)
.await;
assert_success(&resp);
let snapshot = get_data(&resp)["snapshot"].as_str().unwrap();
assert!(
snapshot.contains("Page B"),
"Snapshot with tabId=2 should contain 'Page B', got: {}",
snapshot
);
assert!(
!snapshot.contains("Page A"),
"Snapshot with tabId=2 should NOT contain 'Page A', got: {}",
snapshot
);
// Snapshot without tabId should use the last-switched tab (tab 2)
let resp = execute_command(&json!({ "id": "6", "action": "snapshot" }), &mut state).await;
assert_success(&resp);
let snapshot = get_data(&resp)["snapshot"].as_str().unwrap();
assert!(
snapshot.contains("Page B"),
"Snapshot without tabId should use active tab (Page B), got: {}",
snapshot
);
let resp = execute_command(&json!({ "id": "99", "action": "close" }), &mut state).await;
assert_success(&resp);
}
#[tokio::test]
#[ignore]
async fn e2e_tab_global_targeting_snapshot_non_contiguous() {
// Reproduces the bug where --tab 3 snapshot shows tab 1's content
// when tab IDs are non-contiguous (e.g. tabs [1] and [3] after
// closing tab [2]).
let mut state = DaemonState::new();
let resp = execute_command(
&json!({ "id": "1", "action": "launch", "headless": true }),
&mut state,
)
.await;
assert_success(&resp);
// Navigate tab 1 to Page A
let resp = execute_command(
&json!({ "id": "2", "action": "navigate", "url": "data:text/html,<h1>Page A</h1>" }),
&mut state,
)
.await;
assert_success(&resp);
// Open tab 2
let resp = execute_command(
&json!({ "id": "3", "action": "tab_new", "url": "data:text/html,<h1>Page B</h1>" }),
&mut state,
)
.await;
assert_success(&resp);
assert_eq!(get_data(&resp)["tabId"], 2);
// Open tab 3
let resp = execute_command(
&json!({ "id": "4", "action": "tab_new", "url": "data:text/html,<h1>Page C</h1>" }),
&mut state,
)
.await;
assert_success(&resp);
assert_eq!(get_data(&resp)["tabId"], 3);
// Close tab 2 to create non-contiguous IDs: [1, 3]
let resp = execute_command(
&json!({ "id": "5", "action": "tab_close", "tabId": 2 }),
&mut state,
)
.await;
assert_success(&resp);
// Verify tab list shows [1] and [3]
let resp = execute_command(&json!({ "id": "6", "action": "tab_list" }), &mut state).await;
assert_success(&resp);
let tabs = get_data(&resp)["tabs"].as_array().unwrap();
assert_eq!(tabs.len(), 2);
assert_eq!(tabs[0]["tabId"], 1);
assert_eq!(tabs[1]["tabId"], 3);
// Switch active tab back to tab 1
let resp = execute_command(
&json!({ "id": "7", "action": "tab_switch", "tabId": 1 }),
&mut state,
)
.await;
assert_success(&resp);
// Snapshot tab 3 via tabId while tab 1 is active
// (simulates: --tab 3 snapshot)
let resp = execute_command(
&json!({ "id": "8", "action": "snapshot", "tabId": 3 }),
&mut state,
)
.await;
assert_success(&resp);
let snapshot = get_data(&resp)["snapshot"].as_str().unwrap();
assert!(
snapshot.contains("Page C"),
"Snapshot with tabId=3 should contain 'Page C', got: {}",
snapshot
);
assert!(
!snapshot.contains("Page A"),
"Snapshot with tabId=3 should NOT contain 'Page A', got: {}",
snapshot
);
// Snapshot tab 1 via tabId
let resp = execute_command(
&json!({ "id": "9", "action": "snapshot", "tabId": 1 }),
&mut state,
)
.await;
assert_success(&resp);
let snapshot = get_data(&resp)["snapshot"].as_str().unwrap();
assert!(
snapshot.contains("Page A"),
"Snapshot with tabId=1 should contain 'Page A', got: {}",
snapshot
);
assert!(
!snapshot.contains("Page C"),
"Snapshot with tabId=1 should NOT contain 'Page C', got: {}",
snapshot
);
let resp = execute_command(&json!({ "id": "99", "action": "close" }), &mut state).await;
assert_success(&resp);
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Element queries: isvisible, isenabled, gettext, getattribute // Element queries: isvisible, isenabled, gettext, getattribute
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
+105 -6
View File
@@ -406,7 +406,11 @@ pub fn print_response_with_opts(resp: &Response, action: Option<&str>, opts: &Ou
} }
// Tabs // Tabs
if let Some(tabs) = data.get("tabs").and_then(|v| v.as_array()) { if let Some(tabs) = data.get("tabs").and_then(|v| v.as_array()) {
for (i, tab) in tabs.iter().enumerate() { for tab in tabs {
let tab_id = tab
.get("tabId")
.and_then(|v| v.as_i64())
.unwrap_or_default();
let title = tab let title = tab
.get("title") .get("title")
.and_then(|v| v.as_str()) .and_then(|v| v.as_str())
@@ -418,10 +422,47 @@ pub fn print_response_with_opts(resp: &Response, action: Option<&str>, opts: &Ou
} else { } else {
" ".to_string() " ".to_string()
}; };
println!("{} [{}] {} - {}", marker, i, title, url); println!("{} [{}] {} - {}", marker, tab_id, title, url);
} }
return; return;
} }
// Tab switch
if action == Some("tab_switch") {
if let Some(tab_id) = data.get("tabId").and_then(|v| v.as_i64()) {
if let Some(url) = data.get("url").and_then(|v| v.as_str()) {
println!(
"{} Switched to tab [{}] ({})",
color::success_indicator(),
tab_id,
url
);
} else {
println!(
"{} Switched to tab [{}]",
color::success_indicator(),
tab_id
);
}
return;
}
}
// New tab/window
if let Some(tab_id) = data.get("tabId").and_then(|v| v.as_i64()) {
if let Some(total) = data.get("total").and_then(|v| v.as_i64()) {
let label = match action {
Some("window_new") => "Window opened",
_ => "Tab opened",
};
println!(
"{} {} [{}] ({} total)",
color::success_indicator(),
label,
tab_id,
total
);
return;
}
}
// Console logs // Console logs
if let Some(logs) = data.get("messages").and_then(|v| v.as_array()) { if let Some(logs) = data.get("messages").and_then(|v| v.as_array()) {
if opts.content_boundaries { if opts.content_boundaries {
@@ -562,7 +603,13 @@ pub fn print_response_with_opts(resp: &Response, action: Option<&str>, opts: &Ou
// Closed (browser or tab) // Closed (browser or tab)
if data.get("closed").is_some() { if data.get("closed").is_some() {
let label = match action { let label = match action {
Some("tab_close") => "Tab closed", Some("tab_close") => {
if let Some(closed_id) = data.get("tabId").and_then(|v| v.as_i64()) {
println!("{} Tab [{}] closed", color::success_indicator(), closed_id);
return;
}
"Tab closed"
}
_ => "Browser closed", _ => "Browser closed",
}; };
println!("{} {}", color::success_indicator(), label); println!("{} {}", color::success_indicator(), label);
@@ -1010,6 +1057,7 @@ Aliases: goto, navigate
Global Options: Global Options:
--json Output as JSON --json Output as JSON
--session <name> Use specific session --session <name> Use specific session
--tab <id> Target specific tab ID
--headers <json> Set HTTP headers (scoped to this origin) --headers <json> Set HTTP headers (scoped to this origin)
--headed Show browser window --headed Show browser window
@@ -1033,6 +1081,7 @@ the browser's back button.
Global Options: Global Options:
--json Output as JSON --json Output as JSON
--session <name> Use specific session --session <name> Use specific session
--tab <id> Target specific tab ID
Examples: Examples:
agent-browser back agent-browser back
@@ -1050,6 +1099,7 @@ the browser's forward button.
Global Options: Global Options:
--json Output as JSON --json Output as JSON
--session <name> Use specific session --session <name> Use specific session
--tab <id> Target specific tab ID
Examples: Examples:
agent-browser forward agent-browser forward
@@ -1067,6 +1117,7 @@ the browser's reload button.
Global Options: Global Options:
--json Output as JSON --json Output as JSON
--session <name> Use specific session --session <name> Use specific session
--tab <id> Target specific tab ID
Examples: Examples:
agent-browser reload agent-browser reload
@@ -1090,6 +1141,7 @@ Options:
Global Options: Global Options:
--json Output as JSON --json Output as JSON
--session <name> Use specific session --session <name> Use specific session
--tab <id> Target specific tab ID
Examples: Examples:
agent-browser click "#submit-button" agent-browser click "#submit-button"
@@ -1111,6 +1163,7 @@ or triggering double-click handlers.
Global Options: Global Options:
--json Output as JSON --json Output as JSON
--session <name> Use specific session --session <name> Use specific session
--tab <id> Target specific tab ID
Examples: Examples:
agent-browser dblclick "#editable-text" agent-browser dblclick "#editable-text"
@@ -1129,6 +1182,7 @@ This replaces any existing content in the field.
Global Options: Global Options:
--json Output as JSON --json Output as JSON
--session <name> Use specific session --session <name> Use specific session
--tab <id> Target specific tab ID
Examples: Examples:
agent-browser fill "#email" "user@example.com" agent-browser fill "#email" "user@example.com"
@@ -1148,6 +1202,7 @@ Unlike fill, this does not clear existing content first.
Global Options: Global Options:
--json Output as JSON --json Output as JSON
--session <name> Use specific session --session <name> Use specific session
--tab <id> Target specific tab ID
Examples: Examples:
agent-browser type "#search" "hello" agent-browser type "#search" "hello"
@@ -1171,6 +1226,7 @@ triggering hover states or dropdown menus.
Global Options: Global Options:
--json Output as JSON --json Output as JSON
--session <name> Use specific session --session <name> Use specific session
--tab <id> Target specific tab ID
Examples: Examples:
agent-browser hover "#dropdown-trigger" agent-browser hover "#dropdown-trigger"
@@ -1188,6 +1244,7 @@ Sets keyboard focus to the specified element.
Global Options: Global Options:
--json Output as JSON --json Output as JSON
--session <name> Use specific session --session <name> Use specific session
--tab <id> Target specific tab ID
Examples: Examples:
agent-browser focus "#input-field" agent-browser focus "#input-field"
@@ -1205,6 +1262,7 @@ Checks a checkbox element. If already checked, no action is taken.
Global Options: Global Options:
--json Output as JSON --json Output as JSON
--session <name> Use specific session --session <name> Use specific session
--tab <id> Target specific tab ID
Examples: Examples:
agent-browser check "#terms-checkbox" agent-browser check "#terms-checkbox"
@@ -1222,6 +1280,7 @@ Unchecks a checkbox element. If already unchecked, no action is taken.
Global Options: Global Options:
--json Output as JSON --json Output as JSON
--session <name> Use specific session --session <name> Use specific session
--tab <id> Target specific tab ID
Examples: Examples:
agent-browser uncheck "#newsletter-opt-in" agent-browser uncheck "#newsletter-opt-in"
@@ -1239,6 +1298,7 @@ Selects one or more options in a <select> dropdown by value.
Global Options: Global Options:
--json Output as JSON --json Output as JSON
--session <name> Use specific session --session <name> Use specific session
--tab <id> Target specific tab ID
Examples: Examples:
agent-browser select "#country" "US" agent-browser select "#country" "US"
@@ -1257,6 +1317,7 @@ Drags an element from source to target location.
Global Options: Global Options:
--json Output as JSON --json Output as JSON
--session <name> Use specific session --session <name> Use specific session
--tab <id> Target specific tab ID
Examples: Examples:
agent-browser drag "#draggable" "#drop-zone" agent-browser drag "#draggable" "#drop-zone"
@@ -1274,6 +1335,7 @@ Uploads one or more files to a file input element.
Global Options: Global Options:
--json Output as JSON --json Output as JSON
--session <name> Use specific session --session <name> Use specific session
--tab <id> Target specific tab ID
Examples: Examples:
agent-browser upload "#file-input" ./document.pdf agent-browser upload "#file-input" ./document.pdf
@@ -1295,6 +1357,7 @@ Arguments:
Global Options: Global Options:
--json Output as JSON --json Output as JSON
--session <name> Use specific session --session <name> Use specific session
--tab <id> Target specific tab ID
Examples: Examples:
agent-browser download "#download-btn" ./file.pdf agent-browser download "#download-btn" ./file.pdf
@@ -1326,6 +1389,7 @@ Modifiers (combine with +):
Global Options: Global Options:
--json Output as JSON --json Output as JSON
--session <name> Use specific session --session <name> Use specific session
--tab <id> Target specific tab ID
Examples: Examples:
agent-browser press Enter agent-browser press Enter
@@ -1347,6 +1411,7 @@ Useful for holding modifier keys.
Global Options: Global Options:
--json Output as JSON --json Output as JSON
--session <name> Use specific session --session <name> Use specific session
--tab <id> Target specific tab ID
Examples: Examples:
agent-browser keydown Shift agent-browser keydown Shift
@@ -1364,6 +1429,7 @@ Releases a key that was pressed with keydown.
Global Options: Global Options:
--json Output as JSON --json Output as JSON
--session <name> Use specific session --session <name> Use specific session
--tab <id> Target specific tab ID
Examples: Examples:
agent-browser keyup Shift agent-browser keyup Shift
@@ -1392,6 +1458,7 @@ directly — it already operates on the current focus.
Global Options: Global Options:
--json Output as JSON --json Output as JSON
--session <name> Use specific session --session <name> Use specific session
--tab <id> Target specific tab ID
Examples: Examples:
agent-browser keyboard type "Hello, World!" agent-browser keyboard type "Hello, World!"
@@ -1426,6 +1493,7 @@ Options:
Global Options: Global Options:
--json Output as JSON --json Output as JSON
--session <name> Use specific session --session <name> Use specific session
--tab <id> Target specific tab ID
Examples: Examples:
agent-browser scroll agent-browser scroll
@@ -1448,6 +1516,7 @@ Aliases: scrollinto
Global Options: Global Options:
--json Output as JSON --json Output as JSON
--session <name> Use specific session --session <name> Use specific session
--tab <id> Target specific tab ID
Examples: Examples:
agent-browser scrollintoview "#footer" agent-browser scrollintoview "#footer"
@@ -1485,6 +1554,7 @@ Wait for text to disappear:
Global Options: Global Options:
--json Output as JSON --json Output as JSON
--session <name> Use specific session --session <name> Use specific session
--tab <id> Target specific tab ID
Examples: Examples:
agent-browser wait "#loading-spinner" agent-browser wait "#loading-spinner"
@@ -1526,6 +1596,7 @@ Options:
Global Options: Global Options:
--json Output as JSON --json Output as JSON
--session <name> Use specific session --session <name> Use specific session
--tab <id> Target specific tab ID
Examples: Examples:
agent-browser screenshot agent-browser screenshot
@@ -1549,6 +1620,7 @@ Saves the current page as a PDF file.
Global Options: Global Options:
--json Output as JSON --json Output as JSON
--session <name> Use specific session --session <name> Use specific session
--tab <id> Target specific tab ID
Examples: Examples:
agent-browser pdf ./page.pdf agent-browser pdf ./page.pdf
@@ -1577,6 +1649,7 @@ Options:
Global Options: Global Options:
--json Output as JSON --json Output as JSON
--session <name> Use specific session --session <name> Use specific session
--tab <id> Target specific tab ID
Examples: Examples:
agent-browser snapshot agent-browser snapshot
@@ -1603,6 +1676,7 @@ Options:
Global Options: Global Options:
--json Output as JSON --json Output as JSON
--session <name> Use specific session --session <name> Use specific session
--tab <id> Target specific tab ID
Examples: Examples:
agent-browser eval "document.title" agent-browser eval "document.title"
@@ -1635,6 +1709,7 @@ Options:
Global Options: Global Options:
--json Output as JSON --json Output as JSON
--session <name> Use specific session --session <name> Use specific session
--tab <id> Target specific tab ID
Examples: Examples:
agent-browser close agent-browser close
@@ -1686,6 +1761,7 @@ Subcommands:
Global Options: Global Options:
--json Output as JSON --json Output as JSON
--session <name> Use specific session --session <name> Use specific session
--tab <id> Target specific tab ID
Examples: Examples:
agent-browser get text @e1 agent-browser get text @e1
@@ -1718,6 +1794,7 @@ Subcommands:
Global Options: Global Options:
--json Output as JSON --json Output as JSON
--session <name> Use specific session --session <name> Use specific session
--tab <id> Target specific tab ID
Examples: Examples:
agent-browser is visible "#modal" agent-browser is visible "#modal"
@@ -1757,6 +1834,7 @@ Options:
Global Options: Global Options:
--json Output as JSON --json Output as JSON
--session <name> Use specific session --session <name> Use specific session
--tab <id> Target specific tab ID
Examples: Examples:
agent-browser find role button click --name Submit agent-browser find role button click --name Submit
@@ -1787,6 +1865,7 @@ Subcommands:
Global Options: Global Options:
--json Output as JSON --json Output as JSON
--session <name> Use specific session --session <name> Use specific session
--tab <id> Target specific tab ID
Examples: Examples:
agent-browser mouse move 100 200 agent-browser mouse move 100 200
@@ -1820,6 +1899,7 @@ Settings:
Global Options: Global Options:
--json Output as JSON --json Output as JSON
--session <name> Use specific session --session <name> Use specific session
--tab <id> Target specific tab ID
Examples: Examples:
agent-browser set viewport 1920 1080 agent-browser set viewport 1920 1080
@@ -1860,6 +1940,7 @@ Subcommands:
Global Options: Global Options:
--json Output as JSON --json Output as JSON
--session <name> Use specific session --session <name> Use specific session
--tab <id> Target specific tab ID
Examples: Examples:
agent-browser network route "**/api/*" --abort agent-browser network route "**/api/*" --abort
@@ -1897,6 +1978,7 @@ Operations:
Global Options: Global Options:
--json Output as JSON --json Output as JSON
--session <name> Use specific session --session <name> Use specific session
--tab <id> Target specific tab ID
Examples: Examples:
agent-browser storage local agent-browser storage local
@@ -1936,6 +2018,7 @@ for the current page URL.
Global Options: Global Options:
--json Output as JSON --json Output as JSON
--session <name> Use specific session --session <name> Use specific session
--tab <id> Target specific tab ID
Examples: Examples:
# Simple cookie for current page # Simple cookie for current page
@@ -1971,14 +2054,15 @@ Usage: agent-browser tab [operation] [args]
Manage browser tabs in the current window. Manage browser tabs in the current window.
Operations: Operations:
list List all tabs (default) list List all tabs with tab IDs (default)
new [url] Open new tab new [url] Open new tab
close [index] Close tab (current if no index) close [id] Close tab by ID (current if no ID)
<index> Switch to tab by index <id> Switch to tab by ID
Global Options: Global Options:
--json Output as JSON --json Output as JSON
--session <name> Use specific session --session <name> Use specific session
--tab <id> Target specific tab ID
Examples: Examples:
agent-browser tab agent-browser tab
@@ -2006,6 +2090,7 @@ Operations:
Global Options: Global Options:
--json Output as JSON --json Output as JSON
--session <name> Use specific session --session <name> Use specific session
--tab <id> Target specific tab ID
Examples: Examples:
agent-browser window new agent-browser window new
@@ -2028,6 +2113,7 @@ Arguments:
Global Options: Global Options:
--json Output as JSON --json Output as JSON
--session <name> Use specific session --session <name> Use specific session
--tab <id> Target specific tab ID
Examples: Examples:
agent-browser frame "#embed-iframe" agent-browser frame "#embed-iframe"
@@ -2115,6 +2201,7 @@ Operations:
Global Options: Global Options:
--json Output as JSON --json Output as JSON
--session <name> Use specific session --session <name> Use specific session
--tab <id> Target specific tab ID
Examples: Examples:
agent-browser dialog accept agent-browser dialog accept
@@ -2140,6 +2227,7 @@ Operations:
Global Options: Global Options:
--json Output as JSON --json Output as JSON
--session <name> Use specific session --session <name> Use specific session
--tab <id> Target specific tab ID
Examples: Examples:
agent-browser trace start agent-browser trace start
@@ -2171,6 +2259,7 @@ Start Options:
Global Options: Global Options:
--json Output as JSON --json Output as JSON
--session <name> Use specific session --session <name> Use specific session
--tab <id> Target specific tab ID
Examples: Examples:
# Basic profiling # Basic profiling
@@ -2210,6 +2299,7 @@ Operations:
Global Options: Global Options:
--json Output as JSON --json Output as JSON
--session <name> Use specific session --session <name> Use specific session
--tab <id> Target specific tab ID
Examples: Examples:
# Record from current page (preserves login state) # Record from current page (preserves login state)
@@ -2242,6 +2332,7 @@ Options:
Global Options: Global Options:
--json Output as JSON --json Output as JSON
--session <name> Use specific session --session <name> Use specific session
--tab <id> Target specific tab ID
Examples: Examples:
agent-browser console agent-browser console
@@ -2262,6 +2353,7 @@ Options:
Global Options: Global Options:
--json Output as JSON --json Output as JSON
--session <name> Use specific session --session <name> Use specific session
--tab <id> Target specific tab ID
Examples: Examples:
agent-browser errors agent-browser errors
@@ -2281,6 +2373,7 @@ Visually highlights an element on the page for debugging.
Global Options: Global Options:
--json Output as JSON --json Output as JSON
--session <name> Use specific session --session <name> Use specific session
--tab <id> Target specific tab ID
Examples: Examples:
agent-browser highlight "#target-element" agent-browser highlight "#target-element"
@@ -2306,6 +2399,7 @@ Operations:
Global Options: Global Options:
--json Output as JSON --json Output as JSON
--session <name> Use specific session --session <name> Use specific session
--tab <id> Target specific tab ID
Examples: Examples:
agent-browser clipboard read agent-browser clipboard read
@@ -2345,6 +2439,7 @@ State Encryption:
Global Options: Global Options:
--json Output as JSON --json Output as JSON
--session <name> Use specific session --session <name> Use specific session
--tab <id> Target specific tab ID
Examples: Examples:
agent-browser state save ./auth-state.json agent-browser state save ./auth-state.json
@@ -2377,6 +2472,7 @@ Environment:
Global Options: Global Options:
--json Output as JSON --json Output as JSON
--session <name> Use specific session --session <name> Use specific session
--tab <id> Target specific tab ID
Examples: Examples:
agent-browser session agent-browser session
@@ -2474,6 +2570,7 @@ Supported URL formats:
Global Options: Global Options:
--json Output as JSON --json Output as JSON
--session <name> Use specific session --session <name> Use specific session
--tab <id> Target specific tab ID
Examples: Examples:
# Connect to local Chrome with remote debugging # Connect to local Chrome with remote debugging
@@ -2635,6 +2732,7 @@ URL Diff:
Global Options: Global Options:
--json Output as JSON --json Output as JSON
--session <name> Use specific session --session <name> Use specific session
--tab <id> Target specific tab ID
Examples: Examples:
agent-browser diff snapshot agent-browser diff snapshot
@@ -2929,6 +3027,7 @@ Authentication:
Options: Options:
--session <name> Isolated session (or AGENT_BROWSER_SESSION env) --session <name> Isolated session (or AGENT_BROWSER_SESSION env)
--tab <id> Target specific tab ID for the command
--executable-path <path> Custom browser executable (or AGENT_BROWSER_EXECUTABLE_PATH) --executable-path <path> Custom browser executable (or AGENT_BROWSER_EXECUTABLE_PATH)
--extension <path> Load browser extensions (repeatable) --extension <path> Load browser extensions (repeatable)
--args <args> Browser launch args, comma or newline separated (or AGENT_BROWSER_ARGS) --args <args> Browser launch args, comma or newline separated (or AGENT_BROWSER_ARGS)