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
1 change: 1 addition & 0 deletions cli/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2210,6 +2210,7 @@ mod tests {
screenshot_quality: None,
screenshot_format: None,
idle_timeout: None,
tab_index: None,
}
}

Expand Down
18 changes: 18 additions & 0 deletions cli/src/flags.rs
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ fn extract_config_path(args: &[String]) -> Option<Option<String>> {
"--screenshot-quality",
"--screenshot-format",
"--idle-timeout",
"--tab",
];
let mut i = 0;
while i < args.len() {
Expand Down Expand Up @@ -301,6 +302,7 @@ pub struct Flags {
pub screenshot_quality: Option<u32>,
pub screenshot_format: Option<String>,
pub idle_timeout: Option<String>, // Canonical milliseconds string for AGENT_BROWSER_IDLE_TIMEOUT_MS
pub tab_index: Option<usize>,

// Track which launch-time options were explicitly passed via CLI
// (as opposed to being set only via environment variables)
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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::<usize>() {
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());
Expand Down Expand Up @@ -761,6 +778,7 @@ pub fn clean_args(args: &[String]) -> Vec<String> {
"--screenshot-quality",
"--screenshot-format",
"--idle-timeout",
"--tab",
];

let mut i = 0;
Expand Down
5 changes: 5 additions & 0 deletions cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
33 changes: 33 additions & 0 deletions cli/src/native/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)),
Expand Down
19 changes: 19 additions & 0 deletions cli/src/native/browser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Value, String> {
let session_id = self.active_session_id()?.to_string();
let mut lifecycle_rx = self.client.subscribe();
Expand Down
1 change: 1 addition & 0 deletions cli/src/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2621,6 +2621,7 @@ Options:
--action-policy <path> Action policy JSON file (or AGENT_BROWSER_ACTION_POLICY)
--confirm-actions <list> 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 <index> Run command against a specific tab without switching
--engine <name> Browser engine: chrome (default), lightpanda (or AGENT_BROWSER_ENGINE)
--config <path> Use a custom config file (or AGENT_BROWSER_CONFIG env)
--debug Debug output
Expand Down
Loading