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
+51 -4
View File
@@ -158,6 +158,7 @@ pub fn to_ai_friendly_error(error: &str) -> String {
#[derive(Debug, Clone)]
pub struct PageInfo {
pub tab_id: u32,
pub target_id: String,
pub session_id: String,
pub url: String,
@@ -226,6 +227,7 @@ pub struct BrowserManager {
pub ignore_https_errors: bool,
/// Origins visited during this session, used by save_state to collect cross-origin localStorage.
visited_origins: HashSet<String>,
next_tab_id: u32,
}
const LIGHTPANDA_CDP_CONNECT_TIMEOUT: Duration = Duration::from_secs(5);
@@ -297,6 +299,7 @@ impl BrowserManager {
download_path: download_path.clone(),
ignore_https_errors,
visited_origins: HashSet::new(),
next_tab_id: 1,
};
manager.discover_and_attach_targets().await?;
manager
@@ -385,6 +388,7 @@ impl BrowserManager {
download_path: None,
ignore_https_errors: false,
visited_origins: HashSet::new(),
next_tab_id: 1,
};
if direct_page {
@@ -453,7 +457,10 @@ impl BrowserManager {
)
.await?;
let tab_id = self.next_tab_id;
self.next_tab_id += 1;
self.pages.push(PageInfo {
tab_id,
target_id: result.target_id,
session_id: attach_result.session_id.clone(),
url: "about:blank".to_string(),
@@ -476,7 +483,10 @@ impl BrowserManager {
)
.await?;
let tab_id = self.next_tab_id;
self.next_tab_id += 1;
self.pages.push(PageInfo {
tab_id,
target_id: target.target_id.clone(),
session_id: attach_result.session_id.clone(),
url: target.url.clone(),
@@ -821,7 +831,10 @@ impl BrowserManager {
)
.await?;
let tab_id = self.next_tab_id;
self.next_tab_id += 1;
self.pages.push(PageInfo {
tab_id,
target_id: result.target_id,
session_id: attach_result.session_id.clone(),
url: "about:blank".to_string(),
@@ -864,7 +877,7 @@ impl BrowserManager {
.enumerate()
.map(|(i, p)| {
json!({
"index": i,
"tabId": p.tab_id,
"title": p.title,
"url": p.url,
"type": p.target_type,
@@ -902,8 +915,11 @@ impl BrowserManager {
self.enable_domains(&attach.session_id).await?;
let tab_id = self.next_tab_id;
self.next_tab_id += 1;
let index = self.pages.len();
self.pages.push(PageInfo {
tab_id,
target_id: result.target_id,
session_id: attach.session_id,
url: target_url.to_string(),
@@ -912,7 +928,7 @@ impl BrowserManager {
});
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> {
@@ -942,7 +958,8 @@ impl BrowserManager {
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> {
@@ -958,6 +975,7 @@ impl BrowserManager {
let page = self.pages.remove(target_index);
self.update_active_page_after_removal(target_index);
let closed_tab_id = page.tab_id;
let _ = self
.client
.send_command_typed::<_, Value>(
@@ -972,7 +990,7 @@ impl BrowserManager {
let session_id = self.pages[self.active_page_index].session_id.clone();
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())
}
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) {
let index = self.pages.len();
self.pages.push(page);
@@ -1393,6 +1439,7 @@ async fn initialize_lightpanda_manager(
download_path: None,
ignore_https_errors: false,
visited_origins: HashSet::new(),
next_tab_id: 1,
};
match discover_and_attach_lightpanda_targets(&mut manager, deadline).await {