diff --git a/cli/src/commands.rs b/cli/src/commands.rs index 073385d25..12a25e615 100644 --- a/cli/src/commands.rs +++ b/cli/src/commands.rs @@ -2210,6 +2210,7 @@ mod tests { screenshot_quality: None, screenshot_format: None, idle_timeout: None, + tab_index: None, } } diff --git a/cli/src/flags.rs b/cli/src/flags.rs index 83896f79e..6746b1240 100644 --- a/cli/src/flags.rs +++ b/cli/src/flags.rs @@ -219,6 +219,7 @@ fn extract_config_path(args: &[String]) -> Option> { "--screenshot-quality", "--screenshot-format", "--idle-timeout", + "--tab", ]; let mut i = 0; while i < args.len() { @@ -301,6 +302,7 @@ pub struct Flags { pub screenshot_quality: Option, pub screenshot_format: Option, pub idle_timeout: Option, // Canonical milliseconds string for AGENT_BROWSER_IDLE_TIMEOUT_MS + pub tab_index: Option, // Track which launch-time options were explicitly passed via CLI // (as opposed to being set only via environment variables) @@ -435,6 +437,7 @@ pub fn parse_flags(args: &[String]) -> Flags { cli_annotate: false, cli_download_path: false, cli_headed: false, + tab_index: None, }; let mut i = 0; @@ -488,6 +491,20 @@ pub fn parse_flags(args: &[String]) -> Flags { i += 1; } } + "--tab" => { + if let Some(s) = args.get(i + 1) { + if let Ok(idx) = s.parse::() { + flags.tab_index = Some(idx); + } else { + eprintln!( + "{} Invalid --tab value: {} (expected a number)", + color::warning_indicator(), + s + ); + } + i += 1; + } + } "--headers" => { if let Some(h) = args.get(i + 1) { flags.headers = Some(h.clone()); @@ -761,6 +778,7 @@ pub fn clean_args(args: &[String]) -> Vec { "--screenshot-quality", "--screenshot-format", "--idle-timeout", + "--tab", ]; let mut i = 0; diff --git a/cli/src/main.rs b/cli/src/main.rs index 53d63b9f7..149bd4a64 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -282,6 +282,11 @@ fn main() { } } + // Inject --tab index into command JSON if specified + if let Some(tab_idx) = flags.tab_index { + cmd["tabIndex"] = json!(tab_idx); + } + // Validate session name before starting daemon if let Some(ref name) = flags.session_name { if !validation::is_valid_session_name(name) { diff --git a/cli/src/native/actions.rs b/cli/src/native/actions.rs index 52108236e..d378a5f8d 100644 --- a/cli/src/native/actions.rs +++ b/cli/src/native/actions.rs @@ -704,6 +704,26 @@ pub async fn execute_command(cmd: &Value, state: &mut DaemonState) -> Value { ); } + // If --tab N was specified, temporarily override the active page index + let tab_override = cmd + .get("tabIndex") + .and_then(|v| v.as_u64()) + .map(|v| v as usize); + let prev_page_index = if let Some(idx) = tab_override { + match state.browser.as_mut() { + Some(mgr) => { + let prev = mgr.active_page(); + if let Err(e) = mgr.set_active_page(idx) { + return error_response(&id, &format!("--tab override failed: {}", e)); + } + Some(prev) + } + None => None, + } + } else { + None + }; + let result = match action { "launch" => handle_launch(cmd, state).await, "navigate" => handle_navigate(cmd, state).await, @@ -859,6 +879,19 @@ pub async fn execute_command(cmd: &Value, state: &mut DaemonState) -> Value { _ => Err(format!("Not yet implemented: {}", action)), }; + // Restore original active page index after --tab override. + // Skip restore for tab-mutating commands: after tab_close the saved + // index may be out of range (or point to the wrong tab), and tab_new / + // tab_switch intentionally change the active tab. + let is_tab_mutating = matches!(action, "tab_close" | "tab_new" | "tab_switch"); + if !is_tab_mutating { + if let Some(prev) = prev_page_index { + if let Some(ref mut mgr) = state.browser { + let _ = mgr.set_active_page(prev); + } + } + } + match result { Ok(data) => success_response(&id, data), Err(e) => error_response(&id, &super::browser::to_ai_friendly_error(&e)), diff --git a/cli/src/native/browser.rs b/cli/src/native/browser.rs index 8adf79202..16522e921 100644 --- a/cli/src/native/browser.rs +++ b/cli/src/native/browser.rs @@ -430,6 +430,25 @@ impl BrowserManager { .ok_or_else(|| "No active page".to_string()) } + /// Get the current active page index. + pub fn active_page(&self) -> usize { + self.active_page_index + } + + /// Temporarily override the active page index. Returns an error if the + /// index is out of range. + pub fn set_active_page(&mut self, index: usize) -> Result<(), String> { + if index >= self.pages.len() { + return Err(format!( + "Tab index {} out of range (0-{})", + index, + self.pages.len().saturating_sub(1) + )); + } + self.active_page_index = index; + Ok(()) + } + pub async fn navigate(&mut self, url: &str, wait_until: WaitUntil) -> Result { let session_id = self.active_session_id()?.to_string(); let mut lifecycle_rx = self.client.subscribe(); diff --git a/cli/src/output.rs b/cli/src/output.rs index cd6f8790d..f688d5b90 100644 --- a/cli/src/output.rs +++ b/cli/src/output.rs @@ -2621,6 +2621,7 @@ Options: --action-policy Action policy JSON file (or AGENT_BROWSER_ACTION_POLICY) --confirm-actions Categories requiring confirmation (or AGENT_BROWSER_CONFIRM_ACTIONS) --confirm-interactive Interactive confirmation prompts; auto-denies if stdin is not a TTY (or AGENT_BROWSER_CONFIRM_INTERACTIVE) + --tab Run command against a specific tab without switching --engine Browser engine: chrome (default), lightpanda (or AGENT_BROWSER_ENGINE) --config Use a custom config file (or AGENT_BROWSER_CONFIG env) --debug Debug output