TLDR; Browser automation libraries no longer need crazy hacks to find the foreground tab! Chrome v150+ now exposes tabActive, tabPinned, tabGroupId, and more via TargetInfo!

Chrome’s DevTools Protocol has been the de-facto standard for automating browsers for years.
It’s nearly feature-complete, it can evaluate JavaScript, inspect the DOM, watch network traffic, capture screenshots, drive input, and attach to workers and frames. However, until recently, browser automation libraries still had a painful blind spot: there was no CDP API to tell where a tab sits in the browser UI and whether it was in the foreground or background.
That matters when a browser driver needs to coexist with a human user or another agent.
Automation code often needs to answer basic questions:
- What order are the tabs in?
- Which tab is foregrounded?
- Is this tab pinned?
- Is this tab in a tab group?
- Which browser window contains this tab?
Chrome extension APIs have exposed most of this for a long time via chrome.tabs.query(...), but CDP lacked any equivalent API.
This post documents my work to fix this in a Chromium patch, which just landed in Chrome Canary v150.0.7848.0 (on May 20th, 2026)!
🎭 Playwright, Puppeteer, Selenium, Stagehand, etc. have historically struggled to track foreground tab state reliably.
Browser driver libraries have had to infer tab state indirectly. That leads to bad behavior.
❌ Some libraries assumed the most recently opened tab is the foreground tab. That breaks as soon as a human clicks elsewhere, another automation client activates something else, the browser restores tabs, or a tab opens in the background.
❌ Some libraries forced every tab they used into the foreground with Target.activateTarget. That is disruptive. It steals focus from the human, and on macOS it can bring the entire browser application to the front, which makes background browser agents almost unusable.
❌ Some libraries assumed the order returned by Target.getTargets matches the tab strip order or the most-recently-foregrounded order. It does neither.
❌ Some libraries injected scripts into every page to watch focus, visibility, mousemove, load state, or foreground JavaScript events. Those approaches are brittle. Browser UI transitions do not reliably fire page JS events, pages can be frozen or discarded, and injected scripts cannot access Chrome’s tab strip APIs.
The missing data was ordinary tab metadata users can see with their eyes: order, active state, pinned state, and group membership.
🫣 First Try: Adding a new Target.queryTabs() method
My first proposal was direct: add a CDP command named Target.queryTabs that worked exactly like the Extensions API chrome.tabs.query(...).
I prototyped this in my Chromium fork first:
https://github.com/pirate/chromium/pull/1
Then I submitted it to Gerrit:
https://chromium-review.googlesource.com/c/chromium/src/+/7787097
The Chrome DevTools reviewers pushed the design in a better direction: add an extensible embedderData object to Target.TargetInfo, then let Chrome fill it for tab targets.
👌 Second Try: Target.getTargets() + embedderData
The Google developers on the Chromium team mentioned Tabs can be embedded in multiple UIs, not just Chrome (e.g. on Android, Chrome OS, Fuchsia, etc.). They wanted me to expose implementation-specific metadata that keeps the core protocol generic across embedders with different tab models.
The final shape we landed on is:
tabGroupId is an optional string, present only when the tab is in a group.
browserContextId already exists on TargetInfo, so the final patch kept it there instead of duplicating it inside embedderData.
windowId stayed out of embedderData as well. CDP has Browser.getWindowForTarget for mapping a tab target to its window and bounds.
The change intentionally does not emit new Target.targetInfoChanged events when embedderData changes. For now this is pull-based: clients call Target.getTargets or Target.getTargetInfo.
The feature is available in Chrome Canary today:
Chrome Canary >= 150.0.7848.0 (commit:5aa804ae0b62bd1b0d54f57494211239e2ed5ffe)
📑 Why are tab & page duplicate targets for the same URL?
Most browser automation libraries are built around CDP targets of type "page". These are the targets you attach to for Runtime.*, Page.*, DOM.*, and similar page-level commands.
The new metadata lives on targets of type "tab" (which you may have never seen before).
In Chromium, a tab target is the browser/UI container. It corresponds to a WebContents and gives Chrome a reliable place to answer tab strip questions.
A page target is a renderer/main-frame debugging target: the surface you drive when you inspect DOM, evaluate JavaScript, or watch page lifecycle.
In 2020 Chromium changed their internal models to allow exposing multiple page-like targets associated with one tab as part of the MPArch (multi-page tab architecture) initiative. Examples where it’s used include prerendering and browser UI features that compose multiple page surfaces inside one tab, such as split views.
More historic context on MPArch:
- Overview of the MPArch Project in Chromium | Igalia Blogpost
- Multi-Page Architecture Original Design Document | Google
- Multi Page Architecture (BlinkOn 13) | YouTube Talk + Slides
- Pre-Rendering (BlinkOn 13) | YouTube Talk
- Fixing Long Tail of Features for MPArch | Google Doc
That makes this model too narrow:
A better normalized model is:
The tab target describes where the browser UI container is visible. The page
targets are the debuggable page surfaces you can send DOM.* commands to.
🧑💻 So how do we use this new feature?
➕ Chromium Contribution Flow for Non-Google Devs
This was my first Chromium patch that I’ve submitted upstream, and I hit several process issues that were obvious in hindsight.
The path that worked was:
1. Opened an issue with a detailed problem statement + got stakeholders to upvote it:
https://issues.chromium.org/u/1/issues/497896141

2. Prototyped the fix on GitHub fork (just for convenience, I find GH easier to use than Gerrit): https://github.com/pirate/chromium/pull/1

3. Submitted my patchset to Gerrit: https://chromium-review.googlesource.com/c/chromium/src/+/7787097

4. Updated the patch in Gerrit based on comments + CI errors:
- used an alias email for gerrit &
AUTHORSto avoid getting spam for years - added myself to
AUTHORS(keepingAUTHORSin alphabetical order) - added test coverage on all the happy paths (it’s ok if not every null branch is covered)

5. Get code owner review and a Chromium committer to trigger CI + the commit queue.
I never once had to build all of Chromium on my local machine (which would’ve taken 8hr+).
I used cheap local checks where possible, and relied on Gerrit CI for the full platform matrix.
The most useful local checks were:
🚀 Potential Future Improvements
This is a big improvement already, but there are several things we could do to make it even easier to interact with tab state via CDP:
- a way to get all page/tab/window state in a single call without multiple roundtrips/events
- an event that notifies when foreground tab or tab ordering/grouping/pinning state changes
- inline
tabTargetIdandpageTargetIdon every response to easily link tabs<->pages - a CDP field to get the
chrome.tabs.query()[0].idinteger tab ID exposed to Extensions
For now, Target.getTargets({ filter: [{ type: "tab", exclude: false }] }),Target.autoAttachRelated, and Browser.getWindowForTarget are enough to build a reliable tab inventory without focus-stealing hacks.






