Skip to content

Consistent Tab IDs & Global Tag Targeting#892

Merged
ctate merged 7 commits intovercel-labs:mainfrom
DJRHails:patch/tab-targeting
Apr 16, 2026
Merged

Consistent Tab IDs & Global Tag Targeting#892
ctate merged 7 commits intovercel-labs:mainfrom
DJRHails:patch/tab-targeting

Conversation

@DJRHails
Copy link
Copy Markdown
Contributor

  • replace positional tab indices with tab IDs across tab commands and response payloads
  • add global --tab <id> so page-scoped commands can target a tab directly
  • scope --tab routing so commands restore the previous active tab when they temporarily target a different tab
  • Fix the tab_list fallback which can be a bit confusing (especially when LLMs hallucinate a tab switch functionality).

Personally I'm not in love with the incrementing integer as the id; but it's a pretty significant refactor otherwise. I've still currently made one breaking change by renaming 'index' -> 'tabId' but that seems worth it for the clarity of code.

Motivation

Shared-browser CDP workflows break down when tabs are addressed by positional index because unrelated tab churn changes what a given index points to.

Example:

$ agent-browser tab list
  [0] App - https://app.example.com
  [1] Docs - https://docs.example.com
→ [2] Google - https://www.google.com/

# Another workflow closes a different tab
$ agent-browser tab close 1
✓ Tab 1 closed

$ agent-browser tab list
  [0] App - https://app.example.com
→ [1] Google - https://www.google.com/ (should be [2])

Or another page opens a popup/new tab:

$ agent-browser tab list
→ [0] App - https://app.example.com
     [1] Google - https://www.google.com/

# Another workflow opens a popup (you expect it at tab 11)
$ agent-browser tab list
  [0] App - https://app.example.com
  [1] Google - https://www.google.com/
  [2] Popup - https://accounts.example.com/

With tab IDs, existing tabs keep the same identifier even when other tabs are closed or popups appear, so agents can continue targeting the same tab across commands.

Testing

  • pnpm exec vitest run src/browser.test.ts src/protocol.test.ts src/daemon.test.ts
  • pnpm exec tsc --noEmit
  • cargo test --manifest-path cli/Cargo.toml tab_
  • cargo test --manifest-path cli/Cargo.toml parse_tab_flag
  • cargo test --manifest-path cli/Cargo.toml clean_args_removes_tab_flag

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented Mar 17, 2026

@DJRHails is attempting to deploy a commit to the Vercel Labs Team on Vercel.

A member of the Team first needs to authorize it.

@DJRHails
Copy link
Copy Markdown
Contributor Author

Hey @mvanhorn - just saw #883 - let me know what you think. Had this in a fork.

@patch-stack-bot patch-stack-bot Bot force-pushed the patch/tab-targeting branch from ad6eaca to 654c33c Compare March 17, 2026 23:50
patch-stack-bot Bot pushed a commit to DJRHails/agent-browser that referenced this pull request Mar 17, 2026
Comment thread cli/src/output.rs Outdated
@DJRHails DJRHails force-pushed the patch/tab-targeting branch 2 times, most recently from 9d2cff6 to ceb7451 Compare March 18, 2026 00:56
@patch-stack-bot patch-stack-bot Bot force-pushed the patch/tab-targeting branch from ceb7451 to 29b2ee1 Compare March 18, 2026 04:40
patch-stack-bot Bot added a commit to DJRHails/agent-browser that referenced this pull request Mar 18, 2026
@mvanhorn
Copy link
Copy Markdown
Contributor

Hey @DJRHails - the stable tab ID approach looks solid. My #883 uses positional indexes which is simpler but less robust. I'd defer to @ctate on which direction to go - happy to close mine if the ID-based approach is preferred.

