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
+33
View File
@@ -57,6 +57,7 @@ pub struct Config {
pub json: Option<bool>,
pub debug: Option<bool>,
pub session: Option<String>,
pub tab: Option<u32>,
pub session_name: Option<String>,
pub executable_path: Option<String>,
pub extensions: Option<Vec<String>>,
@@ -98,6 +99,7 @@ impl Config {
json: other.json.or(self.json),
debug: other.debug.or(self.debug),
session: other.session.or(self.session),
tab: other.tab.or(self.tab),
session_name: other.session_name.or(self.session_name),
executable_path: other.executable_path.or(self.executable_path),
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>> {
const FLAGS_WITH_VALUE: &[&str] = &[
"--session",
"--tab",
"--headers",
"--executable-path",
"--cdp",
@@ -273,6 +276,7 @@ pub struct Flags {
pub headed: bool,
pub debug: bool,
pub session: String,
pub tab: Option<u32>,
pub headers: Option<String>,
pub executable_path: Option<String>,
pub cdp: Option<String>,
@@ -355,6 +359,7 @@ pub fn parse_flags(args: &[String]) -> Flags {
.ok()
.or(config.session)
.unwrap_or_else(|| "default".to_string()),
tab: config.tab,
headers: config.headers,
executable_path: env::var("AGENT_BROWSER_EXECUTABLE_PATH")
.ok()
@@ -492,6 +497,12 @@ pub fn parse_flags(args: &[String]) -> Flags {
i += 1;
}
}
"--tab" => {
if let Some(s) = args.get(i + 1) {
flags.tab = s.parse::<u32>().ok();
i += 1;
}
}
"--idle-timeout" => {
if let Some(s) = args.get(i + 1) {
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)
const GLOBAL_FLAGS_WITH_VALUE: &[&str] = &[
"--session",
"--tab",
"--headers",
"--executable-path",
"--cdp",
@@ -1441,4 +1453,25 @@ mod tests {
let clean = clean_args(&input);
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));
}
}