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
+27 -10
View File
@@ -662,7 +662,9 @@ impl DaemonState {
.await;
}
let tab_id = mgr.assign_tab_id();
mgr.add_page(super::browser::PageInfo {
tab_id,
target_id: te.target_info.target_id.clone(),
session_id: attach.session_id,
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 {
"launch" => handle_launch(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> {
let mgr = state.browser.as_mut().ok_or("Browser not launched")?;
let index = cmd
.get("index")
let tab_id = cmd
.get("tabId")
.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.iframe_sessions.clear();
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 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> {
let mgr = state.browser.as_mut().ok_or("Browser not launched")?;
let index = cmd
.get("index")
.and_then(|v| v.as_u64())
.map(|i| i as usize);
let tab_id = cmd.get("tabId").and_then(|v| v.as_u64()).map(|i| i as u32);
state.ref_map.clear();
state.iframe_sessions.clear();
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> {
@@ -4065,7 +4078,9 @@ async fn handle_recording_start(cmd: &Value, state: &mut DaemonState) -> Result<
}
// Add page and switch to it
let tab_id = mgr.assign_tab_id();
mgr.add_page(super::browser::PageInfo {
tab_id,
target_id: create_result.target_id,
session_id: new_session_id.clone(),
url: nav_url.clone(),
@@ -5976,7 +5991,9 @@ async fn handle_window_new(cmd: &Value, state: &mut DaemonState) -> Result<Value
)
.await?;
let tab_id = mgr.assign_tab_id();
mgr.add_page(super::browser::PageInfo {
tab_id,
target_id: create_result.target_id,
session_id: attach.session_id,
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();
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> {