@DJRHails DJRHails force-pushed the patch/tab-targeting branch from 29b2ee1 to ceb7451 Compare March 18, 2026 15:35
@patch-stack-bot patch-stack-bot Bot force-pushed the patch/tab-targeting branch from ceb7451 to 656ff18 Compare March 18, 2026 15:45
patch-stack-bot Bot added a commit to DJRHails/agent-browser that referenced this pull request Mar 18, 2026
@patch-stack-bot patch-stack-bot Bot force-pushed the patch/tab-targeting branch from 656ff18 to b251966 Compare March 19, 2026 04:39
patch-stack-bot Bot added a commit to DJRHails/agent-browser that referenced this pull request Mar 19, 2026
@patch-stack-bot patch-stack-bot Bot force-pushed the patch/tab-targeting branch from b251966 to a10a668 Compare March 20, 2026 04:34
patch-stack-bot Bot added a commit to DJRHails/agent-browser that referenced this pull request Mar 20, 2026
@patch-stack-bot patch-stack-bot Bot force-pushed the patch/tab-targeting branch 2 times, most recently from 915231e to e909a8e Compare March 22, 2026 04:38
patch-stack-bot Bot added a commit to DJRHails/agent-browser that referenced this pull request Mar 22, 2026
@mvanhorn
Copy link
Copy Markdown
Contributor

Interesting approach with persistent tab IDs - that's a cleaner foundation than positional indices. Happy to rebase #883 on top of yours if @ctate prefers your approach, or we can coordinate to merge the concepts.

@patch-stack-bot patch-stack-bot Bot force-pushed the patch/tab-targeting branch from e909a8e to 58b773d Compare March 23, 2026 04:45
patch-stack-bot Bot added a commit to DJRHails/agent-browser that referenced this pull request Mar 23, 2026
@patch-stack-bot patch-stack-bot Bot force-pushed the patch/tab-targeting branch from 58b773d to a2e3e67 Compare March 24, 2026 04:37
patch-stack-bot Bot pushed a commit to DJRHails/agent-browser that referenced this pull request Mar 24, 2026
@patch-stack-bot patch-stack-bot Bot force-pushed the patch/tab-targeting branch from a2e3e67 to f8e67fd Compare March 25, 2026 04:38
patch-stack-bot Bot added a commit to DJRHails/agent-browser that referenced this pull request Mar 25, 2026
@patch-stack-bot patch-stack-bot Bot force-pushed the patch/tab-targeting branch from f8e67fd to 76f1192 Compare March 26, 2026 04:49
patch-stack-bot Bot pushed a commit to DJRHails/agent-browser that referenced this pull request Mar 26, 2026
@patch-stack-bot patch-stack-bot Bot force-pushed the patch/tab-targeting branch from 00c2a67 to c2247ec Compare April 10, 2026 04:58
patch-stack-bot Bot added a commit to DJRHails/agent-browser that referenced this pull request Apr 10, 2026
patch-stack-bot Bot added a commit to DJRHails/agent-browser that referenced this pull request Apr 11, 2026
patch-stack-bot Bot added a commit to DJRHails/agent-browser that referenced this pull request Apr 12, 2026
@patch-stack-bot patch-stack-bot Bot force-pushed the patch/tab-targeting branch from c2247ec to 6ad989f Compare April 13, 2026 05:03
patch-stack-bot Bot added a commit to DJRHails/agent-browser that referenced this pull request Apr 13, 2026
@patch-stack-bot patch-stack-bot Bot force-pushed the patch/tab-targeting branch from 6ad989f to eec479d Compare April 14, 2026 04:58
patch-stack-bot Bot pushed a commit to DJRHails/agent-browser that referenced this pull request Apr 14, 2026
patch-stack-bot Bot added a commit to DJRHails/agent-browser that referenced this pull request Apr 15, 2026
Replace positional indices with stable integer tab IDs throughout
the browser tab management system. Tab IDs are monotonically
increasing (starting at 1) and persist across tab open/close
operations, making them reliable identifiers for AI agents.

