chore: Merge branch 'main'

This commit is contained in:
Nikhil Sonti
2026-03-10 18:22:19 -07:00
7 changed files with 251 additions and 278 deletions

View File

@@ -1,5 +1,5 @@
diff --git a/chrome/browser/devtools/BUILD.gn b/chrome/browser/devtools/BUILD.gn
index 4fc39e878028e..9a7fb91ab49e8 100644
index 3ba666bf1e264..3655114cdaae9 100644
--- a/chrome/browser/devtools/BUILD.gn
+++ b/chrome/browser/devtools/BUILD.gn
@@ -34,6 +34,10 @@ if (enable_devtools_frontend) {
@@ -13,7 +13,20 @@ index 4fc39e878028e..9a7fb91ab49e8 100644
"protocol/pwa.cc",
"protocol/pwa.h",
"protocol/security.cc",
@@ -372,7 +376,10 @@ static_library("devtools") {
@@ -362,7 +366,11 @@ static_library("devtools") {
}
if (is_mac) {
- sources += [ "devtools_dock_tile_mac.mm" ]
+ sources += [
+ "devtools_dock_tile_mac.mm",
+ "protocol/browser_handler_mac.h",
+ "protocol/browser_handler_mac.mm",
+ ]
} else {
sources += [ "devtools_dock_tile.cc" ]
}
@@ -379,7 +387,10 @@ static_library("devtools") {
"//components/media_router/browser",
"//components/media_router/common/mojom:media_router",
"//components/payments/content",
@@ -24,7 +37,7 @@ index 4fc39e878028e..9a7fb91ab49e8 100644
"//components/security_state/content",
"//components/subresource_filter/content/browser",
"//components/web_package",
@@ -386,8 +393,14 @@ static_library("devtools") {
@@ -393,8 +404,12 @@ static_library("devtools") {
sources += [
"protocol/autofill_handler.cc",
"protocol/autofill_handler.h",
@@ -32,8 +45,6 @@ index 4fc39e878028e..9a7fb91ab49e8 100644
+ "protocol/bookmarks_handler.h",
"protocol/browser_handler.cc",
"protocol/browser_handler.h",
+ "protocol/hidden_tab_manager.cc",
+ "protocol/hidden_tab_manager.h",
+ "protocol/history_handler.cc",
+ "protocol/history_handler.h",
"protocol/cast_handler.cc",

View File

@@ -1,5 +1,5 @@
diff --git a/chrome/browser/devtools/protocol/browser_handler.cc b/chrome/browser/devtools/protocol/browser_handler.cc
index 30bd52d09c3fc..2b0bba666f4ae 100644
index 30bd52d09c3fc..2df7c861f26aa 100644
--- a/chrome/browser/devtools/protocol/browser_handler.cc
+++ b/chrome/browser/devtools/protocol/browser_handler.cc
@@ -8,19 +8,32 @@
@@ -9,10 +9,10 @@ index 30bd52d09c3fc..2b0bba666f4ae 100644
+#include "base/memory/raw_ptr.h"
#include "base/memory/ref_counted_memory.h"
+#include "base/strings/utf_string_conversions.h"
+#include "build/build_config.h"
#include "chrome/app/chrome_command_ids.h"
#include "chrome/browser/devtools/chrome_devtools_manager_delegate.h"
#include "chrome/browser/devtools/devtools_dock_tile.h"
+#include "chrome/browser/devtools/protocol/hidden_tab_manager.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_manager.h"
+#include "chrome/browser/ui/browser.h"
@@ -35,19 +35,29 @@ index 30bd52d09c3fc..2b0bba666f4ae 100644
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/devtools_agent_host.h"
@@ -34,6 +47,11 @@ using protocol::Response;
@@ -30,10 +43,21 @@
#include "ui/gfx/image/image.h"
#include "ui/gfx/image/image_png_rep.h"
+#if BUILDFLAG(IS_MAC)
+#include "chrome/browser/devtools/protocol/browser_handler_mac.h"
+#endif
+
using protocol::Response;
namespace {
+#if !BUILDFLAG(IS_MAC)
+// Off-screen position used to hide windows while keeping their compositors
+// active. This enables CDP operations like Page.captureScreenshot on hidden
+// windows. Uses cross-platform ui::BaseWindow::SetBounds/ShowInactive APIs.
+constexpr int kOffScreenPosition = -32000;
+#endif
+
BrowserWindow* GetBrowserWindow(int window_id) {
BrowserWindow* result = nullptr;
ForEachCurrentBrowserWindowInterfaceOrderedByActivation(
@@ -72,11 +90,405 @@ std::unique_ptr<protocol::Browser::Bounds> GetBrowserWindowBounds(
@@ -72,17 +96,398 @@ std::unique_ptr<protocol::Browser::Bounds> GetBrowserWindowBounds(
.Build();
}
@@ -200,7 +210,7 @@ index 30bd52d09c3fc..2b0bba666f4ae 100644
+
+Response ResolveTabIdentifier(std::optional<std::string> target_id,
+ std::optional<int> tab_id,
+ HiddenTabManager* hidden_manager,
+ const base::flat_set<int>& hidden_window_ids,
+ TabLookupResult* result) {
+ if (target_id.has_value() && tab_id.has_value()) {
+ return Response::InvalidParams(
@@ -219,15 +229,6 @@ index 30bd52d09c3fc..2b0bba666f4ae 100644
+ if (!wc)
+ return Response::ServerError("No web contents in the target");
+
+ if (hidden_manager) {
+ SessionID sid = sessions::SessionTabHelper::IdForTab(wc);
+ if (sid.is_valid() && hidden_manager->IsHidden(sid.id())) {
+ result->web_contents = wc;
+ result->is_hidden = true;
+ return Response::Success();
+ }
+ }
+
+ BrowserWindowInterface* found_bwi = nullptr;
+ int found_index = -1;
+ ForEachCurrentBrowserWindowInterfaceOrderedByActivation(
@@ -248,21 +249,14 @@ index 30bd52d09c3fc..2b0bba666f4ae 100644
+ result->web_contents = wc;
+ result->bwi = found_bwi;
+ result->tab_index = found_index;
+ result->is_hidden =
+ hidden_window_ids.contains(found_bwi->GetSessionID().id());
+ return Response::Success();
+ }
+
+ // tab_id provided
+ int tid = tab_id.value();
+
+ if (hidden_manager) {
+ content::WebContents* hidden_wc = hidden_manager->FindByTabId(tid);
+ if (hidden_wc) {
+ result->web_contents = hidden_wc;
+ result->is_hidden = true;
+ return Response::Success();
+ }
+ }
+
+ BrowserWindowInterface* found_bwi = nullptr;
+ content::WebContents* found_wc = nullptr;
+ int found_index = -1;
@@ -289,6 +283,8 @@ index 30bd52d09c3fc..2b0bba666f4ae 100644
+ result->web_contents = found_wc;
+ result->bwi = found_bwi;
+ result->tab_index = found_index;
+ result->is_hidden =
+ hidden_window_ids.contains(found_bwi->GetSessionID().id());
+ return Response::Success();
+}
+
@@ -448,13 +444,21 @@ index 30bd52d09c3fc..2b0bba666f4ae 100644
BrowserHandler::BrowserHandler(protocol::UberDispatcher* dispatcher,
const std::string& target_id)
- : target_id_(target_id) {
+ : target_id_(target_id),
+ hidden_tab_manager_(std::make_unique<HiddenTabManager>()) {
// Dispatcher can be null in tests.
: target_id_(target_id) {
- // Dispatcher can be null in tests.
if (dispatcher)
protocol::Browser::Dispatcher::wire(dispatcher, this);
@@ -120,6 +532,65 @@ Response BrowserHandler::GetWindowForTarget(
}
-BrowserHandler::~BrowserHandler() = default;
+BrowserHandler::~BrowserHandler() {
+ hidden_window_per_profile_.clear();
+ hidden_window_ids_.clear();
+}
Response BrowserHandler::GetWindowForTarget(
std::optional<std::string> target_id,
@@ -120,6 +525,65 @@ Response BrowserHandler::GetWindowForTarget(
return Response::Success();
}
@@ -520,7 +524,7 @@ index 30bd52d09c3fc..2b0bba666f4ae 100644
Response BrowserHandler::GetWindowBounds(
int window_id,
std::unique_ptr<protocol::Browser::Bounds>* out_bounds) {
@@ -297,3 +768,828 @@ protocol::Response BrowserHandler::AddPrivacySandboxEnrollmentOverride(
@@ -297,3 +761,901 @@ protocol::Response BrowserHandler::AddPrivacySandboxEnrollmentOverride(
net::SchemefulSite(url_to_add));
return Response::Success();
}
@@ -589,17 +593,7 @@ index 30bd52d09c3fc..2b0bba666f4ae 100644
+ chrome::AddTabAt(browser, navigate_url, -1, true);
+
+ if (hidden.value_or(false)) {
+ // Move the window off-screen, then show it without stealing focus.
+ // This keeps the compositor active (so CDP operations like
+ // Page.captureScreenshot work) while making the window invisible.
+ // Widget::Hide() cannot be used because it disconnects the compositor
+ // at the platform level on all OSes.
+ gfx::Rect offscreen_bounds = browser->window()->GetBounds();
+ offscreen_bounds.set_origin(
+ gfx::Point(kOffScreenPosition, kOffScreenPosition));
+ browser->window()->SetBounds(offscreen_bounds);
+ browser->window()->ShowInactive();
+ hidden_window_ids_.insert(browser->session_id().id());
+ MakeWindowHidden(browser);
+ } else {
+ browser->window()->Show();
+ }
@@ -620,6 +614,15 @@ index 30bd52d09c3fc..2b0bba666f4ae 100644
+ return Response::ServerError("Browser window not found");
+ }
+ hidden_window_ids_.erase(window_id);
+ // Clean up hidden_window_per_profile_ if this was a hidden window.
+ Browser* browser = bwi->GetBrowserForMigrationOnly();
+ for (auto it = hidden_window_per_profile_.begin();
+ it != hidden_window_per_profile_.end(); ++it) {
+ if (it->second == browser) {
+ hidden_window_per_profile_.erase(it);
+ break;
+ }
+ }
+ bwi->GetTabStripModel()->CloseAllTabs();
+ bwi->GetWindow()->Close();
+ return Response::Success();
@@ -640,10 +643,7 @@ index 30bd52d09c3fc..2b0bba666f4ae 100644
+ return Response::ServerError("Browser window not found");
+ }
+ if (hidden_window_ids_.contains(window_id)) {
+ gfx::Rect bounds = bwi->GetWindow()->GetBounds();
+ bounds.set_origin(gfx::Point(100, 100));
+ bwi->GetWindow()->SetBounds(bounds);
+ hidden_window_ids_.erase(window_id);
+ MakeWindowVisible(bwi);
+ }
+ bwi->GetWindow()->Show();
+ return Response::Success();
@@ -654,11 +654,8 @@ index 30bd52d09c3fc..2b0bba666f4ae 100644
+ if (!bwi) {
+ return Response::ServerError("Browser window not found");
+ }
+ gfx::Rect offscreen_bounds = bwi->GetWindow()->GetBounds();
+ offscreen_bounds.set_origin(
+ gfx::Point(kOffScreenPosition, kOffScreenPosition));
+ bwi->GetWindow()->SetBounds(offscreen_bounds);
+ hidden_window_ids_.insert(window_id);
+ Browser* browser = bwi->GetBrowserForMigrationOnly();
+ MakeWindowHidden(browser);
+ return Response::Success();
+}
+
@@ -677,14 +674,20 @@ index 30bd52d09c3fc..2b0bba666f4ae 100644
+ if (!bwi) {
+ return Response::ServerError("Browser window not found");
+ }
+ bool is_hidden = hidden_window_ids_.contains(bwi->GetSessionID().id());
+ TabStripModel* tab_strip = bwi->GetTabStripModel();
+ for (int i = 0; i < tab_strip->count(); ++i) {
+ tabs->push_back(
+ BuildTabInfo(tab_strip->GetWebContentsAt(i), bwi, i, false));
+ BuildTabInfo(tab_strip->GetWebContentsAt(i), bwi, i, is_hidden));
+ }
+ } else {
+ ForEachCurrentBrowserWindowInterfaceOrderedByActivation(
+ [&tabs](BrowserWindowInterface* bwi) {
+ [&tabs, this](BrowserWindowInterface* bwi) {
+ bool is_hidden =
+ hidden_window_ids_.contains(bwi->GetSessionID().id());
+ if (is_hidden) {
+ return true;
+ }
+ TabStripModel* tab_strip = bwi->GetTabStripModel();
+ for (int i = 0; i < tab_strip->count(); ++i) {
+ tabs->push_back(
@@ -695,9 +698,18 @@ index 30bd52d09c3fc..2b0bba666f4ae 100644
+ }
+
+ if (include_hidden.value_or(false)) {
+ for (const auto& wc : hidden_tab_manager_->hidden_tabs()) {
+ tabs->push_back(BuildTabInfo(wc.get(), nullptr, -1, true));
+ }
+ ForEachCurrentBrowserWindowInterfaceOrderedByActivation(
+ [&tabs, this](BrowserWindowInterface* bwi) {
+ if (!hidden_window_ids_.contains(bwi->GetSessionID().id())) {
+ return true;
+ }
+ TabStripModel* tab_strip = bwi->GetTabStripModel();
+ for (int i = 0; i < tab_strip->count(); ++i) {
+ tabs->push_back(
+ BuildTabInfo(tab_strip->GetWebContentsAt(i), bwi, i, true));
+ }
+ return true;
+ });
+ }
+
+ *out_tabs = std::move(tabs);
@@ -734,7 +746,7 @@ index 30bd52d09c3fc..2b0bba666f4ae 100644
+ std::unique_ptr<protocol::Browser::TabInfo>* out_tab) {
+ TabLookupResult lookup;
+ Response response = ResolveTabIdentifier(target_id, tab_id,
+ hidden_tab_manager_.get(), &lookup);
+ hidden_window_ids_, &lookup);
+ if (!response.IsSuccess())
+ return response;
+
@@ -755,10 +767,6 @@ index 30bd52d09c3fc..2b0bba666f4ae 100644
+ bool is_hidden = hidden.value_or(false);
+
+ if (is_hidden) {
+ if (window_id.has_value()) {
+ return Response::InvalidParams(
+ "Cannot specify windowId for hidden tabs");
+ }
+ if (pinned.value_or(false)) {
+ return Response::InvalidParams("Cannot pin a hidden tab");
+ }
@@ -773,15 +781,24 @@ index 30bd52d09c3fc..2b0bba666f4ae 100644
+ return Response::ServerError("No profile available");
+ }
+
+ Browser* hidden_browser = GetOrCreateHiddenWindow(profile);
+ if (!hidden_browser) {
+ return Response::ServerError("Failed to create hidden window for tab");
+ }
+
+ GURL navigate_url = url.has_value() ? GURL(url.value()) : GURL();
+ int tab_id =
+ hidden_tab_manager_->CreateHiddenTab(navigate_url, profile);
+ content::WebContents* wc = hidden_tab_manager_->FindByTabId(tab_id);
+ chrome::AddTabAt(hidden_browser, navigate_url, -1, false);
+
+ TabStripModel* tab_strip = hidden_browser->tab_strip_model();
+ int new_index = tab_strip->count() - 1;
+ content::WebContents* wc = tab_strip->GetWebContentsAt(new_index);
+ if (!wc) {
+ return Response::ServerError("Failed to create hidden tab");
+ }
+
+ *out_tab = BuildTabInfo(wc, nullptr, -1, true);
+ BrowserWindowInterface* bwi = GetBrowserWindowInterface(
+ hidden_browser->session_id().id());
+ *out_tab = BuildTabInfo(wc, bwi, new_index, true);
+ return Response::Success();
+ }
+
@@ -825,19 +842,10 @@ index 30bd52d09c3fc..2b0bba666f4ae 100644
+ std::optional<int> tab_id) {
+ TabLookupResult lookup;
+ Response response = ResolveTabIdentifier(target_id, tab_id,
+ hidden_tab_manager_.get(), &lookup);
+ hidden_window_ids_, &lookup);
+ if (!response.IsSuccess())
+ return response;
+
+ if (lookup.is_hidden) {
+ SessionID sid =
+ sessions::SessionTabHelper::IdForTab(lookup.web_contents);
+ if (sid.is_valid()) {
+ hidden_tab_manager_->DetachByTabId(sid.id());
+ }
+ return Response::Success();
+ }
+
+ TabStripModel* tab_strip = lookup.bwi->GetTabStripModel();
+ tab_strip->CloseWebContentsAt(lookup.tab_index,
+ TabCloseTypes::CLOSE_CREATE_HISTORICAL_TAB);
@@ -848,7 +856,7 @@ index 30bd52d09c3fc..2b0bba666f4ae 100644
+ std::optional<int> tab_id) {
+ TabLookupResult lookup;
+ Response response = ResolveTabIdentifier(target_id, tab_id,
+ hidden_tab_manager_.get(), &lookup);
+ hidden_window_ids_, &lookup);
+ if (!response.IsSuccess())
+ return response;
+
@@ -870,7 +878,7 @@ index 30bd52d09c3fc..2b0bba666f4ae 100644
+ std::unique_ptr<protocol::Browser::TabInfo>* out_tab) {
+ TabLookupResult lookup;
+ Response response = ResolveTabIdentifier(target_id, tab_id,
+ hidden_tab_manager_.get(), &lookup);
+ hidden_window_ids_, &lookup);
+ if (!response.IsSuccess())
+ return response;
+
@@ -929,7 +937,7 @@ index 30bd52d09c3fc..2b0bba666f4ae 100644
+ std::unique_ptr<protocol::Browser::TabInfo>* out_tab) {
+ TabLookupResult lookup;
+ Response response = ResolveTabIdentifier(target_id, tab_id,
+ hidden_tab_manager_.get(), &lookup);
+ hidden_window_ids_, &lookup);
+ if (!response.IsSuccess())
+ return response;
+
@@ -956,7 +964,7 @@ index 30bd52d09c3fc..2b0bba666f4ae 100644
+ std::unique_ptr<protocol::Browser::TabInfo>* out_tab) {
+ TabLookupResult lookup;
+ Response response = ResolveTabIdentifier(target_id, tab_id,
+ hidden_tab_manager_.get(), &lookup);
+ hidden_window_ids_, &lookup);
+ if (!response.IsSuccess())
+ return response;
+
@@ -977,7 +985,7 @@ index 30bd52d09c3fc..2b0bba666f4ae 100644
+ std::unique_ptr<protocol::Browser::TabInfo>* out_tab) {
+ TabLookupResult lookup;
+ Response response = ResolveTabIdentifier(target_id, tab_id,
+ hidden_tab_manager_.get(), &lookup);
+ hidden_window_ids_, &lookup);
+ if (!response.IsSuccess())
+ return response;
+
@@ -1001,7 +1009,7 @@ index 30bd52d09c3fc..2b0bba666f4ae 100644
+ std::unique_ptr<protocol::Browser::TabInfo>* out_tab) {
+ TabLookupResult lookup;
+ Response response = ResolveTabIdentifier(target_id, tab_id,
+ hidden_tab_manager_.get(), &lookup);
+ hidden_window_ids_, &lookup);
+ if (!response.IsSuccess())
+ return response;
+
@@ -1009,32 +1017,37 @@ index 30bd52d09c3fc..2b0bba666f4ae 100644
+ return Response::InvalidParams("Tab is not hidden");
+ }
+
+ SessionID sid =
+ sessions::SessionTabHelper::IdForTab(lookup.web_contents);
+ if (!sid.is_valid()) {
+ return Response::ServerError("Hidden tab has invalid tab id");
+ }
+
+ // Detach from the hidden window.
+ TabStripModel* source_strip = lookup.bwi->GetTabStripModel();
+ std::unique_ptr<content::WebContents> detached =
+ hidden_tab_manager_->DetachByTabId(sid.id());
+ source_strip->DetachWebContentsAtForInsertion(lookup.tab_index);
+ if (!detached) {
+ return Response::ServerError("Failed to detach hidden tab");
+ }
+
+ // Find target visible window.
+ BrowserWindowInterface* target_bwi = nullptr;
+ if (window_id.has_value()) {
+ target_bwi = GetBrowserWindowInterface(window_id.value());
+ if (!target_bwi) {
+ // Put it back if the window wasn't found.
+ hidden_tab_manager_->TakeWebContents(std::move(detached));
+ // Put it back on the hidden window.
+ source_strip->InsertWebContentsAt(-1, std::move(detached),
+ AddTabTypes::ADD_NONE);
+ return Response::ServerError("Browser window not found");
+ }
+ } else {
+ target_bwi = GetLastActiveBrowserWindowInterfaceWithAnyProfile();
+ // Find last active non-hidden window.
+ ForEachCurrentBrowserWindowInterfaceOrderedByActivation(
+ [this, &target_bwi](BrowserWindowInterface* bwi) {
+ if (!hidden_window_ids_.contains(bwi->GetSessionID().id())) {
+ target_bwi = bwi;
+ return false;
+ }
+ return true;
+ });
+ }
+
+ if (!target_bwi) {
+ // No windows exist — create one.
+ Profile* profile =
+ Profile::FromBrowserContext(detached->GetBrowserContext());
+ Browser::CreateParams params(
@@ -1043,7 +1056,8 @@ index 30bd52d09c3fc..2b0bba666f4ae 100644
+ browser->window()->Show();
+ target_bwi = GetBrowserWindowInterface(browser->session_id().id());
+ if (!target_bwi) {
+ hidden_tab_manager_->TakeWebContents(std::move(detached));
+ source_strip->InsertWebContentsAt(-1, std::move(detached),
+ AddTabTypes::ADD_NONE);
+ return Response::ServerError("Failed to create window for tab");
+ }
+ }
@@ -1069,7 +1083,7 @@ index 30bd52d09c3fc..2b0bba666f4ae 100644
+ std::unique_ptr<protocol::Browser::TabInfo>* out_tab) {
+ TabLookupResult lookup;
+ Response response = ResolveTabIdentifier(target_id, tab_id,
+ hidden_tab_manager_.get(), &lookup);
+ hidden_window_ids_, &lookup);
+ if (!response.IsSuccess())
+ return response;
+
@@ -1077,14 +1091,28 @@ index 30bd52d09c3fc..2b0bba666f4ae 100644
+ return Response::InvalidParams("Tab is already hidden");
+ }
+
+ TabStripModel* tab_strip = lookup.bwi->GetTabStripModel();
+ // Detach from visible window.
+ TabStripModel* source_strip = lookup.bwi->GetTabStripModel();
+ std::unique_ptr<content::WebContents> detached =
+ tab_strip->DetachWebContentsAtForInsertion(lookup.tab_index);
+ source_strip->DetachWebContentsAtForInsertion(lookup.tab_index);
+ if (!detached) {
+ return Response::ServerError("Failed to detach tab");
+ }
+
+ // Insert into hidden window.
+ Profile* profile =
+ Profile::FromBrowserContext(detached->GetBrowserContext());
+ Browser* hidden_browser = GetOrCreateHiddenWindow(profile);
+
+ content::WebContents* raw_wc = detached.get();
+ hidden_tab_manager_->TakeWebContents(std::move(detached));
+ hidden_browser->tab_strip_model()->InsertWebContentsAt(
+ -1, std::move(detached), AddTabTypes::ADD_NONE);
+
+ *out_tab = BuildTabInfo(raw_wc, nullptr, -1, true);
+ BrowserWindowInterface* hidden_bwi = GetBrowserWindowInterface(
+ hidden_browser->session_id().id());
+ int new_index =
+ hidden_browser->tab_strip_model()->GetIndexOfWebContents(raw_wc);
+ *out_tab = BuildTabInfo(raw_wc, hidden_bwi, new_index, true);
+ return Response::Success();
+}
+
@@ -1349,3 +1377,52 @@ index 30bd52d09c3fc..2b0bba666f4ae 100644
+ return Response::Success();
+}
+
+// --- Hidden Window Helpers ---
+
+Browser* BrowserHandler::GetOrCreateHiddenWindow(Profile* profile) {
+ auto it = hidden_window_per_profile_.find(profile);
+ if (it != hidden_window_per_profile_.end()) {
+ return it->second;
+ }
+
+ Browser::CreateParams params(Browser::TYPE_NORMAL, profile, true);
+ Browser* browser = Browser::Create(params);
+
+ // Add a blank tab so ShowInactive has content to composite.
+ chrome::AddTabAt(browser, GURL(), -1, false);
+ MakeWindowHidden(browser);
+
+ hidden_window_per_profile_[profile] = browser;
+ return browser;
+}
+
+void BrowserHandler::MakeWindowHidden(Browser* browser) {
+#if BUILDFLAG(IS_MAC)
+ SetWindowHeadless(browser->window(), true);
+ browser->window()->ShowInactive();
+#else
+ gfx::Rect offscreen_bounds = browser->window()->GetBounds();
+ offscreen_bounds.set_origin(
+ gfx::Point(kOffScreenPosition, kOffScreenPosition));
+ browser->window()->SetBounds(offscreen_bounds);
+ browser->window()->ShowInactive();
+#endif
+ hidden_window_ids_.insert(browser->session_id().id());
+}
+
+void BrowserHandler::MakeWindowVisible(BrowserWindowInterface* bwi) {
+#if BUILDFLAG(IS_MAC)
+ Browser* browser = bwi->GetBrowserForMigrationOnly();
+ SetWindowHeadless(browser->window(), false);
+#else
+ gfx::Rect bounds = bwi->GetWindow()->GetBounds();
+ bounds.set_origin(gfx::Point(100, 100));
+ bwi->GetWindow()->SetBounds(bounds);
+#endif
+ hidden_window_ids_.erase(bwi->GetSessionID().id());
+}
+
+bool BrowserHandler::IsHiddenWindow(int window_id) const {
+ return hidden_window_ids_.contains(window_id);
+}
+

View File

@@ -1,22 +1,26 @@
diff --git a/chrome/browser/devtools/protocol/browser_handler.h b/chrome/browser/devtools/protocol/browser_handler.h
index e1424aa52cbf6..2b8da4a31db41 100644
index e1424aa52cbf6..ffd1c86c5aed9 100644
--- a/chrome/browser/devtools/protocol/browser_handler.h
+++ b/chrome/browser/devtools/protocol/browser_handler.h
@@ -5,9 +5,13 @@
@@ -5,9 +5,17 @@
#ifndef CHROME_BROWSER_DEVTOOLS_PROTOCOL_BROWSER_HANDLER_H_
#define CHROME_BROWSER_DEVTOOLS_PROTOCOL_BROWSER_HANDLER_H_
+#include <memory>
+
+#include "base/containers/flat_map.h"
#include "base/containers/flat_set.h"
+#include "base/memory/raw_ptr.h"
#include "chrome/browser/devtools/protocol/browser.h"
+class HiddenTabManager;
+class Browser;
+class BrowserWindowInterface;
+class Profile;
+
class BrowserHandler : public protocol::Browser::Backend {
public:
BrowserHandler(protocol::UberDispatcher* dispatcher,
@@ -23,6 +27,14 @@ class BrowserHandler : public protocol::Browser::Backend {
@@ -23,6 +31,14 @@ class BrowserHandler : public protocol::Browser::Backend {
std::optional<std::string> target_id,
int* out_window_id,
std::unique_ptr<protocol::Browser::Bounds>* out_bounds) override;
@@ -31,7 +35,7 @@ index e1424aa52cbf6..2b8da4a31db41 100644
protocol::Response GetWindowBounds(
int window_id,
std::unique_ptr<protocol::Browser::Bounds>* out_bounds) override;
@@ -41,9 +53,115 @@ class BrowserHandler : public protocol::Browser::Backend {
@@ -41,9 +57,118 @@ class BrowserHandler : public protocol::Browser::Backend {
protocol::Response AddPrivacySandboxEnrollmentOverride(
const std::string& in_url) override;
@@ -138,12 +142,15 @@ index e1424aa52cbf6..2b8da4a31db41 100644
+ std::unique_ptr<protocol::Browser::TabGroupInfo>* out_group) override;
+
private:
+ Browser* GetOrCreateHiddenWindow(Profile* profile);
+ void MakeWindowHidden(Browser* browser);
+ void MakeWindowVisible(BrowserWindowInterface* bwi);
+ bool IsHiddenWindow(int window_id) const;
+
base::flat_set<std::string> contexts_with_overridden_permissions_;
std::string target_id_;
+ std::unique_ptr<HiddenTabManager> hidden_tab_manager_;
+ // Window IDs that are positioned off-screen to appear "hidden" while
+ // keeping their compositors active for CDP operations.
+ base::flat_set<int> hidden_window_ids_;
+ base::flat_map<raw_ptr<Profile>, raw_ptr<Browser>> hidden_window_per_profile_;
};
#endif // CHROME_BROWSER_DEVTOOLS_PROTOCOL_BROWSER_HANDLER_H_

View File

@@ -0,0 +1,22 @@
diff --git a/chrome/browser/devtools/protocol/browser_handler_mac.h b/chrome/browser/devtools/protocol/browser_handler_mac.h
new file mode 100644
index 0000000000000..52e1b27fbfb0a
--- /dev/null
+++ b/chrome/browser/devtools/protocol/browser_handler_mac.h
@@ -0,0 +1,16 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_DEVTOOLS_PROTOCOL_BROWSER_HANDLER_MAC_H_
+#define CHROME_BROWSER_DEVTOOLS_PROTOCOL_BROWSER_HANDLER_MAC_H_
+
+class BrowserWindow;
+
+// Sets per-window headless mode on macOS. When headless, the window's
+// NSWindow swizzles AppKit methods to fake visibility while keeping the
+// compositor active. This bypasses constrainFrameRect:toScreen: which
+// otherwise clamps off-screen windows back on-screen.
+void SetWindowHeadless(BrowserWindow* window, bool headless);
+
+#endif // CHROME_BROWSER_DEVTOOLS_PROTOCOL_BROWSER_HANDLER_MAC_H_

View File

@@ -0,0 +1,25 @@
diff --git a/chrome/browser/devtools/protocol/browser_handler_mac.mm b/chrome/browser/devtools/protocol/browser_handler_mac.mm
new file mode 100644
index 0000000000000..cd806bae50afd
--- /dev/null
+++ b/chrome/browser/devtools/protocol/browser_handler_mac.mm
@@ -0,0 +1,19 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/devtools/protocol/browser_handler_mac.h"
+
+#import <Cocoa/Cocoa.h>
+
+#include "chrome/browser/ui/browser_window.h"
+#include "components/remote_cocoa/app_shim/native_widget_mac_nswindow.h"
+#include "ui/gfx/native_ui_types.h"
+
+void SetWindowHeadless(BrowserWindow* window, bool headless) {
+ NSWindow* ns_window = window->GetNativeWindow().GetNativeNSWindow();
+ if (auto* native_window =
+ base::apple::ObjCCast<NativeWidgetMacNSWindow>(ns_window)) {
+ [native_window setIsHeadless:headless];
+ }
+}

View File

@@ -1,104 +0,0 @@
diff --git a/chrome/browser/devtools/protocol/hidden_tab_manager.cc b/chrome/browser/devtools/protocol/hidden_tab_manager.cc
new file mode 100644
index 0000000000000..c6de538ee4d90
--- /dev/null
+++ b/chrome/browser/devtools/protocol/hidden_tab_manager.cc
@@ -0,0 +1,98 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/devtools/protocol/hidden_tab_manager.h"
+
+#include <algorithm>
+
+#include "chrome/browser/sessions/session_tab_helper_factory.h"
+#include "components/sessions/content/session_tab_helper.h"
+#include "content/public/browser/devtools_agent_host.h"
+#include "content/public/browser/navigation_controller.h"
+#include "content/public/browser/web_contents.h"
+#include "url/gurl.h"
+
+HiddenTabManager::HiddenTabManager() = default;
+
+HiddenTabManager::~HiddenTabManager() = default;
+
+int HiddenTabManager::CreateHiddenTab(
+ const GURL& url,
+ content::BrowserContext* browser_context) {
+ content::WebContents::CreateParams params(browser_context);
+ auto web_contents = content::WebContents::Create(params);
+ web_contents->SetDelegate(this);
+
+ CreateSessionServiceTabHelper(web_contents.get());
+ content::DevToolsAgentHost::GetOrCreateFor(web_contents.get());
+
+ if (!url.is_empty()) {
+ content::NavigationController::LoadURLParams load_params(url);
+ web_contents->GetController().LoadURLWithParams(load_params);
+ }
+
+ int tab_id =
+ sessions::SessionTabHelper::IdForTab(web_contents.get()).id();
+ hidden_web_contents_.push_back(std::move(web_contents));
+ return tab_id;
+}
+
+content::WebContents* HiddenTabManager::FindByTabId(int tab_id) {
+ for (const auto& wc : hidden_web_contents_) {
+ SessionID sid = sessions::SessionTabHelper::IdForTab(wc.get());
+ if (sid.is_valid() && sid.id() == tab_id) {
+ return wc.get();
+ }
+ }
+ return nullptr;
+}
+
+content::WebContents* HiddenTabManager::FindByTargetId(
+ const std::string& target_id) {
+ for (const auto& wc : hidden_web_contents_) {
+ scoped_refptr<content::DevToolsAgentHost> host =
+ content::DevToolsAgentHost::GetOrCreateFor(wc.get());
+ if (host && host->GetId() == target_id) {
+ return wc.get();
+ }
+ }
+ return nullptr;
+}
+
+bool HiddenTabManager::IsHidden(int tab_id) {
+ return FindByTabId(tab_id) != nullptr;
+}
+
+std::unique_ptr<content::WebContents> HiddenTabManager::DetachByTabId(
+ int tab_id) {
+ for (auto it = hidden_web_contents_.begin();
+ it != hidden_web_contents_.end(); ++it) {
+ SessionID sid = sessions::SessionTabHelper::IdForTab(it->get());
+ if (sid.is_valid() && sid.id() == tab_id) {
+ std::unique_ptr<content::WebContents> result = std::move(*it);
+ hidden_web_contents_.erase(it);
+ return result;
+ }
+ }
+ return nullptr;
+}
+
+void HiddenTabManager::TakeWebContents(
+ std::unique_ptr<content::WebContents> web_contents) {
+ web_contents->SetDelegate(this);
+ hidden_web_contents_.push_back(std::move(web_contents));
+}
+
+void HiddenTabManager::Clear() {
+ hidden_web_contents_.clear();
+}
+
+void HiddenTabManager::CloseContents(content::WebContents* source) {
+ auto it = std::find_if(
+ hidden_web_contents_.begin(), hidden_web_contents_.end(),
+ [source](const auto& wc) { return wc.get() == source; });
+ if (it != hidden_web_contents_.end()) {
+ hidden_web_contents_.erase(it);
+ }
+}

View File

@@ -1,65 +0,0 @@
diff --git a/chrome/browser/devtools/protocol/hidden_tab_manager.h b/chrome/browser/devtools/protocol/hidden_tab_manager.h
new file mode 100644
index 0000000000000..52cb68e094c67
--- /dev/null
+++ b/chrome/browser/devtools/protocol/hidden_tab_manager.h
@@ -0,0 +1,59 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_DEVTOOLS_PROTOCOL_HIDDEN_TAB_MANAGER_H_
+#define CHROME_BROWSER_DEVTOOLS_PROTOCOL_HIDDEN_TAB_MANAGER_H_
+
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "content/public/browser/web_contents_delegate.h"
+
+namespace content {
+class BrowserContext;
+class WebContents;
+} // namespace content
+
+class GURL;
+
+// Manages hidden tab WebContents that are not attached to any browser window.
+// Hidden tabs are first-class CDP targets and are destroyed on CDP disconnect.
+class HiddenTabManager : public content::WebContentsDelegate {
+ public:
+ HiddenTabManager();
+ ~HiddenTabManager() override;
+ HiddenTabManager(const HiddenTabManager&) = delete;
+ HiddenTabManager& operator=(const HiddenTabManager&) = delete;
+
+ // Create a hidden tab, optionally navigated to |url|. Returns the tab ID.
+ int CreateHiddenTab(const GURL& url,
+ content::BrowserContext* browser_context);
+
+ content::WebContents* FindByTabId(int tab_id);
+ content::WebContents* FindByTargetId(const std::string& target_id);
+ bool IsHidden(int tab_id);
+
+ // Remove a hidden tab from management, returning ownership.
+ std::unique_ptr<content::WebContents> DetachByTabId(int tab_id);
+
+ // Take ownership of a WebContents (used by hideTab).
+ void TakeWebContents(std::unique_ptr<content::WebContents> web_contents);
+
+ // Destroy all hidden tabs (called on session disconnect).
+ void Clear();
+
+ const std::vector<std::unique_ptr<content::WebContents>>& hidden_tabs()
+ const {
+ return hidden_web_contents_;
+ }
+
+ // content::WebContentsDelegate:
+ void CloseContents(content::WebContents* source) override;
+
+ private:
+ std::vector<std::unique_ptr<content::WebContents>> hidden_web_contents_;
+};
+
+#endif // CHROME_BROWSER_DEVTOOLS_PROTOCOL_HIDDEN_TAB_MANAGER_H_