Changes:
- browser.rs: Add tab_id field to PageInfo, next_tab_id counter to
  BrowserManager, tab_switch_by_id/tab_close_by_id methods
- actions.rs: Read tabId instead of index for tab_switch/tab_close,
  assign tab IDs in window_new and target-created event handlers
- commands.rs: Emit tabId instead of index in tab switch/close commands
- flags.rs: Add --tab <id> flag to Config and Flags structs
- main.rs: Inject tabId from --tab flag into commands
- output.rs: Display tab IDs in list/new/close output, add --tab
  to all command help Global Options sections
Add pre-dispatch tab switch in execute_command so any command with a
tabId field (injected by --tab CLI flag) runs against the specified
tab. Previously only tab_switch and tab_close honoured tabId.

Add e2e tests for tab ID non-reuse after close and global targeting.
`tab select 3` and other unknown subcommands silently fell through to
`tab_list`, making it appear the tab switch succeeded when it didn't.
Now errors with valid subcommand suggestions, matching the pattern
used by `window` and other commands. Bare `tab` (no args) still
defaults to `tab_list`.
Tests snapshot returns correct content when targeting a specific tab
via tabId, including non-contiguous tab IDs after closing a tab.
The condition `data.get("tabId").is_some()` matched any response
containing tabId, including tab_switch. Now only checks for
`data.get("closed").is_some()` and adds a dedicated tab_switch
output handler showing "Switched to tab [id] (url)".
@patch-stack-bot patch-stack-bot Bot force-pushed the patch/tab-targeting branch from eec479d to d504478 Compare April 16, 2026 05:00
patch-stack-bot Bot added a commit to DJRHails/agent-browser that referenced this pull request Apr 16, 2026
ctate added a commit that referenced this pull request Apr 16, 2026
PR #892 added a required `tab_id: u32` field to `PageInfo` but missed two
initializer sites, which broke the build on the PR branch. CI never caught
this because the external-contributor workflow status was `action_required`
and never ran.

- `cli/src/native/browser.rs:395` — the `direct_page` branch of
  `connect_cdp_inner` used by the cloud providers (Browserbase, Browserless,
  Browser Use, Kernel, AgentCore). Use `assign_tab_id()` to get a fresh id.
- `cli/src/native/browser.rs:1580` — a unit test initializer. Use `tab_id: 1`
  since the test doesn't exercise id assignment.
ctate added a commit that referenced this pull request Apr 16, 2026
Follow-up on PR #892's `--tab <id>` flag.

The original implementation called `tab_switch_by_id` directly from the
pre-dispatch block in `execute_command` but didn't touch the daemon's
per-tab state, and never restored the previously-active tab. Two concrete
issues this fixes:

1. `state.ref_map`, `state.iframe_sessions`, and `state.active_frame_id`
   were left intact across the pre-dispatch switch, so `--tab N click @e1`
   would try to resolve `@e1` against the scoped tab's DOM using a
   backend-node id from the outer tab. In practice the click handler's
   role+name fallback hid this as "element not found" errors, but on pages
   where both tabs have similarly-labelled elements it could click the
   wrong one.

2. The PR description promised scoped routing would "restore the previous
   active tab", but the implementation permanently switched. `--tab 3
   snapshot` would leave tab 3 as the active tab even after the command
   returned, surprising subsequent non-scoped commands.

This change:

- Saves the current tab's stable `tab_id` (not its array index, which
  would shift if the scoped command closed other tabs) before switching.
- Clears per-tab daemon state before the switch so refs/iframes/frame
  context can't leak between tabs.
- After the action runs, restores the original active tab (also via
  stable id) unless that tab was closed during the scoped command, in
  which case we leave the scoped tab active.
- Adds `BrowserManager::active_tab_id()` and `has_tab_id()` accessors
  to support the above without exposing the internal `pages` vector.
ctate added a commit that referenced this pull request Apr 16, 2026
Per AGENTS.md, changes that users or agents would need to know about must
land in every doc surface. Fills the gaps PR #892 left:

- `README.md` — new `--tab <id>` row in the Options table, rewrite the
  tab command examples to use `<id>` instead of `<n>`, add a paragraph
  explaining stable tab IDs and `--tab` peek semantics.
- `docs/src/app/commands/page.mdx` — same command-example rewrite plus a
  new "Stable tab IDs and `--tab`" subsection.
- `docs/src/app/configuration/page.mdx` — add `tab` row to the config
  options table so JSON config users can discover it.
- `agent-browser.schema.json` — add `tab` property with description,
  matching the config schema.
- `skills/agent-browser/references/commands.md` — same command-example
  rewrite plus a short paragraph for agents on when to use `--tab`.
@ctate
Copy link
Copy Markdown
Collaborator

ctate commented Apr 16, 2026

Thanks @DJRHails - this is a solid implementation

@ctate ctate merged commit 67dc631 into vercel-labs:main Apr 16, 2026
2 of 4 checks passed
ctate added a commit that referenced this pull request Apr 16, 2026
PR #892 added a required `tab_id: u32` field to `PageInfo` but missed two
initializer sites, which broke the build on the PR branch. CI never caught
this because the external-contributor workflow status was `action_required`
and never ran.

- `cli/src/native/browser.rs:395` — the `direct_page` branch of
  `connect_cdp_inner` used by the cloud providers (Browserbase, Browserless,
  Browser Use, Kernel, AgentCore). Use `assign_tab_id()` to get a fresh id.
- `cli/src/native/browser.rs:1580` — a unit test initializer. Use `tab_id: 1`
  since the test doesn't exercise id assignment.
ctate added a commit that referenced this pull request Apr 16, 2026
Follow-up on PR #892's `--tab <id>` flag.

The original implementation called `tab_switch_by_id` directly from the
pre-dispatch block in `execute_command` but didn't touch the daemon's
per-tab state, and never restored the previously-active tab. Two concrete
issues this fixes:

1. `state.ref_map`, `state.iframe_sessions`, and `state.active_frame_id`
   were left intact across the pre-dispatch switch, so `--tab N click @e1`
   would try to resolve `@e1` against the scoped tab's DOM using a
   backend-node id from the outer tab. In practice the click handler's
   role+name fallback hid this as "element not found" errors, but on pages
   where both tabs have similarly-labelled elements it could click the
   wrong one.

2. The PR description promised scoped routing would "restore the previous
   active tab", but the implementation permanently switched. `--tab 3
   snapshot` would leave tab 3 as the active tab even after the command
   returned, surprising subsequent non-scoped commands.

This change:

- Saves the current tab's stable `tab_id` (not its array index, which
  would shift if the scoped command closed other tabs) before switching.
- Clears per-tab daemon state before the switch so refs/iframes/frame
  context can't leak between tabs.
- After the action runs, restores the original active tab (also via
  stable id) unless that tab was closed during the scoped command, in
  which case we leave the scoped tab active.
- Adds `BrowserManager::active_tab_id()` and `has_tab_id()` accessors
  to support the above without exposing the internal `pages` vector.
ctate added a commit that referenced this pull request Apr 16, 2026
Per AGENTS.md, changes that users or agents would need to know about must
land in every doc surface. Fills the gaps PR #892 left:

- `README.md` — new `--tab <id>` row in the Options table, rewrite the
  tab command examples to use `<id>` instead of `<n>`, add a paragraph
  explaining stable tab IDs and `--tab` peek semantics.
- `docs/src/app/commands/page.mdx` — same command-example rewrite plus a
  new "Stable tab IDs and `--tab`" subsection.
- `docs/src/app/configuration/page.mdx` — add `tab` row to the config
  options table so JSON config users can discover it.
- `agent-browser.schema.json` — add `tab` property with description,
  matching the config schema.
- `skills/agent-browser/references/commands.md` — same command-example
  rewrite plus a short paragraph for agents on when to use `--tab`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants