diff --git a/DEPS b/DEPS
index 2f8a921..9bc5f324 100644
--- a/DEPS
+++ b/DEPS
@@ -244,7 +244,7 @@
   # luci-go CIPD package version.
   # Make sure the revision is uploaded by infra-packagers builder.
   # https://ci.chromium.org/p/infra-internal/g/infra-packagers/console
-  'luci_go': 'git_revision:6704cef9341f59c9c6b3b996345b28e737bbb69d',
+  'luci_go': 'git_revision:45d1c0a0168f06a2bbde9eca9a03087ed1da523e',
 
   # This can be overridden, e.g. with custom_vars, to build clang from HEAD
   # instead of downloading the prebuilt pinned revision.
@@ -295,7 +295,7 @@
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling V8
   # and whatever else without interference from each other.
-  'src_internal_revision': 'd177ab19a801ad5337736483002b4c7a2ca6c5c4',
+  'src_internal_revision': '2514606e5e8e4ce2af8ebbddaeff318b9a987568',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling Skia
   # and whatever else without interference from each other.
@@ -307,7 +307,7 @@
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling ANGLE
   # and whatever else without interference from each other.
-  'angle_revision': '481000fdb0ae63238e041236545c35715fcfb90f',
+  'angle_revision': 'bf837e01756fdd934c0f6adb17e5e379dde0f74a',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling SwiftShader
   # and whatever else without interference from each other.
@@ -319,7 +319,7 @@
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling BoringSSL
   # and whatever else without interference from each other.
-  'boringssl_revision': '136284f8548bc7fb43e99e7f69e03fab57168e8b',
+  'boringssl_revision': '2a514a51baebd5a232fc64f7b082f7a8b28cd29d',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling Fuchsia sdk
   # and whatever else without interference from each other.
@@ -371,7 +371,7 @@
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling catapult
   # and whatever else without interference from each other.
-  'catapult_revision': '77017288fa6b829b2d31aa8195799ea78e594128',
+  'catapult_revision': '83c00a37f40a93932204f684172a13c62a379e7e',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling CrossBench
   # and whatever else without interference from each other.
@@ -391,7 +391,7 @@
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling devtools-frontend
   # and whatever else without interference from each other.
-  'devtools_frontend_revision': '98f52748ef939c7af67e84f07dd27ceadf35f275',
+  'devtools_frontend_revision': '58221174205b00e188e181a3cbb1d63e05d23331',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling libprotobuf-mutator
   # and whatever else without interference from each other.
@@ -419,7 +419,7 @@
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling feed
   # and whatever else without interference from each other.
-  'quiche_revision': '41bba51b1ae0aa30ff7faf5ac5aa28feb1588afd',
+  'quiche_revision': 'bbe16c18d2fc0ea4bc261b20dbc28fe53e32bf13',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling ink
   # and whatever else without interference from each other.
@@ -495,11 +495,11 @@
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling feed
   # and whatever else without interference from each other.
-  'libcxxabi_revision':    '1efb5e6d7c5eee01624f3730a935285405c9cd22',
+  'libcxxabi_revision':    '5a49db9990ee2ecee2bc7340be9756c0455bde6f',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling feed
   # and whatever else without interference from each other.
-  'libunwind_revision':    '09f6f7b1685888b16c0fe52a7a68f9dff8b8d60e',
+  'libunwind_revision':    '2bd5f3cae13e8ad6727d0a77a2ec9cdc6b06becb',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling feed
   # and whatever else without interference from each other.
@@ -519,11 +519,11 @@
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling llvm-libc
   # and whatever else without interference from each other.
-  'llvm_libc_revision':    '4a2940b40b394ca57312aa9bbc8af430fe9a5340',
+  'llvm_libc_revision':    'a0ab545a0eaa7a8cd7154025d1902904e1800cfc',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling llvm-libc
   # and whatever else without interference from each other.
-  'compiler_rt_revision': '948a46574ce380c306ae4f01d2a630d93847b88f',
+  'compiler_rt_revision': 'b5f72d2ab6190b6cea33512880e6cbfe3648d513',
 
   # If you change this, also update the libc++ revision in
   # //buildtools/deps_revisions.gni.
@@ -1486,12 +1486,12 @@
 
   'src/clank': {
     'url': Var('chrome_git') + '/clank/internal/apps.git' + '@' +
-    '99be04a0cf568303f3f820a5e8de7d39287e6dfb',
+    '24dba4094427e89cdb21ff38df13ac54ce2bb238',
     'condition': 'checkout_android and checkout_src_internal',
   },
 
   'src/docs/website': {
-    'url': Var('chromium_git') + '/website.git' + '@' + '406e2b2832f3d734a2ba4f8fa93fb78deecb16d2',
+    'url': Var('chromium_git') + '/website.git' + '@' + 'f73045886d28209ea87d06c41eafcf521906486d',
   },
 
   'src/ios/third_party/earl_grey2/src': {
@@ -1645,7 +1645,7 @@
     'packages': [
       {
           'package': 'chromium/third_party/androidx',
-          'version': 'PAuqhSDBsIGL9f8sVxtknszgmnEYv3cgSAU7aUM1AjMC',
+          'version': '77JFcazE3nHDyZZ4RYND-IEyes3SQTICM2Kz231QD60C',
       },
     ],
     'condition': 'checkout_android and non_git_source',
@@ -1978,7 +1978,7 @@
 
 
   'src/third_party/depot_tools':
-    Var('chromium_git') + '/chromium/tools/depot_tools.git' + '@' + '7d18f854503a26cb29540012327ad3f78926de96',
+    Var('chromium_git') + '/chromium/tools/depot_tools.git' + '@' + '14bfda17088cb03d7bc0ba7df6cac2699e051e14',
 
   'src/third_party/devtools-frontend/src':
     Var('chromium_git') + '/devtools/devtools-frontend' + '@' + Var('devtools_frontend_revision'),
@@ -2526,7 +2526,7 @@
     Var('pdfium_git') + '/pdfium.git' + '@' +  Var('pdfium_revision'),
 
   'src/third_party/perfetto':
-    Var('chromium_git') + '/external/github.com/google/perfetto.git' + '@' + 'bff34086ee3abf81659b5184c243f4cf2f799992',
+    Var('chromium_git') + '/external/github.com/google/perfetto.git' + '@' + '4402fd7953d5dedf65659e99ab80404f6e94a004',
 
   'src/base/tracing/test/data': {
     'bucket': 'perfetto',
@@ -2840,7 +2840,7 @@
       'dep_type': 'cipd',
   },
 
-  'src/third_party/vulkan-deps': '{chromium_git}/vulkan-deps@6b14cce1d656d873db79be9c88dd5d343bd66f59',
+  'src/third_party/vulkan-deps': '{chromium_git}/vulkan-deps@cd7971b83f29d029d06895f39630cffa5ec421f1',
   'src/third_party/glslang/src': '{chromium_git}/external/github.com/KhronosGroup/glslang@963588074b26326ff0426c8953c1235213309bdb',
   'src/third_party/spirv-cross/src': '{chromium_git}/external/github.com/KhronosGroup/SPIRV-Cross@b8fcf307f1f347089e3c46eb4451d27f32ebc8d3',
   'src/third_party/spirv-headers/src': '{chromium_git}/external/github.com/KhronosGroup/SPIRV-Headers@6d0784e9f1ab92c17eeea94821b2465c14a52be9',
@@ -2849,7 +2849,7 @@
   'src/third_party/vulkan-loader/src': '{chromium_git}/external/github.com/KhronosGroup/Vulkan-Loader@a8bec310845ce80af5c00342243ae972cbe95e3b',
   'src/third_party/vulkan-tools/src': '{chromium_git}/external/github.com/KhronosGroup/Vulkan-Tools@60b640cb931814fcc6dabe4fc61f4738c56579f6',
   'src/third_party/vulkan-utility-libraries/src': '{chromium_git}/external/github.com/KhronosGroup/Vulkan-Utility-Libraries@4f628210460c4df62029959cc7fb237ac75f7189',
-  'src/third_party/vulkan-validation-layers/src': '{chromium_git}/external/github.com/KhronosGroup/Vulkan-ValidationLayers@21baa6bb2e0d5f4ae093397733f4534ea6e8cd6e',
+  'src/third_party/vulkan-validation-layers/src': '{chromium_git}/external/github.com/KhronosGroup/Vulkan-ValidationLayers@f74722ee649f9c9cc7daa7d13d434febc424e7a4',
 
   'src/third_party/vulkan_memory_allocator':
     Var('chromium_git') + '/external/github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator.git' + '@' + '56300b29fbfcc693ee6609ddad3fdd5b7a449a21',
@@ -2888,7 +2888,7 @@
     Var('chromium_git') + '/external/khronosgroup/webgl.git' + '@' + 'c01b768bce4a143e152c1870b6ba99ea6267d2b0',
 
   'src/third_party/webgpu-cts/src':
-    Var('chromium_git') + '/external/github.com/gpuweb/cts.git' + '@' + 'e25f9bec845320e76ac95e0cf57d2656f4dd5801',
+    Var('chromium_git') + '/external/github.com/gpuweb/cts.git' + '@' + '71e9fad6cf757c1d6ce2ec56c5201dd42eec0aa3',
 
   'src/third_party/webpagereplay':
     Var('chromium_git') + '/webpagereplay.git' + '@' + Var('webpagereplay_revision'),
@@ -2940,7 +2940,7 @@
       'packages': [
         {
           'package': 'skia/tools/goldctl/linux-amd64',
-          'version': 'XvA_7L7EBxuteX50DCEOF09vFmi3j5JjHZllbUAhrH0C',
+          'version': 'bTGNxI6oWidGO0rn_gOIbBQco8hNmWt0yS6ARBqSjlsC',
         },
       ],
       'dep_type': 'cipd',
@@ -2950,7 +2950,7 @@
       'packages': [
         {
           'package': 'skia/tools/goldctl/windows-amd64',
-          'version': 'sIE9gbPbfWS2wFcKfjLYZuqk2QbRwaQetYWH8FlaHWkC',
+          'version': 'Qp03cZtVqA0CskUhi8g8_bciB5Nz1CHp4lpn0cprJWgC',
         },
       ],
       'dep_type': 'cipd',
@@ -2961,7 +2961,7 @@
       'packages': [
         {
           'package': 'skia/tools/goldctl/mac-amd64',
-          'version': 'B4z1WT_C_-NfLkHKjxtinLyNC_b3B7r9N_OvGVWhrdsC',
+          'version': 'uq36HPTxtJXcSA6VRu488Anq19CjDz64IikQoKeegjIC',
         },
       ],
       'dep_type': 'cipd',
@@ -2972,7 +2972,7 @@
       'packages': [
         {
           'package': 'skia/tools/goldctl/mac-arm64',
-          'version': 'Nflk7_lewYF5S6u_5uI4QONIXSORkjmS_JxwgEX9mFEC',
+          'version': 'BpQkTzfhHyLozwZTFhy8nvc_t9VRDDXcaqdjQLA56bcC',
         },
       ],
       'dep_type': 'cipd',
@@ -3005,7 +3005,7 @@
     'packages': [
       {
         'package': 'chromeos_internal/apps/eche_app/app',
-        'version': 'KjLzfew2bv7towaDgJRXs5uh4HCfRCuabHB8JUr158IC',
+        'version': 'GnQgtoHVaf4bQUm9TxPaGiRJRQHjDcl8qQyO-kwu_MIC',
       },
     ],
     'condition': 'checkout_chromeos and checkout_src_internal',
@@ -4614,7 +4614,7 @@
 
   'src/components/optimization_guide/internal': {
       'url': Var('chrome_git') + '/chrome/components/optimization_guide.git' + '@' +
-        '03d56c66293bf2113bb997d537a2749d2dd7da14',
+        'c69f1d7a8ad0ed4c22595719e1d78b7bb8754516',
       'condition': 'checkout_src_internal',
   },
 
diff --git a/WATCHLISTS b/WATCHLISTS
index 9320ab22..2a318242 100644
--- a/WATCHLISTS
+++ b/WATCHLISTS
@@ -978,8 +978,12 @@
     },
     'core_web_vitals_wpt': {
       'filepath': 'third_party/blink/web_tests/external/wpt/event-timing/|' \
+                  'third_party/blink/web_tests/wpt_internal/event-timing/|' \
                   'third_party/blink/web_tests/external/wpt/largest-contentful-paint/|' \
-                  'third_party/blink/web_tests/external/wpt/layout-instability/',
+                  'third_party/blink/web_tests/external/wpt/layout-instability/|' \
+                  'third_party/blink/web_tests/wpt_internal/layout-instability/|' \
+		  'third_party/blink/web_tests/external/wpt/soft-navigation-heuristics/|' \
+		  'third_party/blink/web_tests/wpt_internal/soft-navigation-heuristics/',
     },
     'courgette': {
       'filepath': 'courgette/',
@@ -2874,8 +2878,10 @@
     'contextual_search': ['donnd+watch@chromium.org',
                           'twellington+watch@chromium.org',
                           'gangwu+watch@chromium.org'],
-    'core_timing': ['core-timing-reviews@chromium.org'],
-    'core_web_vitals_plm': ['core-web-vitals-plm-reviews@chromium.org'],
+    'core_timing': ['chrome-speed-metrics-core+watchlist@google.com',
+                    'core-timing-reviews@chromium.org'],
+    'core_web_vitals_plm': ['chrome-speed-metrics-core+watchlist@google.com',
+                            'core-web-vitals-plm-reviews@chromium.org'],
     'core_web_vitals_wpt': ['chrome-speed-metrics-core+watchlist@google.com',
                             'lighthouse-eng-external+watch-speed-metrics@google.com'],
     'courgette': ['huangs+watch@chromium.org',
@@ -3272,7 +3278,8 @@
                   'jasonrhee+watch-smartlock@google.com'],
     'smb': ['cros-enterprise-lax+smbwatch@chromium.org'],
     'source_idls': ['jmedley+watch@chromium.org'],
-    'speed_metrics_changelog': ['igrigorik@chromium.org',
+    'speed_metrics_changelog': ['chrome-speed-metrics-core+watchlist@google.com',
+                                'igrigorik@chromium.org',
                                 'kayce@chromium.org',
                                 'lighthouse-eng-external+watch-speed-metrics@google.com',
                                 'lighthouse-eng+watch-speed-metrics@google.com',
diff --git a/ash/fast_ink/fast_ink_host.cc b/ash/fast_ink/fast_ink_host.cc
index ad6ec939..5ac763c 100644
--- a/ash/fast_ink/fast_ink_host.cc
+++ b/ash/fast_ink/fast_ink_host.cc
@@ -100,18 +100,15 @@
 }
 
 void FastInkHost::OnFirstFrameRequested() {
-  // Only create a buffer if not initialized as `OnFirstFrameRequested()` is
-  // called for every first begin frame for a FrameSink.
-  if (!client_shared_image_) {
-    InitializeFastInkBuffer(host_window());
-  }
+  CHECK(!client_shared_image_);
+  InitializeFastInkBuffer(host_window());
 }
 
 void FastInkHost::OnFrameSinkLost() {
-  // The fast ink buffer becomes unusable and a new buffer must be created when
-  // the GPU crashes, which is one of the most common causes of FrameSink loss.
+  // The fast ink buffer becomes unusable the GPU crashes, which is one of the
+  // most common causes of FrameSink loss. A new buffer will be created once
+  // `OnFirstFrameRequested()` will be called.
   ResetGpuBuffer();
-  InitializeFastInkBuffer(host_window());
   FrameSinkHost::OnFrameSinkLost();
 }
 
diff --git a/ash/fast_ink/fast_ink_host_unittest.cc b/ash/fast_ink/fast_ink_host_unittest.cc
index a89ac1b..c289537 100644
--- a/ash/fast_ink/fast_ink_host_unittest.cc
+++ b/ash/fast_ink/fast_ink_host_unittest.cc
@@ -136,7 +136,6 @@
 
   // MappableSI should be initialized after receiving the first begin frame.
   ASSERT_TRUE(fast_ink_host_test.client_shared_image());
-  auto sync_token = fast_ink_host_test.sync_token();
 
   // A new frame-sink will be created. FastInkHost should also create a new
   // shared image.
@@ -144,16 +143,15 @@
       ->frame_sink_holder_for_testing()
       ->DidLoseLayerTreeFrameSink();
 
-  EXPECT_NE(sync_token, fast_ink_host_test.sync_token());
-  sync_token = fast_ink_host_test.sync_token();
+  // MappableSI should be destroyed after losing a frame sink.
+  EXPECT_FALSE(fast_ink_host_test.client_shared_image());
 
-  // This will be the first OnBeginFrame for the new frame sink, therefore
-  // `FrameSinkHost::OnFirstFrameRequested()` will be called again.
+  // A new MappableSI should be initialized once
+  // `FrameSinkHost::OnFirstFrameRequested()` is called for the new
+  // FrameSinkHolder.
   OnBeginFrame();
 
-  // Ensure we do not recreate a shared image on
-  // `FrameSinkHost::OnFirstFrameRequested()`.
-  EXPECT_EQ(sync_token, fast_ink_host_test.sync_token());
+  EXPECT_TRUE(fast_ink_host_test.client_shared_image());
 }
 
 TEST_P(FastInkHostTest, DelayPaintingUntilReceivingFirstBeginFrame) {
diff --git a/ash/system/ime_menu/ime_list_view.cc b/ash/system/ime_menu/ime_list_view.cc
index 5b1cdeb..88b1293f3 100644
--- a/ash/system/ime_menu/ime_list_view.cc
+++ b/ash/system/ime_menu/ime_list_view.cc
@@ -280,13 +280,6 @@
   }
 }
 
-void ImeListView::CloseImeListView() {
-  last_selected_item_id_.clear();
-  current_ime_view_ = nullptr;
-  last_item_selected_with_keyboard_ = false;
-  GetWidget()->Close();
-}
-
 void ImeListView::AppendImeListAndProperties(
     const std::string& current_ime_id,
     const std::vector<ImeInfo>& list,
@@ -369,11 +362,6 @@
     last_selected_item_id_ = key;
     ime_controller->ActivateImeMenuItem(key);
   }
-
-  if (!should_focus_ime_after_selection_with_keyboard_ ||
-      !last_item_selected_with_keyboard_) {
-    CloseImeListView();
-  }
 }
 
 void ImeListView::VisibilityChanged(View* starting_from, bool is_visible) {
diff --git a/ash/system/ime_menu/ime_list_view.h b/ash/system/ime_menu/ime_list_view.h
index 75bc6ee..ebffbfa 100644
--- a/ash/system/ime_menu/ime_list_view.h
+++ b/ash/system/ime_menu/ime_list_view.h
@@ -52,9 +52,6 @@
   // Removes (and destroys) all child views.
   virtual void ResetImeListView();
 
-  // Closes the view.
-  void CloseImeListView();
-
   // Scrolls contents such that |item_view| is visible.
   void ScrollItemToVisible(views::View* item_view);
 
diff --git a/ash/system/mahi/mahi_nudge_controller_unittest.cc b/ash/system/mahi/mahi_nudge_controller_unittest.cc
index f9d41563..c04e944 100644
--- a/ash/system/mahi/mahi_nudge_controller_unittest.cc
+++ b/ash/system/mahi/mahi_nudge_controller_unittest.cc
@@ -28,7 +28,7 @@
 // A class that mocks `MagicBoostState` to use in tests.
 class TestMagicBoostState : public chromeos::MagicBoostState {
  public:
-  TestMagicBoostState() = default;
+  TestMagicBoostState() { UpdateMagicBoostAvailable(true); }
 
   TestMagicBoostState(const TestMagicBoostState&) = delete;
   TestMagicBoostState& operator=(const TestMagicBoostState&) = delete;
@@ -46,7 +46,6 @@
   }
 
   bool ShouldIncludeOrcaInOptInSync() override { return false; }
-  bool IsMagicBoostAvailable() override { return true; }
   bool CanShowNoticeBannerForHMR() override { return false; }
   int32_t AsyncIncrementHMRConsentWindowDismissCount() override { return 0; }
   void DisableOrcaFeature() override {}
diff --git a/ash/webui/boca_ui/boca_app.gni b/ash/webui/boca_ui/boca_app.gni
index 4114882..1d9cdaa3 100644
--- a/ash/webui/boca_ui/boca_app.gni
+++ b/ash/webui/boca_ui/boca_app.gni
@@ -4,8 +4,10 @@
 
 import("//build/config/chrome_build.gni")
 
+assert(is_chromeos)
+
 declare_args() {
   # Whether to enable the "real" ChromeOS Boca App. When false, a mock app is
   # bundled for testing integration points.
-  enable_cros_boca_app = is_chromeos && is_chrome_branded
+  enable_cros_boca_app = is_chrome_branded
 }
diff --git a/ash/webui/eche_app_ui/eche_app_ui.gni b/ash/webui/eche_app_ui/eche_app_ui.gni
index 076fe219..2be459ac 100644
--- a/ash/webui/eche_app_ui/eche_app_ui.gni
+++ b/ash/webui/eche_app_ui/eche_app_ui.gni
@@ -1,7 +1,13 @@
+# Copyright 2025 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
 import("//build/config/chrome_build.gni")
 
+assert(is_chromeos)
+
 declare_args() {
   # Whether to enable the "real" ChromeOS Media App. When false, a mock app is
   # bundled for testing integration points.
-  enable_cros_eche_app = is_chromeos && is_chrome_branded
+  enable_cros_eche_app = is_chrome_branded
 }
diff --git a/ash/webui/help_app_ui/help_app_ui.gni b/ash/webui/help_app_ui/help_app_ui.gni
index 8c29b691..8786ff33 100644
--- a/ash/webui/help_app_ui/help_app_ui.gni
+++ b/ash/webui/help_app_ui/help_app_ui.gni
@@ -1,7 +1,13 @@
+# Copyright 2025 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
 import("//build/config/chrome_build.gni")
 
+assert(is_chromeos)
+
 declare_args() {
   # Whether to enable the "real" ChromeOS Help App. When false, a mock app is
   # bundled for testing integration points.
-  enable_cros_help_app = is_chromeos && is_chrome_branded
+  enable_cros_help_app = is_chrome_branded
 }
diff --git a/ash/webui/media_app_ui/media_app_ui.gni b/ash/webui/media_app_ui/media_app_ui.gni
index 0f17b6f..3657f54c 100644
--- a/ash/webui/media_app_ui/media_app_ui.gni
+++ b/ash/webui/media_app_ui/media_app_ui.gni
@@ -1,3 +1,7 @@
+# Copyright 2025 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
 import("//build/config/chrome_build.gni")
 
 declare_args() {
diff --git a/ash/webui/projector_app/projector_app.gni b/ash/webui/projector_app/projector_app.gni
index c50cf69aa..654ff74 100644
--- a/ash/webui/projector_app/projector_app.gni
+++ b/ash/webui/projector_app/projector_app.gni
@@ -1,10 +1,12 @@
 import("//ash/webui/media_app_ui/media_app_ui.gni")
 import("//build/config/chrome_build.gni")
 
+assert(is_chromeos)
+
 declare_args() {
   # Whether to enable the "real" ChromeOS Projector App. When false, a mock app is
   # bundled for testing integration points.
-  enable_cros_projector_app = is_chromeos && is_chrome_branded
+  enable_cros_projector_app = is_chrome_branded
 }
 
 assert(!enable_cros_projector_app || enable_cros_media_app,
diff --git a/ash/webui/status_area_internals/status_area_internals_handler_unittest.cc b/ash/webui/status_area_internals/status_area_internals_handler_unittest.cc
index 3332a5f..1ee66e6 100644
--- a/ash/webui/status_area_internals/status_area_internals_handler_unittest.cc
+++ b/ash/webui/status_area_internals/status_area_internals_handler_unittest.cc
@@ -53,7 +53,7 @@
 // A class that mocks `MagicBoostStateAsh` to use in tests.
 class TestMagicBoostState : public chromeos::MagicBoostState {
  public:
-  TestMagicBoostState() = default;
+  TestMagicBoostState() { UpdateMagicBoostAvailable(true); }
 
   TestMagicBoostState(const TestMagicBoostState&) = delete;
   TestMagicBoostState& operator=(const TestMagicBoostState&) = delete;
@@ -67,7 +67,6 @@
   }
 
   bool ShouldIncludeOrcaInOptInSync() override { return false; }
-  bool IsMagicBoostAvailable() override { return true; }
   bool CanShowNoticeBannerForHMR() override { return false; }
   int32_t AsyncIncrementHMRConsentWindowDismissCount() override { return 0; }
   void AsyncWriteHMREnabled(bool enabled) override {}
diff --git a/ash/wm/desks/desks_controller.cc b/ash/wm/desks/desks_controller.cc
index f04c9ba3..f55369b 100644
--- a/ash/wm/desks/desks_controller.cc
+++ b/ash/wm/desks/desks_controller.cc
@@ -2,14 +2,10 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#ifdef UNSAFE_BUFFERS_BUILD
-// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
-#pragma allow_unsafe_buffers
-#endif
-
 #include "ash/wm/desks/desks_controller.h"
 
 #include <algorithm>
+#include <array>
 #include <utility>
 
 #include "ash/accessibility/accessibility_controller.h"
@@ -125,7 +121,7 @@
 // its "close" hooks before being forcefully closed.
 base::TimeDelta g_close_all_window_close_timeout = base::Seconds(1);
 
-constexpr int kDeskDefaultNameIds[] = {
+constexpr auto kDeskDefaultNameIds = std::to_array<int>({
     IDS_ASH_DESKS_DESK_1_MINI_VIEW_TITLE,
     IDS_ASH_DESKS_DESK_2_MINI_VIEW_TITLE,
     IDS_ASH_DESKS_DESK_3_MINI_VIEW_TITLE,
@@ -142,7 +138,7 @@
     IDS_ASH_DESKS_DESK_14_MINI_VIEW_TITLE,
     IDS_ASH_DESKS_DESK_15_MINI_VIEW_TITLE,
     IDS_ASH_DESKS_DESK_16_MINI_VIEW_TITLE,
-};
+});
 
 // Appends the given |windows| to the end of the currently active overview mode
 // session such that the most-recently used window is added first. If
diff --git a/ash/wm/desks/desks_unittests.cc b/ash/wm/desks/desks_unittests.cc
index 9bd958d..a303abfa 100644
--- a/ash/wm/desks/desks_unittests.cc
+++ b/ash/wm/desks/desks_unittests.cc
@@ -2,11 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#ifdef UNSAFE_BUFFERS_BUILD
-// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
-#pragma allow_unsafe_buffers
-#endif
-
+#include <array>
 #include <memory>
 #include <string>
 #include <vector>
@@ -11531,16 +11527,16 @@
     bool enabled;
     bool show_context_menu;
   };
-  const DeskSwitchButtonTestCase prev_test_cases[] = {
+  const auto prev_test_cases = std::to_array<DeskSwitchButtonTestCase>({
       {.visible = false, .enabled = false, .show_context_menu = false},
       {.visible = true, .enabled = true, .show_context_menu = true},
       {.visible = true, .enabled = true, .show_context_menu = true},
-  };
-  const DeskSwitchButtonTestCase next_test_cases[] = {
+  });
+  const auto next_test_cases = std::to_array<DeskSwitchButtonTestCase>({
       {.visible = true, .enabled = true, .show_context_menu = true},
       {.visible = true, .enabled = true, .show_context_menu = true},
       {.visible = true, .enabled = false, .show_context_menu = false},
-  };
+  });
 
   auto* event_generator = GetEventGenerator();
   auto* shelf_view = GetPrimaryShelf()->GetShelfViewForTesting();
diff --git a/ash/wm/desks/desks_util.cc b/ash/wm/desks/desks_util.cc
index 42b99667..c483ddc 100644
--- a/ash/wm/desks/desks_util.cc
+++ b/ash/wm/desks/desks_util.cc
@@ -2,11 +2,6 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#ifdef UNSAFE_BUFFERS_BUILD
-// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
-#pragma allow_unsafe_buffers
-#endif
-
 #include "ash/wm/desks/desks_util.h"
 
 #include <array>
@@ -83,14 +78,24 @@
 const char* GetDeskContainerName(int container_id) {
   CHECK(IsDeskContainerId(container_id));
 
-  static const char* kDeskContainerNames[] = {
-      "Desk_Container_A", "Desk_Container_B", "Desk_Container_C",
-      "Desk_Container_D", "Desk_Container_E", "Desk_Container_F",
-      "Desk_Container_G", "Desk_Container_H", "Desk_Container_I",
-      "Desk_Container_J", "Desk_Container_K", "Desk_Container_L",
-      "Desk_Container_M", "Desk_Container_N", "Desk_Container_O",
+  static constexpr auto kDeskContainerNames = std::to_array<const char*>({
+      "Desk_Container_A",
+      "Desk_Container_B",
+      "Desk_Container_C",
+      "Desk_Container_D",
+      "Desk_Container_E",
+      "Desk_Container_F",
+      "Desk_Container_G",
+      "Desk_Container_H",
+      "Desk_Container_I",
+      "Desk_Container_J",
+      "Desk_Container_K",
+      "Desk_Container_L",
+      "Desk_Container_M",
+      "Desk_Container_N",
+      "Desk_Container_O",
       "Desk_Container_P",
-  };
+  });
   return kDeskContainerNames[container_id - kShellWindowId_DeskContainerA];
 }
 
diff --git a/ash/wm/desks/templates/saved_desk_unittest.cc b/ash/wm/desks/templates/saved_desk_unittest.cc
index 1bc79fb..493251d6 100644
--- a/ash/wm/desks/templates/saved_desk_unittest.cc
+++ b/ash/wm/desks/templates/saved_desk_unittest.cc
@@ -2,11 +2,6 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#ifdef UNSAFE_BUFFERS_BUILD
-// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
-#pragma allow_unsafe_buffers
-#endif
-
 #include <algorithm>
 #include <array>
 #include <string>
@@ -4510,12 +4505,13 @@
                                                0);
   EXPECT_EQ(4u, menu_model.GetItemCount());
 
-  DeskActionContextMenu::CommandId expected_command[] = {
-      DeskActionContextMenu::CommandId::kSaveAsTemplate,
-      DeskActionContextMenu::CommandId::kSaveForLater,
-      DeskActionContextMenu::CommandId::kCombineDesks,
-      DeskActionContextMenu::CommandId::kCloseAll};
-  for (size_t i = 0; i < 4u; ++i) {
+  constexpr auto expected_command =
+      std::to_array<DeskActionContextMenu::CommandId>(
+          {DeskActionContextMenu::CommandId::kSaveAsTemplate,
+           DeskActionContextMenu::CommandId::kSaveForLater,
+           DeskActionContextMenu::CommandId::kCombineDesks,
+           DeskActionContextMenu::CommandId::kCloseAll});
+  for (size_t i = 0; i < expected_command.size(); ++i) {
     EXPECT_EQ(expected_command[i],
               static_cast<DeskActionContextMenu::CommandId>(
                   menu_model.GetCommandIdAt(i)));
diff --git a/ash/wm/lock_state_controller.cc b/ash/wm/lock_state_controller.cc
index e9bc5139..27c3c910 100644
--- a/ash/wm/lock_state_controller.cc
+++ b/ash/wm/lock_state_controller.cc
@@ -2,11 +2,6 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#ifdef UNSAFE_BUFFERS_BUILD
-// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
-#pragma allow_unsafe_buffers
-#endif
-
 #include "ash/wm/lock_state_controller.h"
 
 #include <algorithm>
@@ -40,6 +35,7 @@
 #include "ash/wm/workspace/backdrop_controller.h"
 #include "ash/wm/workspace/workspace_layout_manager.h"
 #include "ash/wm/workspace_controller.h"
+#include "base/containers/span.h"
 #include "base/debug/crash_logging.h"
 #include "base/debug/dump_without_crashing.h"
 #include "base/files/file_path.h"
@@ -147,8 +143,7 @@
       image, gfx::Size(informed_restore::kPreviewContainerWidth,
                        resized_image_height));
   auto png_bytes = resized_image.As1xPNGBytes();
-  if (!base::WriteFile(file_path,
-                       base::span(png_bytes->data(), png_bytes->size()))) {
+  if (!base::WriteFile(file_path, base::span(*png_bytes))) {
     LOG(ERROR) << "Failed to write informed restore image to "
                << file_path.MaybeAsASCII();
   }
diff --git a/ash/wm/overview/overview_controller_unittest.cc b/ash/wm/overview/overview_controller_unittest.cc
index e36ab840..bc50775 100644
--- a/ash/wm/overview/overview_controller_unittest.cc
+++ b/ash/wm/overview/overview_controller_unittest.cc
@@ -2,14 +2,11 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#ifdef UNSAFE_BUFFERS_BUILD
-// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
-#pragma allow_unsafe_buffers
-#endif
-
 #include "ash/wm/overview/overview_controller.h"
 
+#include <array>
 #include <memory>
+#include <vector>
 
 #include "ash/app_list/test/app_list_test_helper.h"
 #include "ash/frame_throttler/frame_throttling_controller.h"
@@ -811,12 +808,13 @@
   FrameThrottlingController* frame_throttling_controller =
       Shell::Get()->frame_throttling_controller();
   frame_throttling_controller->AddArcObserver(&observer);
-  const int browser_window_count = 3;
-  const int arc_window_count = 2;
+  constexpr int browser_window_count = 3;
+  constexpr int arc_window_count = 2;
 
   const std::vector<viz::FrameSinkId> ids{{1u, 1u}, {2u, 2u}, {3u, 3u}};
-  std::unique_ptr<aura::Window>
-      created_windows[browser_window_count + arc_window_count];
+  std::array<std::unique_ptr<aura::Window>,
+             browser_window_count + arc_window_count>
+      created_windows;
   for (int i = 0; i < browser_window_count; ++i) {
     created_windows[i] =
         CreateAppWindow(gfx::Rect(), chromeos::AppType::BROWSER);
diff --git a/ash/wm/overview/overview_session_unittest.cc b/ash/wm/overview/overview_session_unittest.cc
index 44c8262..6a926237 100644
--- a/ash/wm/overview/overview_session_unittest.cc
+++ b/ash/wm/overview/overview_session_unittest.cc
@@ -2,13 +2,9 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#ifdef UNSAFE_BUFFERS_BUILD
-// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
-#pragma allow_unsafe_buffers
-#endif
-
 #include "ash/wm/overview/overview_session.h"
 
+#include <array>
 #include <memory>
 #include <optional>
 #include <string>
@@ -5265,22 +5261,22 @@
   // item dropping down from the first row to the second row. Create the windows
   // backward so the the window indexs match the order seen in overview, as
   // overview windows are ordered by MRU.
-  const int kWindows = 5;
-  std::unique_ptr<aura::Window> windows[kWindows];
+  constexpr int kWindows = 5;
+  std::array<std::unique_ptr<aura::Window>, kWindows> windows;
   for (int i = kWindows - 1; i >= 0; --i)
     windows[i] = CreateTestWindow();
 
   ToggleOverview();
   ASSERT_TRUE(GetOverviewController()->InOverviewSession());
 
-  OverviewItemBase* items[kWindows];
-  gfx::RectF item_bounds[kWindows];
+  std::array<OverviewItemBase*, kWindows> items;
+  std::array<gfx::RectF, kWindows> item_bounds;
   for (int i = 0; i < kWindows; ++i) {
     items[i] = GetOverviewItemForWindow(windows[i].get());
     item_bounds[i] = items[i]->target_bounds();
   }
 
-  // Drag the forth item past the drag to swipe threshold. None of the other
+  // Drag the fourth item past the drag to swipe threshold. None of the other
   // window bounds should change, as none of them should be nudged, because
   // deleting the fourth item will cause the third item to drop down from the
   // first row to the second.
diff --git a/base/OWNERS b/base/OWNERS
index e563ab0b..e4a908d 100644
--- a/base/OWNERS
+++ b/base/OWNERS
@@ -29,7 +29,6 @@
 per-file ..._win*=file://base/win/OWNERS
 
 per-file feature_list*=asvitkine@chromium.org
-per-file feature_list*=isherman@chromium.org
 
 # Logging-related changes:
 per-file check*=olivierli@chromium.org
diff --git a/base/allocator/partition_allocator/src/partition_alloc/partition_address_space.h b/base/allocator/partition_allocator/src/partition_alloc/partition_address_space.h
index f678c2a1..34cc169 100644
--- a/base/allocator/partition_allocator/src/partition_alloc/partition_address_space.h
+++ b/base/allocator/partition_allocator/src/partition_alloc/partition_address_space.h
@@ -21,6 +21,7 @@
 #include "partition_alloc/partition_alloc_config.h"
 #include "partition_alloc/partition_alloc_constants.h"
 #include "partition_alloc/partition_alloc_forward.h"
+#include "partition_alloc/tagging.h"
 #include "partition_alloc/thread_isolation/alignment.h"
 
 #if PA_BUILDFLAG(ENABLE_THREAD_ISOLATION)
@@ -34,6 +35,38 @@
 
 namespace internal {
 
+// Utility class to calculate offset within a known pool.
+class PA_COMPONENT_EXPORT(PARTITION_ALLOC) PoolOffsetLookup {
+ public:
+  // Under default-constructed values all lookup will hit DCHECK.
+  PoolOffsetLookup()
+      : base_address_(0), base_mask_(static_cast<uintptr_t>(-1)) {}
+
+  PA_ALWAYS_INLINE uintptr_t GetTaggedOffset(void* ptr) const {
+    const uintptr_t address = reinterpret_cast<uintptr_t>(ptr);
+    PA_DCHECK((address & base_mask_) == base_address_);
+    return address & (kPtrTagMask | ~base_mask_);
+  }
+
+  PA_ALWAYS_INLINE void* GetPointer(uintptr_t tagged_offset) const {
+    PA_DCHECK(IsValidTaggedOffset(tagged_offset));
+    return reinterpret_cast<void*>(base_address_ | tagged_offset);
+  }
+
+  PA_ALWAYS_INLINE bool IsValidTaggedOffset(uintptr_t tagged_offset) const {
+    return !(tagged_offset & base_mask_ & ~kPtrTagMask);
+  }
+
+ private:
+  PoolOffsetLookup(uintptr_t base_address, uintptr_t base_mask)
+      : base_address_(base_address), base_mask_(base_mask) {}
+
+  uintptr_t base_address_;
+  uintptr_t base_mask_;
+
+  friend class PartitionAddressSpace;
+};
+
 // Manages PartitionAlloc address space, which is split into pools.
 // See `glossary.md`.
 class PA_COMPONENT_EXPORT(PARTITION_ALLOC) PartitionAddressSpace {
@@ -101,13 +134,34 @@
     return kConfigurablePoolMinSize;
   }
 
+  PA_ALWAYS_INLINE static PoolOffsetLookup GetOffsetLookup(pool_handle pool) {
+    switch (pool) {
+      case kRegularPoolHandle:
+        return PoolOffsetLookup(setup_.regular_pool_base_address_,
+                                CorePoolBaseMask());
+      case kBRPPoolHandle:
+        return PoolOffsetLookup(setup_.brp_pool_base_address_,
+                                CorePoolBaseMask());
+#if PA_BUILDFLAG(ENABLE_THREAD_ISOLATION)
+      case kThreadIsolatedPoolHandle:
+        return PoolOffsetLookup(setup_.thread_isolated_pool_base_address_,
+                                kThreadIsolatedPoolBaseMask);
+#endif  // PA_BUILDFLAG(ENABLE_THREAD_ISOLATION)
+      case kConfigurablePoolHandle:
+        return PoolOffsetLookup(setup_.configurable_pool_base_mask_,
+                                setup_.configurable_pool_base_mask_);
+      default:
+        PA_NOTREACHED();
+    }
+  }
+
   // Initialize pools (except for the configurable one).
   //
   // This function must only be called from the main thread.
   static void Init();
   // Initialize the ConfigurablePool at the given address |pool_base|. It must
   // be aligned to the size of the pool. The size must be a power of two and
-  // must be within [ConfigurablePoolMinSize(), ConfigurablePoolMaxSize()].
+  // must be within [kConfigurablePoolMinSize, kConfigurablePoolMaxSize].
   //
   // This function must only be called from the main thread.
   static void InitConfigurablePool(uintptr_t pool_base, size_t size);
diff --git a/base/allocator/partition_allocator/src/partition_alloc/partition_bucket.cc b/base/allocator/partition_allocator/src/partition_alloc/partition_bucket.cc
index 3fdd92e..5296fd7 100644
--- a/base/allocator/partition_allocator/src/partition_alloc/partition_bucket.cc
+++ b/base/allocator/partition_allocator/src/partition_alloc/partition_bucket.cc
@@ -847,7 +847,8 @@
     }
   }
 
-  if (root->ChoosePool() == kBRPPoolHandle) {
+#if PA_BUILDFLAG(ENABLE_BACKUP_REF_PTR_SUPPORT)
+  if (root->brp_enabled()) {
     // Allocate a system page for InSlotMetadata table (only one of its
     // elements will be used). Shadow metadata does not need to protect
     // this table, because (1) corrupting the table won't help with the
@@ -859,6 +860,7 @@
                             PageAccessibilityConfiguration::kReadWrite),
                         PageAccessibilityDisposition::kRequireUpdate);
   }
+#endif  // PA_BUILDFLAG(ENABLE_BACKUP_REF_PTR_SUPPORT)
 
   // If we were after a specific address, but didn't get it, assume that
   // the system chose a lousy address. Here most OS'es have a default
diff --git a/base/allocator/partition_allocator/src/partition_alloc/partition_freelist_entry.h b/base/allocator/partition_allocator/src/partition_alloc/partition_freelist_entry.h
index 1461ef1..3b1a6c3 100644
--- a/base/allocator/partition_allocator/src/partition_alloc/partition_freelist_entry.h
+++ b/base/allocator/partition_allocator/src/partition_alloc/partition_freelist_entry.h
@@ -5,6 +5,8 @@
 #ifndef PARTITION_ALLOC_PARTITION_FREELIST_ENTRY_H_
 #define PARTITION_ALLOC_PARTITION_FREELIST_ENTRY_H_
 
+#include <utility>
+
 #include "partition_alloc/buildflags.h"
 #include "partition_alloc/partition_alloc_constants.h"
 
@@ -36,8 +38,9 @@
 #endif
   {
   }
-  explicit FreelistEntry(FreelistEntry* next)
-      : encoded_next_(EncodedPtr(next))
+  template <typename... Args>
+  explicit FreelistEntry(FreelistEntry* next, Args&&... args)
+      : encoded_next_(EncodedPtr(next, std::forward<Args>(args)...))
 #if PA_CONFIG(HAS_FREELIST_SHADOW_ENTRY)
         ,
         shadow_(encoded_next_.Inverted())
@@ -45,8 +48,9 @@
   {
   }
   // For testing only.
-  FreelistEntry(void* next, bool make_shadow_match)
-      : encoded_next_(EncodedPtr(next))
+  template <typename... Args>
+  FreelistEntry(void* next, Args&&... args, bool make_shadow_match)
+      : encoded_next_(EncodedPtr(next, std::forward<Args>(args)...))
 #if PA_CONFIG(HAS_FREELIST_SHADOW_ENTRY)
         ,
         shadow_(make_shadow_match ? encoded_next_.Inverted() : 12345)
@@ -76,10 +80,13 @@
   // This freelist is built for the purpose of thread-cache. This means that we
   // can't perform a check that this and the next pointer belong to the same
   // super page, as thread-cache spans may chain slots across super pages.
+  template <typename... Args>
   PA_ALWAYS_INLINE static FreelistEntry* EmplaceAndInitForThreadCache(
       uintptr_t slot_start,
-      FreelistEntry* next) {
-    auto* entry = new (SlotStartAddr2Ptr(slot_start)) FreelistEntry(next);
+      FreelistEntry* next,
+      Args&&... args) {
+    auto* entry = new (SlotStartAddr2Ptr(slot_start))
+        FreelistEntry(next, std::forward<Args>(args)...);
     return entry;
   }
 
@@ -88,10 +95,13 @@
   //
   // This is for testing purposes only! |make_shadow_match| allows you to choose
   // if the shadow matches the next pointer properly or is trash.
+  template <typename... Args>
   PA_ALWAYS_INLINE static void EmplaceAndInitForTest(uintptr_t slot_start,
                                                      void* next,
+                                                     Args&&... args,
                                                      bool make_shadow_match) {
-    new (SlotStartAddr2Ptr(slot_start)) FreelistEntry(next, make_shadow_match);
+    new (SlotStartAddr2Ptr(slot_start))
+        FreelistEntry(next, std::forward<Args>(args)..., make_shadow_match);
   }
 
   void CorruptNextForTesting(uintptr_t v) {
@@ -101,12 +111,17 @@
 
   // Puts `slot_size` on the stack before crashing in case of memory
   // corruption. Meant to be used to report the failed allocation size.
-  PA_ALWAYS_INLINE FreelistEntry* GetNextForThreadCache(
-      size_t slot_size) const {
-    return GetNextInternal</*for_thread_cache=*/true>(slot_size);
+  template <typename... Args>
+  PA_ALWAYS_INLINE FreelistEntry* GetNextForThreadCache(size_t slot_size,
+                                                        Args&&... args) const {
+    return GetNextInternal</*for_thread_cache=*/true>(
+        slot_size, std::forward<Args>(args)...);
   }
-  PA_ALWAYS_INLINE FreelistEntry* GetNext(size_t slot_size) const {
-    return GetNextInternal</*for_thread_cache=*/false>(slot_size);
+  template <typename... Args>
+  PA_ALWAYS_INLINE FreelistEntry* GetNext(size_t slot_size,
+                                          Args&&... args) const {
+    return GetNextInternal</*for_thread_cache=*/false>(
+        slot_size, std::forward<Args>(args)...);
   }
 
   PA_NOINLINE void CheckFreeList(size_t slot_size) const {
@@ -122,7 +137,8 @@
     }
   }
 
-  PA_ALWAYS_INLINE void SetNext(FreelistEntry* entry) {
+  template <typename... Args>
+  PA_ALWAYS_INLINE void SetNext(FreelistEntry* entry, Args&&... args) {
     // SetNext() is either called on the freelist head, when provisioning new
     // slots, or when GetNext() has been called before, no need to pass the
     // size.
@@ -137,7 +153,7 @@
     }
 #endif  // PA_BUILDFLAG(DCHECKS_ARE_ON)
 
-    encoded_next_ = EncodedPtr(entry);
+    encoded_next_ = EncodedPtr(entry, std::forward<Args>(args)...);
 #if PA_CONFIG(HAS_FREELIST_SHADOW_ENTRY)
     shadow_ = encoded_next_.Inverted();
 #endif
@@ -159,8 +175,9 @@
   }
 
  private:
-  template <bool for_thread_cache>
-  PA_ALWAYS_INLINE FreelistEntry* GetNextInternal(size_t slot_size) const {
+  template <bool for_thread_cache, typename... Args>
+  PA_ALWAYS_INLINE FreelistEntry* GetNextInternal(size_t slot_size,
+                                                  Args&&... args) const {
     // GetNext() can be called on discarded memory, in which case
     // |encoded_next_| is 0, and none of the checks apply. Don't prefetch
     // nullptr either.
@@ -168,7 +185,7 @@
       return nullptr;
     }
 
-    auto* ret = encoded_next_.Decode(slot_size);
+    auto* ret = encoded_next_.Decode(slot_size, std::forward<Args>(args)...);
     if (!IsWellFormed<for_thread_cache>(this, ret)) [[unlikely]] {
       // Put the corrupted data on the stack, it may give us more information
       // about what kind of corruption that was.
diff --git a/base/allocator/partition_allocator/src/partition_alloc/partition_root.cc b/base/allocator/partition_allocator/src/partition_alloc/partition_root.cc
index d4994d96..6755bdf0 100644
--- a/base/allocator/partition_allocator/src/partition_alloc/partition_root.cc
+++ b/base/allocator/partition_allocator/src/partition_alloc/partition_root.cc
@@ -1134,15 +1134,16 @@
     ReserveBackupRefPtrGuardRegionIfNeeded();
 #endif
 
-#if PA_BUILDFLAG(ENABLE_BACKUP_REF_PTR_SUPPORT)
-    settings.brp_enabled_ = opts.backup_ref_ptr == PartitionOptions::kEnabled;
-#else   // PA_BUILDFLAG(ENABLE_BACKUP_REF_PTR_SUPPORT)
-    PA_CHECK(opts.backup_ref_ptr == PartitionOptions::kDisabled);
-#endif  // PA_BUILDFLAG(ENABLE_BACKUP_REF_PTR_SUPPORT)
-    settings.use_configurable_pool =
-        (opts.use_configurable_pool == PartitionOptions::kAllowed) &&
-        IsConfigurablePoolAvailable();
-    PA_DCHECK(!settings.use_configurable_pool || IsConfigurablePoolAvailable());
+#if PA_BUILDFLAG(HAS_64_BIT_POINTERS)
+    if (opts.use_configurable_pool == PartitionOptions::kAllowed &&
+        IsConfigurablePoolAvailable()) {
+      // BRP is not supported in the configurable pool because BRP requires
+      // objects to be in a different Pool.
+      PA_CHECK(opts.backup_ref_ptr == PartitionOptions::kDisabled);
+      PA_CHECK(settings.pool_handle == internal::kNullPoolHandle);
+      settings.pool_handle = internal::kConfigurablePoolHandle;
+    }
+#endif  // PA_BUILDFLAG(HAS_64_BIT_POINTERS)
     settings.eventually_zero_freed_memory =
         opts.eventually_zero_freed_memory == PartitionOptions::kEnabled;
     settings.fewer_memory_regions =
@@ -1167,7 +1168,7 @@
     // representations. All custom representations encountered so far rely on an
     // "is in configurable pool?" check, so we use that as a proxy.
     PA_CHECK(!settings.memory_tagging_enabled_ ||
-             !settings.use_configurable_pool);
+             settings.pool_handle != internal::kConfigurablePoolHandle);
 
     settings.use_random_memory_tagging_ =
         opts.memory_tagging.random_memory_tagging == PartitionOptions::kEnabled;
@@ -1176,18 +1177,15 @@
         opts.memory_tagging.reporting_mode;
 #endif  // PA_BUILDFLAG(HAS_MEMORY_TAGGING)
 
-    // brp_enabled() is not supported in the configurable pool because
-    // BRP requires objects to be in a different Pool.
-#if PA_BUILDFLAG(ENABLE_BACKUP_REF_PTR_SUPPORT)
-    PA_CHECK(!(settings.use_configurable_pool && brp_enabled()));
-#endif
-
 #if PA_BUILDFLAG(ENABLE_THREAD_ISOLATION)
-    // BRP and thread isolated mode use different pools, so they can't be
-    // enabled at the same time.
-    PA_CHECK(!opts.thread_isolation.enabled ||
-             opts.backup_ref_ptr == PartitionOptions::kDisabled);
     settings.thread_isolation = opts.thread_isolation;
+    if (opts.thread_isolation.enabled) {
+      // BRP and thread isolated mode use different pools, so they can't be
+      // enabled at the same time.
+      PA_CHECK(opts.backup_ref_ptr == PartitionOptions::kDisabled);
+      PA_CHECK(settings.pool_handle == internal::kNullPoolHandle);
+      settings.pool_handle = internal::kThreadIsolatedPoolHandle;
+    }
 #endif  // PA_BUILDFLAG(ENABLE_THREAD_ISOLATION)
 
 #if PA_CONFIG(EXTRAS_REQUIRED)
@@ -1198,17 +1196,33 @@
     }
 
 #if PA_BUILDFLAG(ENABLE_BACKUP_REF_PTR_SUPPORT)
-    if (brp_enabled()) {
+    settings.brp_enabled_ = opts.backup_ref_ptr == PartitionOptions::kEnabled;
+    if (opts.backup_ref_ptr == PartitionOptions::kEnabled) {
       settings.in_slot_metadata_size = internal::kInSlotMetadataSizeAdjustment;
       settings.extras_size += internal::kInSlotMetadataSizeAdjustment;
       settings.extras_size += opts.backup_ref_ptr_extra_extras_size;
 #if PA_CONFIG(MAYBE_ENABLE_MAC11_MALLOC_SIZE_HACK)
       EnableMac11MallocSizeHackIfNeeded();
 #endif
+
+      PA_CHECK(settings.pool_handle == internal::kNullPoolHandle);
+      settings.pool_handle = internal::kBRPPoolHandle;
     }
 #endif  // PA_BUILDFLAG(ENABLE_BACKUP_REF_PTR_SUPPORT)
 #endif  // PA_CONFIG(EXTRAS_REQUIRED)
 
+#if !PA_BUILDFLAG(ENABLE_BACKUP_REF_PTR_SUPPORT)
+    PA_CHECK(opts.backup_ref_ptr == PartitionOptions::kDisabled);
+#endif  // !PA_BUILDFLAG(ENABLE_BACKUP_REF_PTR_SUPPORT)
+
+    if (settings.pool_handle == internal::kNullPoolHandle) {
+      settings.pool_handle = internal::kRegularPoolHandle;
+    }
+#if PA_BUILDFLAG(HAS_64_BIT_POINTERS)
+    settings.offset_lookup =
+        internal::PartitionAddressSpace::GetOffsetLookup(settings.pool_handle);
+#endif  // PA_BUILDFLAG(HAS_64_BIT_POINTERS)
+
     // We mark the sentinel slot span as free to make sure it is skipped by our
     // logic to find a new active slot span.
     memset(&sentinel_bucket, 0, sizeof(sentinel_bucket));
@@ -1252,8 +1266,8 @@
 
 #if PA_CONFIG(ENABLE_SHADOW_METADATA)
     if (internal::PartitionAddressSpace::IsShadowMetadataEnabled(
-            ChoosePool())) {
-      switch (ChoosePool()) {
+            settings.pool_handle)) {
+      switch (settings.pool_handle) {
         case internal::kRegularPoolHandle:
           settings.shadow_pool_offset_ =
               internal::PartitionAddressSpace::RegularPoolShadowOffset();
diff --git a/base/allocator/partition_allocator/src/partition_alloc/partition_root.h b/base/allocator/partition_allocator/src/partition_alloc/partition_root.h
index 3f00bde..6dc8bb9 100644
--- a/base/allocator/partition_allocator/src/partition_alloc/partition_root.h
+++ b/base/allocator/partition_allocator/src/partition_alloc/partition_root.h
@@ -265,7 +265,12 @@
 #endif  // PA_CONFIG(MAYBE_ENABLE_MAC11_MALLOC_SIZE_HACK)
     size_t in_slot_metadata_size = 0;
 #endif  // PA_BUILDFLAG(ENABLE_BACKUP_REF_PTR_SUPPORT)
-    bool use_configurable_pool = false;
+
+    internal::pool_handle pool_handle = internal::pool_handle::kNullPoolHandle;
+#if PA_BUILDFLAG(HAS_64_BIT_POINTERS)
+    internal::PoolOffsetLookup offset_lookup;
+#endif  // PA_BUILDFLAG(HAS_64_BIT_POINTERS)
+
     bool eventually_zero_freed_memory = false;
     internal::SchedulerLoopQuarantineConfig
         scheduler_loop_quarantine_thread_local_config;
@@ -710,28 +715,12 @@
     return PA_TS_UNCHECKED_READ(max_size_of_allocated_bytes);
   }
 
-  internal::pool_handle ChoosePool() const {
+  internal::pool_handle ChoosePool() const { return settings.pool_handle; }
 #if PA_BUILDFLAG(HAS_64_BIT_POINTERS)
-    if (settings.use_configurable_pool) {
-      PA_DCHECK(IsConfigurablePoolAvailable());
-      return internal::kConfigurablePoolHandle;
-    }
-#endif
-#if PA_BUILDFLAG(ENABLE_THREAD_ISOLATION)
-    if (settings.thread_isolation.enabled) {
-      return internal::kThreadIsolatedPoolHandle;
-    }
-#endif
-#if PA_BUILDFLAG(ENABLE_BACKUP_REF_PTR_SUPPORT)
-    if (brp_enabled()) [[likely]] {
-      return internal::kBRPPoolHandle;
-    } else {
-      return internal::kRegularPoolHandle;
-    }
-#else
-    return internal::kRegularPoolHandle;
-#endif  // PA_BUILDFLAG(ENABLE_BACKUP_REF_PTR_SUPPORT)
+  PA_ALWAYS_INLINE const internal::PoolOffsetLookup& GetOffsetLookup() const {
+    return settings.offset_lookup;
   }
+#endif  // PA_BUILDFLAG(HAS_64_BIT_POINTERS)
 
   PA_ALWAYS_INLINE static PAGE_ALLOCATOR_CONSTANTS_DECLARE_CONSTEXPR size_t
   GetDirectMapMetadataAndGuardPagesSize() {
@@ -838,10 +827,6 @@
   bool brp_enabled() const { return settings.brp_enabled_; }
 #endif  // PA_BUILDFLAG(ENABLE_BACKUP_REF_PTR_SUPPORT)
 
-  PA_ALWAYS_INLINE bool uses_configurable_pool() const {
-    return settings.use_configurable_pool;
-  }
-
   void AdjustForForeground() {
     max_empty_slot_spans_dirty_bytes_shift = 2;
     ::partition_alloc::internal::ScopedGuard guard{
diff --git a/base/allocator/partition_allocator/src/partition_alloc/pool_offset_freelist.h b/base/allocator/partition_allocator/src/partition_alloc/pool_offset_freelist.h
index 74dd190..56b7f391 100644
--- a/base/allocator/partition_allocator/src/partition_alloc/pool_offset_freelist.h
+++ b/base/allocator/partition_allocator/src/partition_alloc/pool_offset_freelist.h
@@ -13,6 +13,7 @@
 #include "partition_alloc/partition_address_space.h"
 #include "partition_alloc/partition_alloc-inl.h"
 #include "partition_alloc/partition_alloc_base/compiler_specific.h"
+#include "partition_alloc/partition_alloc_check.h"
 #include "partition_alloc/partition_alloc_config.h"
 #include "partition_alloc/partition_alloc_constants.h"
 #include "partition_alloc/tagging.h"
@@ -50,6 +51,11 @@
   PA_ALWAYS_INLINE explicit EncodedPoolOffset(void* ptr)
       // The encoded pointer stays MTE-tagged.
       : encoded_(Encode(ptr)) {}
+  // Similar to above, but faster with known pool.
+  PA_ALWAYS_INLINE explicit EncodedPoolOffset(
+      void* ptr,
+      const PoolOffsetLookup& offset_lookup)
+      : encoded_(Encode(ptr, offset_lookup)) {}
 
   PA_ALWAYS_INLINE constexpr uintptr_t Inverted() const { return ~encoded_; }
 
@@ -94,6 +100,17 @@
     return Transform(tagged_offset);
   }
 
+  // Similar to above, but faster with known pool.
+  PA_ALWAYS_INLINE static uintptr_t Encode(
+      void* ptr,
+      const PoolOffsetLookup& offset_lookup) {
+    if (!ptr) {
+      return kEncodeedNullptr;
+    }
+    // Save a MTE tag as well as an offset.
+    return Transform(offset_lookup.GetTaggedOffset(ptr));
+  }
+
   // Given `pool_info`, decodes a `tagged_offset` into a tagged pointer.
   PA_ALWAYS_INLINE FreelistEntry* Decode(size_t slot_size) const {
     PoolInfo pool_info = GetPoolInfo(SlotStartPtr2Addr(this));
@@ -109,6 +126,22 @@
     return reinterpret_cast<FreelistEntry*>(pool_info.base | tagged_offset);
   }
 
+  // Given `pool_info`, decodes a `tagged_offset` into a tagged pointer.
+  PA_ALWAYS_INLINE FreelistEntry* Decode(
+      size_t slot_size,
+      const PoolOffsetLookup& offset_lookup) const {
+    uintptr_t tagged_offset = Transform(encoded_);
+
+    // `tagged_offset` must not have bits set in the pool base mask, except MTE
+    // tag.
+    if (!offset_lookup.IsValidTaggedOffset(tagged_offset)) {
+      FreelistCorruptionDetected(slot_size);
+    }
+
+    // We assume `tagged_offset` contains a proper MTE tag.
+    return static_cast<FreelistEntry*>(offset_lookup.GetPointer(tagged_offset));
+  }
+
   uintptr_t encoded_;
 
   friend FreelistEntry;
diff --git a/base/allocator/partition_allocator/src/partition_alloc/thread_cache.cc b/base/allocator/partition_allocator/src/partition_alloc/thread_cache.cc
index bff4d358e..ef2984c 100644
--- a/base/allocator/partition_allocator/src/partition_alloc/thread_cache.cc
+++ b/base/allocator/partition_allocator/src/partition_alloc/thread_cache.cc
@@ -490,6 +490,9 @@
 
 ThreadCache::ThreadCache(PartitionRoot* root)
     : should_purge_(false),
+#if PA_BUILDFLAG(HAS_64_BIT_POINTERS)
+      offset_lookup_(root->GetOffsetLookup()),
+#endif  // PA_BUILDFLAG(HAS_64_BIT_POINTERS)
       root_(root),
       thread_id_(internal::base::PlatformThread::CurrentId()),
       next_(nullptr),
@@ -688,11 +691,20 @@
     auto* head = bucket.freelist_head;
     size_t items = 1;  // Cannot free the freelist head.
     while (items < limit) {
+#if PA_BUILDFLAG(HAS_64_BIT_POINTERS)
+      head = head->GetNextForThreadCache(bucket.slot_size, offset_lookup_);
+#else
       head = head->GetNextForThreadCache(bucket.slot_size);
+#endif  // PA_BUILDFLAG(HAS_64_BIT_POINTERS)
       items++;
     }
 
+#if PA_BUILDFLAG(HAS_64_BIT_POINTERS)
+    FreeAfter(head->GetNextForThreadCache(bucket.slot_size, offset_lookup_),
+              bucket.slot_size);
+#else
     FreeAfter(head->GetNextForThreadCache(bucket.slot_size), bucket.slot_size);
+#endif  // PA_BUILDFLAG(HAS_64_BIT_POINTERS)
     head->SetNext(nullptr);
   }
   bucket.count = limit;
@@ -711,7 +723,11 @@
   internal::ScopedGuard guard(internal::PartitionRootLock(root_));
   while (head) {
     uintptr_t slot_start = internal::SlotStartPtr2Addr(head);
+#if PA_BUILDFLAG(HAS_64_BIT_POINTERS)
+    head = head->GetNextForThreadCache(slot_size, offset_lookup_);
+#else
     head = head->GetNextForThreadCache(slot_size);
+#endif  // PA_BUILDFLAG(HAS_64_BIT_POINTERS)
     root_->RawFreeLocked(slot_start);
   }
 }
@@ -835,8 +851,13 @@
       position = index;
       return true;
     }
+#if PA_BUILDFLAG(HAS_64_BIT_POINTERS)
+    internal::FreelistEntry* next =
+        entry->GetNextForThreadCache(bucket.slot_size, offset_lookup_);
+#else
     internal::FreelistEntry* next =
         entry->GetNextForThreadCache(bucket.slot_size);
+#endif
     entry = next;
     ++index;
   }
diff --git a/base/allocator/partition_allocator/src/partition_alloc/thread_cache.h b/base/allocator/partition_allocator/src/partition_alloc/thread_cache.h
index c2f3ff6..1b93aff 100644
--- a/base/allocator/partition_allocator/src/partition_alloc/thread_cache.h
+++ b/base/allocator/partition_allocator/src/partition_alloc/thread_cache.h
@@ -439,6 +439,9 @@
   // These are at the beginning as they're accessed for each allocation.
   uint32_t cached_memory_ = 0;
   std::atomic<bool> should_purge_;
+#if PA_BUILDFLAG(HAS_64_BIT_POINTERS)
+  const internal::PoolOffsetLookup offset_lookup_;
+#endif  // PA_BUILDFLAG(HAS_64_BIT_POINTERS)
   ThreadCacheStats stats_;
   ThreadAllocStats thread_alloc_stats_;
 
@@ -549,12 +552,17 @@
 #endif  // PA_BUILDFLAG(IS_CHROMEOS) && PA_BUILDFLAG(PA_ARCH_CPU_X86_64) &&
         // PA_BUILDFLAG(HAS_64_BIT_POINTERS)
 
-  // Passes the bucket size to |GetNext()|, so that in case of freelist
-  // corruption, we know the bucket size that lead to the crash, helping to
-  // narrow down the search for culprit. |bucket| was touched just now, so this
-  // does not introduce another cache miss.
+  // Passes the bucket size to |GetNextForThreadCache()|, so that in case of
+  // freelist corruption, we know the bucket size that lead to the crash,
+  // helping to narrow down the search for culprit. |bucket| was touched just
+  // now, so this does not introduce another cache miss.
+#if PA_BUILDFLAG(HAS_64_BIT_POINTERS)
+  internal::FreelistEntry* next =
+      entry->GetNextForThreadCache(bucket.slot_size, offset_lookup_);
+#else
   internal::FreelistEntry* next =
       entry->GetNextForThreadCache(bucket.slot_size);
+#endif  // PA_BUILDFLAG(HAS_64_BIT_POINTERS)
 
   PA_DCHECK(entry != next);
   bucket.count--;
@@ -621,8 +629,13 @@
 #endif  // PA_CONFIG(HAS_FREELIST_SHADOW_ENTRY) &&
         // PA_BUILDFLAG(PA_ARCH_CPU_X86_64) && PA_BUILDFLAG(HAS_64_BIT_POINTERS)
 
+#if PA_BUILDFLAG(HAS_64_BIT_POINTERS)
+  auto* entry = internal::FreelistEntry::EmplaceAndInitForThreadCache(
+      slot_start, bucket.freelist_head, offset_lookup_);
+#else
   auto* entry = internal::FreelistEntry::EmplaceAndInitForThreadCache(
       slot_start, bucket.freelist_head);
+#endif
   bucket.freelist_head = entry;
   bucket.count++;
 }
diff --git a/base/containers/OWNERS b/base/containers/OWNERS
index 3c2fb6400..706ee66f 100644
--- a/base/containers/OWNERS
+++ b/base/containers/OWNERS
@@ -1,2 +1,5 @@
 per-file enum_set.h=file://components/sync/OWNERS
 per-file enum_set_unittest.cc=file://components/sync/OWNERS
+
+per-file auto_spanification_helper.h=file://tools/clang/spanify/OWNERS
+per-file auto_spanification_helper_unittest.cc=file://tools/clang/spanify/OWNERS
diff --git a/base/containers/auto_spanification_helper.h b/base/containers/auto_spanification_helper.h
index b38420e..4b1a114c 100644
--- a/base/containers/auto_spanification_helper.h
+++ b/base/containers/auto_spanification_helper.h
@@ -73,11 +73,25 @@
 
 // https://source.chromium.org/chromium/chromium/src/+/main:third_party/skia/include/core/SkBitmap.h;drc=f72bd467feb15edd9323e46eab1b74ab6025bc5b;l=936
 #define UNSAFE_SKBITMAP_GETADDR32(arg_self, arg_x, arg_y) \
-  (([](auto&& self, int x, int y) {                       \
+  ([](auto&& self, int x, int y) {                        \
     uint32_t* row = self->getAddr32(x, y);                \
     ::base::CheckedNumeric<size_t> width = self->width(); \
     size_t size = (width - x).ValueOrDie();               \
     return UNSAFE_TODO(base::span<uint32_t>(row, size));  \
-  })(::base::spanification_internal::ToPointer(arg_self), arg_x, arg_y))
+  }(::base::spanification_internal::ToPointer(arg_self), arg_x, arg_y))
+
+// https://source.chromium.org/chromium/chromium/src/+/main:third_party/perl/c/include/harfbuzz/hb-buffer.h;drc=6f3e5028eb65d0b4c5fdd792106ac4c84eee1eb3;l=442
+#define UNSAFE_HB_BUFFER_GET_GLYPH_POSITIONS(arg_buffer, arg_length)        \
+  ([](hb_buffer_t* buffer, unsigned int* length) {                          \
+    unsigned int len;                                                       \
+    hb_glyph_position_t* pos = hb_buffer_get_glyph_positions(buffer, &len); \
+    if (length)                                                             \
+      *length = len;                                                        \
+    /* It's not clear whether the length is guaranteed to be 0 when !pos.   \
+       Explicitly set the length to 0 just in case. */                      \
+    if (!pos)                                                               \
+      return UNSAFE_TODO(base::span<hb_glyph_position_t>(pos, 0u));         \
+    return UNSAFE_TODO(base::span<hb_glyph_position_t>(pos, len));          \
+  }(arg_buffer, arg_length))
 
 #endif  // BASE_CONTAINERS_AUTO_SPANIFICATION_HELPER_H_
diff --git a/base/containers/auto_spanification_helper_unittest.cc b/base/containers/auto_spanification_helper_unittest.cc
index 3c90459..63fe4958 100644
--- a/base/containers/auto_spanification_helper_unittest.cc
+++ b/base/containers/auto_spanification_helper_unittest.cc
@@ -8,6 +8,7 @@
 #include <cstdint>
 
 #include "base/containers/span.h"
+#include "base/memory/raw_ptr.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
 namespace base::internal::spanification {
@@ -51,6 +52,45 @@
   EXPECT_EQ(span.size(), sk_bitmap->row_.size() - x);
 }
 
+// Minimized mock of hb_buffer_get_glyph_positions defined in
+// //third_party/perl/c/include/harfbuzz/hb-buffer.h
+struct hb_glyph_position_t {};
+struct hb_buffer_t {
+  base::raw_ptr<hb_glyph_position_t> pos = nullptr;
+  unsigned int len = 0;
+};
+hb_glyph_position_t* hb_buffer_get_glyph_positions(hb_buffer_t* buffer,
+                                                   unsigned int* length) {
+  if (length) {
+    *length = buffer->len;
+  }
+  return buffer->pos.get();
+}
+
+TEST(AutoSpanificationHelperTest, HbBufferGetGlyphPositions) {
+  std::array<hb_glyph_position_t, 128> pos_array;
+  hb_buffer_t buffer;
+  unsigned int length = 0;
+  base::span<hb_glyph_position_t> positions;
+
+  buffer = {pos_array.data(), pos_array.size()};
+  positions = UNSAFE_HB_BUFFER_GET_GLYPH_POSITIONS(&buffer, &length);
+  EXPECT_EQ(positions.data(), pos_array.data());
+  EXPECT_EQ(positions.size(), pos_array.size());
+  EXPECT_EQ(length, pos_array.size());
+
+  buffer = {pos_array.data(), pos_array.size()};
+  positions = UNSAFE_HB_BUFFER_GET_GLYPH_POSITIONS(&buffer, /*length=*/nullptr);
+  EXPECT_EQ(positions.data(), pos_array.data());
+  EXPECT_EQ(positions.size(), pos_array.size());
+
+  buffer = {nullptr, pos_array.size()};  // pos == nullptr, len != 0
+  positions = UNSAFE_HB_BUFFER_GET_GLYPH_POSITIONS(&buffer, &length);
+  EXPECT_EQ(positions.data(), nullptr);
+  EXPECT_EQ(positions.size(), 0);  // The span's size is 0
+  EXPECT_NE(length, 0);            // even when `length` is non-zero.
+}
+
 }  // namespace
 
 }  // namespace base::internal::spanification
diff --git a/base/metrics/METRICS_OWNERS b/base/metrics/METRICS_OWNERS
index cd90c94..0be8ea7 100644
--- a/base/metrics/METRICS_OWNERS
+++ b/base/metrics/METRICS_OWNERS
@@ -10,7 +10,6 @@
 asvitkine@chromium.org #{LAST_RESORT_SUGGESTION}
 caitlinfischer@google.com #{LAST_RESORT_SUGGESTION}
 holte@chromium.org #{LAST_RESORT_SUGGESTION}
-isherman@chromium.org #{LAST_RESORT_SUGGESTION}
 lucnguyen@google.com #{LAST_RESORT_SUGGESTION}
 mpearson@chromium.org #{LAST_RESORT_SUGGESTION}
 rkaplow@chromium.org #{LAST_RESORT_SUGGESTION}
diff --git a/base/task/sequence_manager/work_queue.cc b/base/task/sequence_manager/work_queue.cc
index 9ded5d26..1c036419 100644
--- a/base/task/sequence_manager/work_queue.cc
+++ b/base/task/sequence_manager/work_queue.cc
@@ -229,10 +229,6 @@
 }
 
 bool WorkQueue::RemoveCancelledTasks(RemoveCancelledTasksPolicy policy) {
-  if (!work_queue_sets_) {
-    return false;
-  }
-
   // Since task destructors could have a side-effect of deleting this task queue
   // we move cancelled tasks into a temporary container which can be emptied
   // without accessing |this|.
@@ -286,6 +282,7 @@
   // If we have a valid |heap_handle_| (i.e. we're not blocked by a fence or
   // disabled) then |work_queue_sets_| needs to be told.
   if (heap_handle_.IsValid()) {
+    CHECK(work_queue_sets_);
     work_queue_sets_->OnQueuesFrontTaskChanged(this);
   }
   task_queue_->TraceQueueSize();
diff --git a/base/test/android/javatests/src/org/chromium/base/test/transit/ViewElement.java b/base/test/android/javatests/src/org/chromium/base/test/transit/ViewElement.java
index c00893f7..fdf6e769 100644
--- a/base/test/android/javatests/src/org/chromium/base/test/transit/ViewElement.java
+++ b/base/test/android/javatests/src/org/chromium/base/test/transit/ViewElement.java
@@ -118,6 +118,19 @@
         return mViewSpec.descendant(viewClass, viewMatcher);
     }
 
+    /** Returns a {@link ViewSpec} to declare an ancestor of this ViewElement. */
+    @SafeVarargs
+    public final ViewSpec<View> ancestor(Matcher<View>... viewMatcher) {
+        return mViewSpec.ancestor(viewMatcher);
+    }
+
+    /** Returns a {@link ViewSpec} to declare an ancestor of this ViewElement. */
+    @SafeVarargs
+    public final <DescendantViewT extends View> ViewSpec<DescendantViewT> ancestor(
+            Class<DescendantViewT> viewClass, Matcher<View>... viewMatcher) {
+        return mViewSpec.ancestor(viewClass, viewMatcher);
+    }
+
     /** Trigger an Espresso action on this View. */
     public Transition.Trigger getPerformTrigger(ViewAction action) {
         return () -> {
diff --git a/base/test/android/javatests/src/org/chromium/base/test/transit/ViewSpec.java b/base/test/android/javatests/src/org/chromium/base/test/transit/ViewSpec.java
index 0714fee..a2ed3a54 100644
--- a/base/test/android/javatests/src/org/chromium/base/test/transit/ViewSpec.java
+++ b/base/test/android/javatests/src/org/chromium/base/test/transit/ViewSpec.java
@@ -4,6 +4,7 @@
 
 package org.chromium.base.test.transit;
 
+import static androidx.test.espresso.matcher.ViewMatchers.hasDescendant;
 import static androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA;
 
 import static org.hamcrest.CoreMatchers.instanceOf;
@@ -81,6 +82,23 @@
         return viewSpec(viewClass, allViewMatchers);
     }
 
+    /** Create a ViewSpec for a descendant of this ViewSpec that matches multiple Matchers<View>. */
+    @SafeVarargs
+    public final ViewSpec<View> ancestor(Matcher<View>... viewMatchers) {
+        Matcher<View>[] allViewMatchers = Arrays.copyOf(viewMatchers, viewMatchers.length + 1);
+        allViewMatchers[viewMatchers.length] = hasDescendant(mViewMatcher);
+        return viewSpec(allViewMatchers);
+    }
+
+    /** Create a ViewSpec for a descendant of this ViewSpec that matches multiple Matchers<View>. */
+    @SafeVarargs
+    public final <ChildViewT extends View> ViewSpec<ChildViewT> ancestor(
+            Class<ChildViewT> viewClass, Matcher<View>... viewMatchers) {
+        Matcher<View>[] allViewMatchers = Arrays.copyOf(viewMatchers, viewMatchers.length + 1);
+        allViewMatchers[viewMatchers.length] = hasDescendant(mViewMatcher);
+        return viewSpec(viewClass, allViewMatchers);
+    }
+
     /** Creates a ViewSpec that matches this ViewSpec _and_ another Matcher<View>. */
     public final ViewSpec<View> and(Matcher<View> viewMatcher) {
         return viewSpec(viewMatcher, mViewMatcher);
diff --git a/base/win/elevation_util.cc b/base/win/elevation_util.cc
index 4413323..ce72552 100644
--- a/base/win/elevation_util.cc
+++ b/base/win/elevation_util.cc
@@ -112,7 +112,9 @@
 }
 
 HRESULT RunDeElevatedNoWait(const std::wstring& path,
-                            const std::wstring& parameters) {
+                            const std::wstring& parameters,
+                            std::optional<std::wstring_view> current_directory,
+                            bool start_hidden) {
   Microsoft::WRL::ComPtr<IShellWindows> shell;
   HRESULT hr = ::CoCreateInstance(CLSID_ShellWindows, nullptr,
                                   CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&shell));
@@ -171,8 +173,10 @@
 
   return shell_dispatch->ShellExecute(
       ScopedBstr(path.c_str()).Get(), ScopedVariant(parameters.c_str()),
-      ScopedVariant::kEmptyVariant, ScopedVariant::kEmptyVariant,
-      ScopedVariant::kEmptyVariant);
+      current_directory ? ScopedVariant(current_directory->data())
+                        : ScopedVariant::kEmptyVariant,
+      ScopedVariant::kEmptyVariant /* vOperation */,
+      ScopedVariant(start_hidden ? SW_HIDE : SW_SHOWDEFAULT));
 }
 
 }  // namespace base::win
diff --git a/base/win/elevation_util.h b/base/win/elevation_util.h
index 5584cce..e793b07a 100644
--- a/base/win/elevation_util.h
+++ b/base/win/elevation_util.h
@@ -5,6 +5,7 @@
 #ifndef BASE_WIN_ELEVATION_UTIL_H_
 #define BASE_WIN_ELEVATION_UTIL_H_
 
+#include <optional>
 #include <string>
 
 #include "base/base_export.h"
@@ -36,10 +37,15 @@
 // Runs `path` de-elevated using `IShellDispatch2::ShellExecute`. `path`
 // specifies the file or object on which to execute the default verb (typically
 // "open"). If `path` specifies an executable file, `parameters` specifies the
-// parameters to be passed to the executable. The function does not wait for the
-// spawned process.
-BASE_EXPORT HRESULT RunDeElevatedNoWait(const std::wstring& path,
-                                        const std::wstring& parameters);
+// parameters to be passed to the executable. The current directory is
+// recommended, as the default is system32. `start_hidden` will influence the
+// show command. The function does not wait for the spawned process. N.B. this
+// function requires COM to be initialized.
+BASE_EXPORT HRESULT RunDeElevatedNoWait(
+    const std::wstring& path,
+    const std::wstring& parameters,
+    std::optional<std::wstring_view> current_directory = std::nullopt,
+    bool start_hidden = false);
 
 }  // namespace base::win
 
diff --git a/base/win/win_util.cc b/base/win/win_util.cc
index b4cab64..78fa88e 100644
--- a/base/win/win_util.cc
+++ b/base/win/win_util.cc
@@ -724,6 +724,22 @@
   return (uac_enabled != 0);
 }
 
+bool UserAccountIsUnnecessarilyElevated() {
+  // Check the process token to tell us that it's:
+  // * Elevated
+  // * UAC is enabled
+  // * It's not an account that always runs elevated even with UAC enabled
+  // The last bullet happens on built-in Admin *without* the "run BA filtered"
+  // policy set.
+  DWORD size;
+  TOKEN_ELEVATION_TYPE elevation_type;
+  if (GetTokenInformation(GetCurrentProcessToken(), TokenElevationType,
+                          &elevation_type, sizeof(elevation_type), &size)) {
+    return elevation_type == TokenElevationTypeFull;
+  }
+  return false;
+}
+
 bool SetBooleanValueForPropertyStore(IPropertyStore* property_store,
                                      const PROPERTYKEY& property_key,
                                      bool property_bool_value) {
diff --git a/base/win/win_util.h b/base/win/win_util.h
index 6f2fd13..9fd850c 100644
--- a/base/win/win_util.h
+++ b/base/win/win_util.h
@@ -61,6 +61,11 @@
 // if the OS is Vista or later.
 BASE_EXPORT bool UserAccountControlIsEnabled();
 
+// Returns true if the process is running at elevated permissions, but could
+// be at medium IL (eg. UAC is enabled and the account is not a built-in
+// administrator).
+BASE_EXPORT bool UserAccountIsUnnecessarilyElevated();
+
 // Sets the boolean value for a given key in given IPropertyStore.
 BASE_EXPORT bool SetBooleanValueForPropertyStore(
     IPropertyStore* property_store,
diff --git a/build/config/clang/BUILD.gn b/build/config/clang/BUILD.gn
index e4119b1b..47046082 100644
--- a/build/config/clang/BUILD.gn
+++ b/build/config/clang/BUILD.gn
@@ -6,19 +6,15 @@
 import("//build/config/rust.gni")
 import("clang.gni")
 
-declare_args() {
-  # Whether to use Clang runtime libraries from the ChromeOS chroot.
-  # This should be true iff using the compiler from the chroot.
-  use_cros_sysroot_libs =
-      is_chromeos_device && current_toolchain == default_toolchain
-}
-
 if (is_ios) {
   # For `target_environment` and `target_platform`.
   import("//build/config/apple/mobile_config.gni")
 }
 
-if (use_cros_sysroot_libs) {
+_use_cros_sysroot_libs =
+    is_chromeos_device && current_toolchain == default_toolchain
+
+if (_use_cros_sysroot_libs) {
   import("//build/toolchain/cros_toolchain.gni")  # For `cros_target_cc`
 }
 
@@ -151,10 +147,10 @@
 }
 
 _cros_resource_dir = ""
-if (use_cros_sysroot_libs) {
+if (_use_cros_sysroot_libs) {
   _cros_resource_dir =
       exec_script(rebase_path("../../toolchain/cros/get_resource_dir.py"),
-                  [ rebase_path(cros_target_cc, root_build_dir) ],
+                  [ cros_target_cc ],
                   "trim string",
                   [])
 }
@@ -188,7 +184,7 @@
         }
       } else if (is_apple) {
         _dir = "darwin"
-      } else if (use_cros_sysroot_libs) {
+      } else if (_use_cros_sysroot_libs) {
         _clang_lib_dir = _cros_resource_dir
         _dir = "lib/linux"
         if (current_cpu == "x64") {
@@ -249,7 +245,7 @@
       # HACK: using ChromeOS' compiler-rt results in DSOs exporting
       # compiler-rt symbols; figure out why it's (presumably) not using hidden
       # visibility for most symbols.
-      if (use_cros_sysroot_libs) {
+      if (_use_cros_sysroot_libs) {
         ldflags = [ "-Wl,--exclude-libs=$_lib_file" ]
       }
     }
diff --git a/build/config/linux/cached_results.scope b/build/config/linux/cached_results.scope
deleted file mode 100644
index 360984b0..0000000
--- a/build/config/linux/cached_results.scope
+++ /dev/null
@@ -1,104 +0,0 @@
-# Copyright 2025 The Chromium Authors
-# Use of this source code is governed by a BSD-style license that can be
-# found in the LICENSE file.
-
-# Cached results for common pkg_config.py runs.
-data = [
-  {
-    sysroot = "../../build/linux/debian_bullseye_amd64-sysroot"
-    entries = [
-      {
-        args = ["atk", "atk-bridge-2.0"]
-        pkgresult = [["../../build/linux/debian_bullseye_amd64-sysroot/usr/include/atk-1.0", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/glib-2.0", "../../build/linux/debian_bullseye_amd64-sysroot/usr/lib/x86_64-linux-gnu/glib-2.0/include", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/at-spi2-atk/2.0", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/dbus-1.0", "../../build/linux/debian_bullseye_amd64-sysroot/usr/lib/x86_64-linux-gnu/dbus-1.0/include", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/at-spi-2.0"], [], ["atk-1.0", "gobject-2.0", "glib-2.0", "atk-bridge-2.0"], []]
-      },
-      {
-        args = ["atspi-2"]
-        pkgresult = [["../../build/linux/debian_bullseye_amd64-sysroot/usr/include/at-spi-2.0", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/dbus-1.0", "../../build/linux/debian_bullseye_amd64-sysroot/usr/lib/x86_64-linux-gnu/dbus-1.0/include", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/glib-2.0", "../../build/linux/debian_bullseye_amd64-sysroot/usr/lib/x86_64-linux-gnu/glib-2.0/include"], [], ["atspi", "dbus-1", "glib-2.0"], []]
-      },
-      {
-        args = ["dbus-1"]
-        pkgresult = [["../../build/linux/debian_bullseye_amd64-sysroot/usr/include/dbus-1.0", "../../build/linux/debian_bullseye_amd64-sysroot/usr/lib/x86_64-linux-gnu/dbus-1.0/include" ], [], [ "dbus-1" ], []]
-      },
-      {
-        args = ["dri"]
-        pkgresult = [["../../build/linux/debian_bullseye_amd64-sysroot/usr/include/libdrm"], [], [], []]
-      },
-      {
-        args = ["egl"]
-        pkgresult = [[], [], [ "EGL" ], []]
-      },
-      {
-        args = ["gbm"]
-        pkgresult = [[], [], [ "gbm" ], []]
-      },
-      {
-        args = ["gio-2.0", "gio-unix-2.0"]
-        pkgresult = [["../../build/linux/debian_bullseye_amd64-sysroot/usr/include/glib-2.0", "../../build/linux/debian_bullseye_amd64-sysroot/usr/lib/x86_64-linux-gnu/glib-2.0/include", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/libmount", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/blkid", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/gio-unix-2.0" ], [], [ "gio-2.0", "gobject-2.0", "glib-2.0", ], []]
-      },
-      {
-        args = ["gl"]
-        pkgresult = [[], [], [ "GL" ], []]
-      },
-      {
-        args = ["gmodule-2.0", "gthread-2.0", "gtk+-3.0"]
-        pkgresult = [["../../build/linux/debian_bullseye_amd64-sysroot/usr/include/glib-2.0", "../../build/linux/debian_bullseye_amd64-sysroot/usr/lib/x86_64-linux-gnu/glib-2.0/include", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/gtk-3.0", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/pango-1.0", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/harfbuzz", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/freetype2", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/libpng16", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/libmount", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/blkid", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/fribidi", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/uuid", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/cairo", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/pixman-1", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/gdk-pixbuf-2.0", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/gio-unix-2.0", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/atk-1.0", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/at-spi2-atk/2.0", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/dbus-1.0", "../../build/linux/debian_bullseye_amd64-sysroot/usr/lib/x86_64-linux-gnu/dbus-1.0/include", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/at-spi-2.0"], [], ["gmodule-2.0", "glib-2.0", "gthread-2.0", "glib-2.0", "gtk-3", "gdk-3", "pangocairo-1.0", "pango-1.0", "harfbuzz", "atk-1.0", "cairo-gobject", "cairo", "gdk_pixbuf-2.0", "gio-2.0", "gobject-2.0", "glib-2.0"], []]
-      },
-      {
-        args = ["gmodule-2.0", "gthread-2.0", "gtk+-3.0", "gtk+-unix-print-3.0"]
-        pkgresult = [["../../build/linux/debian_bullseye_amd64-sysroot/usr/include/glib-2.0", "../../build/linux/debian_bullseye_amd64-sysroot/usr/lib/x86_64-linux-gnu/glib-2.0/include", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/gtk-3.0", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/pango-1.0", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/harfbuzz", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/freetype2", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/libpng16", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/libmount", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/blkid", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/fribidi", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/uuid", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/cairo", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/pixman-1", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/gdk-pixbuf-2.0", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/gio-unix-2.0", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/atk-1.0", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/at-spi2-atk/2.0", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/dbus-1.0", "../../build/linux/debian_bullseye_amd64-sysroot/usr/lib/x86_64-linux-gnu/dbus-1.0/include", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/at-spi-2.0", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/gtk-3.0/unix-print"], [], ["gmodule-2.0", "glib-2.0", "gthread-2.0", "glib-2.0", "gtk-3", "gdk-3", "pangocairo-1.0", "pango-1.0", "harfbuzz", "atk-1.0", "cairo-gobject", "cairo", "gdk_pixbuf-2.0", "gio-2.0", "gobject-2.0", "glib-2.0"], []]
-      },
-      {
-        args = ["gmodule-2.0", "gthread-2.0", "gtk4"]
-        pkgresult = [["../../build/linux/debian_bullseye_amd64-sysroot/usr/include/glib-2.0", "../../build/linux/debian_bullseye_amd64-sysroot/usr/lib/x86_64-linux-gnu/glib-2.0/include", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/gtk-4.0", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/pango-1.0", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/harfbuzz", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/freetype2", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/libpng16", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/libmount", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/blkid", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/fribidi", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/uuid", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/cairo", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/pixman-1", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/gdk-pixbuf-2.0", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/graphene-1.0", "../../build/linux/debian_bullseye_amd64-sysroot/usr/lib/x86_64-linux-gnu/graphene-1.0/include"], ["-mfpmath=sse", "-msse", "-msse2"], ["gmodule-2.0", "glib-2.0", "gthread-2.0", "glib-2.0", "gtk-4", "pangocairo-1.0", "pango-1.0", "harfbuzz", "gdk_pixbuf-2.0", "cairo-gobject", "cairo", "graphene-1.0", "gio-2.0", "gobject-2.0", "glib-2.0"], []]
-      },
-      {
-        args = ["gmodule-2.0", "gthread-2.0", "gtk4", "gtk4-unix-print"]
-        pkgresult = [["../../build/linux/debian_bullseye_amd64-sysroot/usr/include/glib-2.0", "../../build/linux/debian_bullseye_amd64-sysroot/usr/lib/x86_64-linux-gnu/glib-2.0/include", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/gtk-4.0", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/pango-1.0", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/harfbuzz", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/freetype2", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/libpng16", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/libmount", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/blkid", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/fribidi", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/uuid", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/cairo", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/pixman-1", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/gdk-pixbuf-2.0", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/graphene-1.0", "../../build/linux/debian_bullseye_amd64-sysroot/usr/lib/x86_64-linux-gnu/graphene-1.0/include", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/gtk-4.0/unix-print"], ["-mfpmath=sse", "-msse", "-msse2"], ["gmodule-2.0", "glib-2.0", "gthread-2.0", "glib-2.0", "gtk-4", "pangocairo-1.0", "pango-1.0", "harfbuzz", "gdk_pixbuf-2.0", "cairo-gobject", "cairo", "graphene-1.0", "gio-2.0", "gobject-2.0", "glib-2.0"], []]
-      },
-      {
-        args = ["glib-2.0", "gmodule-2.0", "gobject-2.0", "gthread-2.0"]
-        pkgresult = [["../../build/linux/debian_bullseye_amd64-sysroot/usr/include/glib-2.0", "../../build/linux/debian_bullseye_amd64-sysroot/usr/lib/x86_64-linux-gnu/glib-2.0/include"], [], ["gmodule-2.0", "glib-2.0", "gobject-2.0", "gthread-2.0", "glib-2.0"], []]
-      },
-      {
-        args = ["libdrm"]
-        pkgresult = [["../../build/linux/debian_bullseye_amd64-sysroot/usr/include/libdrm" ], [], [ "drm" ], []]
-      },
-      {
-        args = ["libffi"]
-        pkgresult = [[], [], ["ffi"], []]
-      },
-      {
-        args = ["libpipewire-0.3"]
-        pkgresult = [["../../build/linux/debian_bullseye_amd64-sysroot/usr/include/pipewire-0.3", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/spa-0.2" ], [ "-D_REENTRANT" ], [ "pipewire-0.3" ], []]
-      },
-      {
-        args = ["libudev"]
-        pkgresult = [[], [], ["udev"], []]
-      },
-      {
-        args = ["libva"]
-        pkgresult = [[], [], ["va"], []]
-      },
-      {
-        args = ["nss", "-v", "-lssl3"]
-        pkgresult = [["../../build/linux/debian_bullseye_amd64-sysroot/usr/include/nss", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/nspr" ], [], [ "nss3", "nssutil3", "smime3", "plds4", "plc4", "nspr4" ], []]
-      },
-      {
-        args = ["pangocairo", "-v", "freetype"]
-        pkgresult = [["../../build/linux/debian_bullseye_amd64-sysroot/usr/include/pango-1.0", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/glib-2.0", "../../build/linux/debian_bullseye_amd64-sysroot/usr/lib/x86_64-linux-gnu/glib-2.0/include", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/harfbuzz", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/libpng16", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/libmount", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/blkid", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/fribidi", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/uuid", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/cairo", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/pixman-1"], [], ["pangocairo-1.0", "pango-1.0", "gobject-2.0", "glib-2.0", "harfbuzz", "cairo"], []]
-      },
-      {
-        args = ["Qt5Core", "Qt5Widgets"]
-        pkgresult = [["../../build/linux/debian_bullseye_amd64-sysroot/usr/include/x86_64-linux-gnu/qt5/QtCore", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/x86_64-linux-gnu/qt5", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/x86_64-linux-gnu/qt5/QtWidgets", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/x86_64-linux-gnu/qt5/QtGui"], ["-DQT_WIDGETS_LIB", "-DQT_GUI_LIB", "-DQT_CORE_LIB"], ["Qt5Widgets", "Qt5Gui", "Qt5Core"], []]
-      },
-      {
-        args = ["Qt6Core", "Qt6Widgets"]
-        pkgresult = [["../../build/linux/debian_bullseye_amd64-sysroot/usr/include/x86_64-linux-gnu/qt6/QtCore", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/x86_64-linux-gnu/qt6", "../../build/linux/debian_bullseye_amd64-sysroot/usr/lib/x86_64-linux-gnu/qt6/mkspecs/linux-g++", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/x86_64-linux-gnu/qt6/QtWidgets", "../../build/linux/debian_bullseye_amd64-sysroot/usr/include/x86_64-linux-gnu/qt6/QtGui"], ["-DQT_WIDGETS_LIB", "-DQT_GUI_LIB", "-DQT_CORE_LIB"], ["Qt6Widgets", "Qt6Gui", "Qt6Core"], []]
-      },
-      {
-        args = ["xkbcommon"]
-        pkgresult = [[], [], ["xkbcommon"], []]
-      },
-    ]
-  },
-]
diff --git a/build/config/linux/pkg_config.gni b/build/config/linux/pkg_config.gni
index f8a4bbe..47fccd12 100644
--- a/build/config/linux/pkg_config.gni
+++ b/build/config/linux/pkg_config.gni
@@ -2,8 +2,6 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
-import("//build/config/compute_inputs_for_analyze.gni")
-import("//build/config/gclient_args.gni")
 import("//build/config/sysroot.gni")
 import("//build/toolchain/rbe.gni")
 
@@ -57,33 +55,11 @@
 pkg_config_args = []
 
 common_pkg_config_args = []
-
-# Chrome OS uses custom pkg_config wrappers that require absolute paths as inputs.
-# See https://chromium-review.googlesource.com/c/chromium/src/+/6506002
-_pkg_config_requires_abs_path =
-    pkg_config != "" ||
-    (current_toolchain == host_toolchain && host_pkg_config != "")
-
-# Chrome-adjacent repos might be using different sysroot versions (e.g. ANGLE).
-_enable_cache =
-    !_pkg_config_requires_abs_path && defined(build_with_chromium) &&
-    build_with_chromium && sysroot != ""
-
 if (sysroot != "") {
-  if (_pkg_config_requires_abs_path) {
-    _rebased_sysroot = rebase_path(sysroot)
-  } else {
-    _rebased_sysroot = rebase_path(sysroot, root_build_dir)
-  }
-  if (_enable_cache) {
-    _cached_results = read_file("cached_results.scope", "scope")
-    _cached_results = _cached_results.data
-  }
-
   # Pass the sysroot if we're using one (it requires the CPU arch also).
   common_pkg_config_args += [
     "-s",
-    _rebased_sysroot,
+    rebase_path(sysroot),
     "-a",
     current_cpu,
   ]
@@ -117,74 +93,30 @@
   assert(defined(invoker.packages),
          "Variable |packages| must be defined to be a list in pkg_config.")
   config(target_name) {
-    if (current_toolchain == host_toolchain) {
-      _cache_args = host_pkg_config_args + invoker.packages
+    if (host_toolchain == current_toolchain) {
+      args = common_pkg_config_args + host_pkg_config_args + invoker.packages
     } else {
-      _cache_args = pkg_config_args + invoker.packages
+      args = common_pkg_config_args + pkg_config_args + invoker.packages
     }
     if (defined(invoker.extra_args)) {
-      _cache_args += invoker.extra_args
+      args += invoker.extra_args
     }
 
-    # exec_script() is slow, so cache results.
-    if (_enable_cache) {
-      foreach(_entry, _cached_results) {
-        if (_rebased_sysroot == _entry.sysroot) {
-          foreach(_subentry, _entry.entries) {
-            if (_subentry.args == _cache_args) {
-              pkgresult = _subentry.pkgresult
-            }
-          }
-
-          # Do not expect cache hits when:
-          #  * No entries exist for the sysroot.
-          #  * It's a package outside of the core ones in //build/config/linux
-          _expected_cache_hit =
-              filter_include([ get_path_info(".", "abspath") ],
-                             [ "//build/config/linux/*" ]) != []
-          not_needed([ "_expected_cache_hit" ])
-        }
-      }
-    }
-
-    if (!defined(pkgresult)) {
-      _script_args = common_pkg_config_args + _cache_args
-      pkgresult = exec_script(pkg_config_script, _script_args, "json")
-      if (defined(_expected_cache_hit) && _expected_cache_hit) {
-        print("sysroot=$_rebased_sysroot")
-        print("      {")
-        print("        args = $_cache_args")
-        print("        pkgresult = $pkgresult")
-        print("      },")
-        assert(
-            false,
-            "Non-cached pkg_config query. Please update //build/config/linux/cached_results.scope with the above.")
-      }
-    } else if (compute_inputs_for_analyze) {
-      # Generally, only bots add this arg. Use it to ensure cache is not stale.
-      _script_args = common_pkg_config_args + _cache_args
-      _expected_pkgresult = exec_script(pkg_config_script, _script_args, "json")
-      if (pkgresult != _expected_pkgresult) {
-        print("sysroot=$_rebased_sysroot")
-        print("      {")
-        print("        args = $_cache_args")
-        print("        pkgresult = $_expected_pkgresult")
-        print("      },")
-        assert(
-            false,
-            "Outdated pkg_config query result detected. Please update //build/config/linux/cached_results.scope with the above.")
-      }
-    }
+    pkgresult = exec_script(pkg_config_script, args, "json")
     cflags = pkgresult[1]
 
-    foreach(_path, pkgresult[0]) {
-      if (_pkg_config_requires_abs_path && (use_sysroot || use_remoteexec)) {
-        _path = rebase_path(_path, root_build_dir)
-      }
-
+    foreach(include, pkgresult[0]) {
       # We want the system include paths to use -isystem instead of -I to
       # suppress warnings in those headers.
-      cflags += [ "-isystem$_path" ]
+      # When building remotely, we make the path relative just as for use_sysroot.
+      # This ensures we are using the header files that match the platform we are
+      # building for.
+      if (use_sysroot || use_remoteexec) {
+        include_relativized = rebase_path(include, root_build_dir)
+        cflags += [ "-isystem$include_relativized" ]
+      } else {
+        cflags += [ "-isystem$include" ]
+      }
     }
 
     if (!defined(invoker.ignore_libs) || !invoker.ignore_libs) {
diff --git a/cc/BUILD.gn b/cc/BUILD.gn
index 1e499d56..d12562f 100644
--- a/cc/BUILD.gn
+++ b/cc/BUILD.gn
@@ -755,7 +755,6 @@
     "base/tiling_data_unittest.cc",
     "base/unique_notifier_unittest.cc",
     "benchmarks/micro_benchmark_controller_unittest.cc",
-    "debug/rendering_stats_unittest.cc",
     "input/browser_controls_offset_manager_unittest.cc",
     "input/hit_test_opaqueness_unittest.cc",
     "input/main_thread_scrolling_reason_unittest.cc",
diff --git a/cc/debug/rendering_stats.cc b/cc/debug/rendering_stats.cc
index e60f39f..bc34a17db 100644
--- a/cc/debug/rendering_stats.cc
+++ b/cc/debug/rendering_stats.cc
@@ -8,37 +8,6 @@
 
 namespace cc {
 
-RenderingStats::TimeDeltaList::TimeDeltaList() = default;
-
-RenderingStats::TimeDeltaList::TimeDeltaList(TimeDeltaList&& other) = default;
-
-RenderingStats::TimeDeltaList& RenderingStats::TimeDeltaList::operator=(
-    TimeDeltaList&& other) = default;
-
-RenderingStats::TimeDeltaList::~TimeDeltaList() = default;
-
-void RenderingStats::TimeDeltaList::Append(base::TimeDelta value) {
-  values.push_back(value);
-}
-
-void RenderingStats::TimeDeltaList::AddToTracedValue(
-    const char* name,
-    base::trace_event::TracedValue* list_value) const {
-  list_value->BeginArray(name);
-  for (const auto& value : values) {
-    list_value->AppendDouble(value.InMillisecondsF());
-  }
-  list_value->EndArray();
-}
-
-void RenderingStats::TimeDeltaList::Add(const TimeDeltaList& other) {
-  values.insert(values.end(), other.values.begin(), other.values.end());
-}
-
-base::TimeDelta RenderingStats::TimeDeltaList::GetLastTimeDelta() const {
-  return values.empty() ? base::TimeDelta() : values.back();
-}
-
 RenderingStats::RenderingStats() = default;
 
 RenderingStats::RenderingStats(RenderingStats&& other) = default;
@@ -51,23 +20,9 @@
 RenderingStats::AsTraceableData() const {
   std::unique_ptr<base::trace_event::TracedValue> record_data(
       new base::trace_event::TracedValue());
-  record_data->SetInteger("frame_count", frame_count);
   record_data->SetInteger("visible_content_area", visible_content_area);
   record_data->SetInteger("approximated_visible_content_area",
                           approximated_visible_content_area);
-  draw_duration.AddToTracedValue("draw_duration_ms", record_data.get());
-
-  draw_duration_estimate.AddToTracedValue("draw_duration_estimate_ms",
-                                          record_data.get());
-
-  begin_main_frame_to_commit_duration.AddToTracedValue(
-      "begin_main_frame_to_commit_duration_ms", record_data.get());
-
-  commit_to_activate_duration.AddToTracedValue("commit_to_activate_duration_ms",
-                                               record_data.get());
-
-  commit_to_activate_duration_estimate.AddToTracedValue(
-      "commit_to_activate_duration_estimate_ms", record_data.get());
   return std::move(record_data);
 }
 
diff --git a/cc/debug/rendering_stats.h b/cc/debug/rendering_stats.h
index 95d9a4c..4c8aa0d 100644
--- a/cc/debug/rendering_stats.h
+++ b/cc/debug/rendering_stats.h
@@ -17,28 +17,6 @@
 namespace cc {
 
 struct CC_DEBUG_EXPORT RenderingStats {
-  // Stores a sequence of TimeDelta objects.
-  class CC_DEBUG_EXPORT TimeDeltaList {
-   public:
-    TimeDeltaList();
-    TimeDeltaList(const TimeDeltaList& other) = delete;
-    TimeDeltaList(TimeDeltaList&& other);
-    TimeDeltaList& operator=(const TimeDeltaList& other) = delete;
-    TimeDeltaList& operator=(TimeDeltaList&& other);
-    ~TimeDeltaList();
-
-    void Append(base::TimeDelta value);
-    void AddToTracedValue(const char* name,
-                          base::trace_event::TracedValue* list_value) const;
-
-    void Add(const TimeDeltaList& other);
-
-    base::TimeDelta GetLastTimeDelta() const;
-
-   private:
-    std::vector<base::TimeDelta> values;
-  };
-
   RenderingStats();
   RenderingStats(const RenderingStats& other) = delete;
   RenderingStats(RenderingStats&& other);
@@ -46,16 +24,9 @@
   RenderingStats& operator=(RenderingStats&& other);
   ~RenderingStats();
 
-  int64_t frame_count = 0;
   int64_t visible_content_area = 0;
   int64_t approximated_visible_content_area = 0;
 
-  TimeDeltaList draw_duration;
-  TimeDeltaList draw_duration_estimate;
-  TimeDeltaList begin_main_frame_to_commit_duration;
-  TimeDeltaList commit_to_activate_duration;
-  TimeDeltaList commit_to_activate_duration_estimate;
-
   std::unique_ptr<base::trace_event::ConvertableToTraceFormat> AsTraceableData()
       const;
 };
diff --git a/cc/debug/rendering_stats_instrumentation.cc b/cc/debug/rendering_stats_instrumentation.cc
index 3bf285b..c6cb37a 100644
--- a/cc/debug/rendering_stats_instrumentation.cc
+++ b/cc/debug/rendering_stats_instrumentation.cc
@@ -25,25 +25,14 @@
 RenderingStatsInstrumentation::~RenderingStatsInstrumentation() = default;
 
 RenderingStats RenderingStatsInstrumentation::TakeImplThreadRenderingStats() {
-  base::AutoLock scoped_lock(lock_);
   auto stats = std::move(impl_thread_rendering_stats_);
   impl_thread_rendering_stats_ = RenderingStats();
   return stats;
 }
 
-void RenderingStatsInstrumentation::IncrementFrameCount(int64_t count) {
-  if (!record_rendering_stats_)
-    return;
-
-  base::AutoLock scoped_lock(lock_);
-  impl_thread_rendering_stats_.frame_count += count;
-}
-
 void RenderingStatsInstrumentation::AddVisibleContentArea(int64_t area) {
   if (!record_rendering_stats_)
     return;
-
-  base::AutoLock scoped_lock(lock_);
   impl_thread_rendering_stats_.visible_content_area += area;
 }
 
@@ -51,44 +40,7 @@
     int64_t area) {
   if (!record_rendering_stats_)
     return;
-
-  base::AutoLock scoped_lock(lock_);
   impl_thread_rendering_stats_.approximated_visible_content_area += area;
 }
 
-void RenderingStatsInstrumentation::AddDrawDuration(
-    base::TimeDelta draw_duration,
-    base::TimeDelta draw_duration_estimate) {
-  if (!record_rendering_stats_)
-    return;
-
-  base::AutoLock scoped_lock(lock_);
-  impl_thread_rendering_stats_.draw_duration.Append(draw_duration);
-  impl_thread_rendering_stats_.draw_duration_estimate.Append(
-      draw_duration_estimate);
-}
-
-void RenderingStatsInstrumentation::AddBeginMainFrameToCommitDuration(
-    base::TimeDelta begin_main_frame_to_commit_duration) {
-  if (!record_rendering_stats_)
-    return;
-
-  base::AutoLock scoped_lock(lock_);
-  impl_thread_rendering_stats_.begin_main_frame_to_commit_duration.Append(
-      begin_main_frame_to_commit_duration);
-}
-
-void RenderingStatsInstrumentation::AddCommitToActivateDuration(
-    base::TimeDelta commit_to_activate_duration,
-    base::TimeDelta commit_to_activate_duration_estimate) {
-  if (!record_rendering_stats_)
-    return;
-
-  base::AutoLock scoped_lock(lock_);
-  impl_thread_rendering_stats_.commit_to_activate_duration.Append(
-      commit_to_activate_duration);
-  impl_thread_rendering_stats_.commit_to_activate_duration_estimate.Append(
-      commit_to_activate_duration_estimate);
-}
-
 }  // namespace cc
diff --git a/cc/debug/rendering_stats_instrumentation.h b/cc/debug/rendering_stats_instrumentation.h
index c144b03..b7103a4 100644
--- a/cc/debug/rendering_stats_instrumentation.h
+++ b/cc/debug/rendering_stats_instrumentation.h
@@ -38,16 +38,8 @@
       record_rendering_stats_ = record_rendering_stats;
   }
 
-  void IncrementFrameCount(int64_t count);
   void AddVisibleContentArea(int64_t area);
   void AddApproximatedVisibleContentArea(int64_t area);
-  void AddDrawDuration(base::TimeDelta draw_duration,
-                       base::TimeDelta draw_duration_estimate);
-  void AddBeginMainFrameToCommitDuration(
-      base::TimeDelta begin_main_frame_to_commit_duration);
-  void AddCommitToActivateDuration(
-      base::TimeDelta commit_to_activate_duration,
-      base::TimeDelta commit_to_activate_duration_estimate);
 
  protected:
   RenderingStatsInstrumentation();
@@ -56,8 +48,6 @@
   RenderingStats impl_thread_rendering_stats_;
 
   bool record_rendering_stats_;
-
-  base::Lock lock_;
 };
 
 }  // namespace cc
diff --git a/cc/debug/rendering_stats_unittest.cc b/cc/debug/rendering_stats_unittest.cc
deleted file mode 100644
index d0d458e..0000000
--- a/cc/debug/rendering_stats_unittest.cc
+++ /dev/null
@@ -1,50 +0,0 @@
-// Copyright 2014 The Chromium Authors
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#include <string>
-
-#include "base/time/time.h"
-#include "base/values.h"
-#include "cc/debug/rendering_stats.h"
-#include "testing/gtest/include/gtest/gtest.h"
-
-namespace cc {
-namespace {
-
-static std::string ToString(const RenderingStats::TimeDeltaList& list) {
-  base::trace_event::TracedValueJSON value;
-  list.AddToTracedValue("list_value", &value);
-  return value.ToJSON();
-}
-
-TEST(RenderingStatsTest, TimeDeltaListEmpty) {
-  RenderingStats::TimeDeltaList time_delta_list;
-  EXPECT_EQ("{\"list_value\":[]}", ToString(time_delta_list));
-}
-
-TEST(RenderingStatsTest, TimeDeltaListNonEmpty) {
-  RenderingStats::TimeDeltaList time_delta_list;
-  time_delta_list.Append(base::Milliseconds(234));
-  time_delta_list.Append(base::Milliseconds(827));
-
-  EXPECT_EQ("{\"list_value\":[234.0,827.0]}", ToString(time_delta_list));
-}
-
-TEST(RenderingStatsTest, TimeDeltaListAdd) {
-  RenderingStats::TimeDeltaList time_delta_list_a;
-  time_delta_list_a.Append(base::Milliseconds(810));
-  time_delta_list_a.Append(base::Milliseconds(32));
-
-  RenderingStats::TimeDeltaList time_delta_list_b;
-  time_delta_list_b.Append(base::Milliseconds(43));
-  time_delta_list_b.Append(base::Milliseconds(938));
-  time_delta_list_b.Append(base::Milliseconds(2));
-
-  time_delta_list_a.Add(time_delta_list_b);
-  EXPECT_EQ("{\"list_value\":[810.0,32.0,43.0,938.0,2.0]}",
-            ToString(time_delta_list_a));
-}
-
-}  // namespace
-}  // namespace cc
diff --git a/cc/metrics/compositor_timing_history.cc b/cc/metrics/compositor_timing_history.cc
index b34a0a0..d9785e5 100644
--- a/cc/metrics/compositor_timing_history.cc
+++ b/cc/metrics/compositor_timing_history.cc
@@ -13,23 +13,10 @@
 
 #include "base/memory/ptr_util.h"
 #include "base/metrics/histogram_macros.h"
-#include "base/notreached.h"
 #include "base/trace_event/trace_event.h"
-#include "cc/debug/rendering_stats_instrumentation.h"
-
 namespace cc {
-
 namespace {
 
-// Used to generate a unique id when emitting the "Long Draw Interval" trace
-// event.
-int g_num_long_draw_intervals = 0;
-
-// The threshold to emit a trace event is the 99th percentile
-// of the histogram on Windows Stable as of Feb 26th, 2020.
-constexpr base::TimeDelta kDrawIntervalTraceThreshold =
-    base::Microseconds(34478);
-
 // Using the 90th percentile will disable latency recovery
 // if we are missing the deadline approximately ~6 times per
 // second.
@@ -161,9 +148,7 @@
 
 }  // namespace
 
-CompositorTimingHistory::CompositorTimingHistory(
-    UMACategory uma_category,
-    RenderingStatsInstrumentation* rendering_stats_instrumentation)
+CompositorTimingHistory::CompositorTimingHistory(UMACategory uma_category)
     : enabled_(false),
       compositor_drawing_continuously_(false),
       begin_main_frame_queue_duration_history_(kDurationHistorySize),
@@ -177,8 +162,7 @@
       activate_duration_history_(kDurationHistorySize),
       draw_duration_history_(kDurationHistorySize),
       pending_tree_is_impl_side_(false),
-      uma_category_(uma_category),
-      rendering_stats_instrumentation_(rendering_stats_instrumentation) {}
+      uma_category_(uma_category) {}
 
 CompositorTimingHistory::~CompositorTimingHistory() = default;
 
@@ -339,15 +323,9 @@
   if (begin_main_frame_start_time_.is_null())
     begin_main_frame_start_time_ = begin_main_frame_sent_time_;
 
-  base::TimeDelta bmf_sent_to_commit_duration =
-      begin_main_frame_end_time - begin_main_frame_sent_time_;
-  base::TimeDelta bmf_queue_duration =
-      begin_main_frame_start_time_ - begin_main_frame_sent_time_;
-
-  rendering_stats_instrumentation_->AddBeginMainFrameToCommitDuration(
-      bmf_sent_to_commit_duration);
-
   if (enabled_) {
+    base::TimeDelta bmf_queue_duration =
+        begin_main_frame_start_time_ - begin_main_frame_sent_time_;
     begin_main_frame_queue_duration_history_.InsertSample(bmf_queue_duration);
     if (begin_main_frame_on_critical_path_) {
       begin_main_frame_queue_duration_critical_history_.InsertSample(
@@ -378,23 +356,11 @@
   DCHECK_EQ(pending_tree_ready_to_activate_time_, base::TimeTicks());
 
   pending_tree_ready_to_activate_time_ = Now();
-  if (!pending_tree_is_impl_side_) {
+  if (!pending_tree_is_impl_side_ && enabled_) {
     base::TimeDelta time_since_commit =
         pending_tree_ready_to_activate_time_ - pending_tree_creation_time_;
-
-    // Before adding the new data point to the timing history, see what we would
-    // have predicted for this frame. This allows us to keep track of the
-    // accuracy of our predictions.
-
-    base::TimeDelta commit_to_ready_to_activate_estimate =
-        CommitToReadyToActivateDurationEstimate();
-    rendering_stats_instrumentation_->AddCommitToActivateDuration(
-        time_since_commit, commit_to_ready_to_activate_estimate);
-
-    if (enabled_) {
-      commit_to_ready_to_activate_duration_history_.InsertSample(
-          time_since_commit);
-    }
+    commit_to_ready_to_activate_duration_history_.InsertSample(
+        time_since_commit);
   }
 }
 
@@ -428,39 +394,16 @@
 void CompositorTimingHistory::DidDraw() {
   DCHECK_NE(base::TimeTicks(), draw_start_time_);
   base::TimeTicks draw_end_time = Now();
-  base::TimeDelta draw_duration = draw_end_time - draw_start_time_;
-
-  // Before adding the new data point to the timing history, see what we would
-  // have predicted for this frame. This allows us to keep track of the accuracy
-  // of our predictions.
-  base::TimeDelta draw_estimate = DrawDurationEstimate();
-  rendering_stats_instrumentation_->AddDrawDuration(draw_duration,
-                                                    draw_estimate);
-
   if (enabled_) {
+    base::TimeDelta draw_duration = draw_end_time - draw_start_time_;
     draw_duration_history_.InsertSample(draw_duration);
   }
 
   SetCompositorDrawingContinuously(true);
-  if (!draw_end_time_prev_.is_null()) {
+  if (!draw_end_time_prev_.is_null() && uma_category_ == RENDERER_UMA) {
     base::TimeDelta draw_interval = draw_end_time - draw_end_time_prev_;
-    if (uma_category_ == RENDERER_UMA) {
-      UMA_HISTOGRAM_CUSTOM_TIMES_VSYNC_ALIGNED(
-          "Scheduling.Renderer.DrawInterval", draw_interval);
-    }
-    // Emit a trace event to highlight a long time lapse between the draw times
-    // of back-to-back BeginImplFrames.
-    if (draw_interval > kDrawIntervalTraceThreshold) {
-      TRACE_EVENT_NESTABLE_ASYNC_BEGIN_WITH_TIMESTAMP0(
-          "latency", "Long Draw Interval",
-          TRACE_ID_WITH_SCOPE("Long Draw Interval", g_num_long_draw_intervals),
-          draw_start_time_);
-      TRACE_EVENT_NESTABLE_ASYNC_END_WITH_TIMESTAMP0(
-          "latency", "Long Draw Interval",
-          TRACE_ID_WITH_SCOPE("Long Draw Interval", g_num_long_draw_intervals),
-          draw_end_time);
-      g_num_long_draw_intervals++;
-    }
+    UMA_HISTOGRAM_CUSTOM_TIMES_VSYNC_ALIGNED("Scheduling.Renderer.DrawInterval",
+                                             draw_interval);
   }
   draw_end_time_prev_ = draw_end_time;
 
diff --git a/cc/metrics/compositor_timing_history.h b/cc/metrics/compositor_timing_history.h
index d19e0ed..ff6a2c1 100644
--- a/cc/metrics/compositor_timing_history.h
+++ b/cc/metrics/compositor_timing_history.h
@@ -22,7 +22,6 @@
 }  // namespace perfetto
 
 namespace cc {
-class RenderingStatsInstrumentation;
 
 class CC_EXPORT CompositorTimingHistory {
  public:
@@ -33,9 +32,7 @@
   };
   class UMAReporter;
 
-  CompositorTimingHistory(
-      UMACategory uma_category,
-      RenderingStatsInstrumentation* rendering_stats_instrumentation);
+  explicit CompositorTimingHistory(UMACategory uma_category);
   CompositorTimingHistory(const CompositorTimingHistory&) = delete;
   virtual ~CompositorTimingHistory();
 
@@ -134,10 +131,6 @@
   bool pending_tree_is_impl_side_;
 
   const UMACategory uma_category_;
-
-  // Owned by LayerTreeHost and is destroyed when LayerTreeHost is destroyed.
-  raw_ptr<RenderingStatsInstrumentation, DanglingUntriaged>
-      rendering_stats_instrumentation_;
 };
 
 }  // namespace cc
diff --git a/cc/metrics/compositor_timing_history_unittest.cc b/cc/metrics/compositor_timing_history_unittest.cc
index 9e86198..6e4f011d 100644
--- a/cc/metrics/compositor_timing_history_unittest.cc
+++ b/cc/metrics/compositor_timing_history_unittest.cc
@@ -9,7 +9,6 @@
 #include "base/memory/raw_ptr.h"
 #include "base/time/time.h"
 #include "cc/base/features.h"
-#include "cc/debug/rendering_stats_instrumentation.h"
 #include "cc/metrics/dropped_frame_counter.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
@@ -20,9 +19,8 @@
 
 class TestCompositorTimingHistory : public CompositorTimingHistory {
  public:
-  TestCompositorTimingHistory(CompositorTimingHistoryTest* test,
-                              RenderingStatsInstrumentation* rendering_stats)
-      : CompositorTimingHistory(RENDERER_UMA, rendering_stats), test_(test) {}
+  explicit TestCompositorTimingHistory(CompositorTimingHistoryTest* test)
+      : CompositorTimingHistory(RENDERER_UMA), test_(test) {}
 
   TestCompositorTimingHistory(const TestCompositorTimingHistory&) = delete;
   TestCompositorTimingHistory& operator=(const TestCompositorTimingHistory&) =
@@ -36,9 +34,7 @@
 
 class CompositorTimingHistoryTest : public testing::Test {
  public:
-  CompositorTimingHistoryTest()
-      : rendering_stats_(RenderingStatsInstrumentation::Create()),
-        timing_history_(this, rendering_stats_.get()) {
+  CompositorTimingHistoryTest() : timing_history_(this) {
     AdvanceNowBy(base::Milliseconds(1));
     timing_history_.SetRecordingEnabled(true);
   }
@@ -48,7 +44,6 @@
   base::TimeTicks Now() { return now_; }
 
  protected:
-  std::unique_ptr<RenderingStatsInstrumentation> rendering_stats_;
   TestCompositorTimingHistory timing_history_;
   base::TimeTicks now_;
   uint64_t sequence_number = 0;
diff --git a/cc/test/scheduler_test_common.cc b/cc/test/scheduler_test_common.cc
index 72d44bc..ad1e697 100644
--- a/cc/test/scheduler_test_common.cc
+++ b/cc/test/scheduler_test_common.cc
@@ -12,28 +12,19 @@
 #include "base/memory/ptr_util.h"
 #include "base/task/single_thread_task_runner.h"
 #include "base/time/tick_clock.h"
-#include "cc/debug/rendering_stats_instrumentation.h"
 
 namespace cc {
 
 std::unique_ptr<FakeCompositorTimingHistory>
 FakeCompositorTimingHistory::Create(
     bool using_synchronous_renderer_compositor) {
-  std::unique_ptr<RenderingStatsInstrumentation>
-      rendering_stats_instrumentation = RenderingStatsInstrumentation::Create();
-  return base::WrapUnique(new FakeCompositorTimingHistory(
-      using_synchronous_renderer_compositor,
-      std::move(rendering_stats_instrumentation)));
+  return base::WrapUnique(
+      new FakeCompositorTimingHistory(using_synchronous_renderer_compositor));
 }
 
 FakeCompositorTimingHistory::FakeCompositorTimingHistory(
-    bool using_synchronous_renderer_compositor,
-    std::unique_ptr<RenderingStatsInstrumentation>
-        rendering_stats_instrumentation)
-    : CompositorTimingHistory(CompositorTimingHistory::NULL_UMA,
-                              rendering_stats_instrumentation.get()),
-      rendering_stats_instrumentation_owned_(
-          std::move(rendering_stats_instrumentation)) {}
+    bool using_synchronous_renderer_compositor)
+    : CompositorTimingHistory(CompositorTimingHistory::NULL_UMA) {}
 
 FakeCompositorTimingHistory::~FakeCompositorTimingHistory() = default;
 
diff --git a/cc/test/scheduler_test_common.h b/cc/test/scheduler_test_common.h
index c5fd0b6..0164dd8 100644
--- a/cc/test/scheduler_test_common.h
+++ b/cc/test/scheduler_test_common.h
@@ -24,8 +24,6 @@
 
 namespace cc {
 
-class RenderingStatsInstrumentation;
-
 class FakeCompositorTimingHistory : public CompositorTimingHistory {
  public:
   static std::unique_ptr<FakeCompositorTimingHistory> Create(
@@ -60,12 +58,8 @@
   base::TimeDelta DrawDurationEstimate() const override;
 
  protected:
-  FakeCompositorTimingHistory(bool using_synchronous_renderer_compositor,
-                              std::unique_ptr<RenderingStatsInstrumentation>
-                                  rendering_stats_instrumentation_owned);
-
-  std::unique_ptr<RenderingStatsInstrumentation>
-      rendering_stats_instrumentation_owned_;
+  explicit FakeCompositorTimingHistory(
+      bool using_synchronous_renderer_compositor);
 
   base::TimeDelta begin_main_frame_queue_duration_critical_;
   base::TimeDelta begin_main_frame_queue_duration_not_critical_;
diff --git a/cc/trees/layer_tree_host_impl.cc b/cc/trees/layer_tree_host_impl.cc
index 3bf8070..ce09752 100644
--- a/cc/trees/layer_tree_host_impl.cc
+++ b/cc/trees/layer_tree_host_impl.cc
@@ -3036,8 +3036,6 @@
 
 viz::CompositorFrame LayerTreeHostImpl::GenerateCompositorFrame(
     FrameData* frame) {
-  rendering_stats_instrumentation_->IncrementFrameCount(1);
-
   if (!settings_.trees_in_viz_in_viz_process) {
     memory_history_->SaveEntry(tile_manager_.memory_stats_from_last_assign());
   }
diff --git a/cc/trees/proxy_impl.cc b/cc/trees/proxy_impl.cc
index 77c2bf5..56e29d6 100644
--- a/cc/trees/proxy_impl.cc
+++ b/cc/trees/proxy_impl.cc
@@ -25,7 +25,6 @@
 #include "cc/base/devtools_instrumentation.h"
 #include "cc/base/features.h"
 #include "cc/benchmarks/benchmark_instrumentation.h"
-#include "cc/debug/rendering_stats_instrumentation.h"
 #include "cc/input/browser_controls_offset_manager.h"
 #include "cc/input/browser_controls_offset_tag_modifications.h"
 #include "cc/metrics/compositor_timing_history.h"
@@ -103,13 +102,11 @@
   base::WeakPtr<ProxyMain> proxy_main_weak_ptr_;
 };
 
-ProxyImpl::ProxyImpl(
-    base::WeakPtr<ProxyMain> proxy_main_weak_ptr,
-    LayerTreeHost* layer_tree_host,
-    int id,
-    const LayerTreeSettings* settings,
-    RenderingStatsInstrumentation* rendering_stats_instrumentation,
-    TaskRunnerProvider* task_runner_provider)
+ProxyImpl::ProxyImpl(base::WeakPtr<ProxyMain> proxy_main_weak_ptr,
+                     LayerTreeHost* layer_tree_host,
+                     int id,
+                     const LayerTreeSettings* settings,
+                     TaskRunnerProvider* task_runner_provider)
     : layer_tree_host_id_(id),
       next_frame_is_newly_committed_frame_(false),
       inside_draw_(false),
@@ -130,9 +127,7 @@
   scheduler_settings.main_frame_before_commit_enabled = true;
 
   std::unique_ptr<CompositorTimingHistory> compositor_timing_history(
-      new CompositorTimingHistory(
-          CompositorTimingHistory::RENDERER_UMA,
-          rendering_stats_instrumentation));
+      new CompositorTimingHistory(CompositorTimingHistory::RENDERER_UMA));
   scheduler_ = std::make_unique<Scheduler>(
       this, scheduler_settings, layer_tree_host_id_,
       task_runner_provider_->ImplThreadTaskRunner(),
diff --git a/cc/trees/proxy_impl.h b/cc/trees/proxy_impl.h
index 20f1cda5..593007a 100644
--- a/cc/trees/proxy_impl.h
+++ b/cc/trees/proxy_impl.h
@@ -42,7 +42,6 @@
 class PaintWorkletLayerPainter;
 class ProxyMain;
 class RenderFrameMetadataObserver;
-class RenderingStatsInstrumentation;
 class ScopedCommitCompletionEvent;
 class SwapPromise;
 class TaskRunnerProvider;
@@ -58,7 +57,6 @@
             LayerTreeHost* layer_tree_host,
             int id,
             const LayerTreeSettings* settings,
-            RenderingStatsInstrumentation* rendering_stats_instrumentation,
             TaskRunnerProvider* task_runner_provider);
   ProxyImpl(const ProxyImpl&) = delete;
   ~ProxyImpl() override;
diff --git a/cc/trees/proxy_main.cc b/cc/trees/proxy_main.cc
index e09350b0..5f1f79c 100644
--- a/cc/trees/proxy_main.cc
+++ b/cc/trees/proxy_main.cc
@@ -21,8 +21,6 @@
 #include "cc/base/completion_event.h"
 #include "cc/base/devtools_instrumentation.h"
 #include "cc/base/features.h"
-#include "cc/benchmarks/benchmark_instrumentation.h"
-#include "cc/debug/rendering_stats_instrumentation.h"
 #include "cc/input/browser_controls_offset_tag_modifications.h"
 #include "cc/paint/paint_worklet_layer_painter.h"
 #include "cc/resources/ui_resource_manager.h"
@@ -64,16 +62,14 @@
   DCHECK(!started_);
 }
 
-void ProxyMain::InitializeOnImplThread(
-    CompletionEvent* completion_event,
-    int id,
-    const LayerTreeSettings* settings,
-    RenderingStatsInstrumentation* rendering_stats_instrumentation) {
+void ProxyMain::InitializeOnImplThread(CompletionEvent* completion_event,
+                                       int id,
+                                       const LayerTreeSettings* settings) {
   DCHECK(task_runner_provider_->IsImplThread());
   DCHECK(!proxy_impl_);
-  proxy_impl_ = std::make_unique<ProxyImpl>(
-      weak_factory_.GetWeakPtr(), layer_tree_host_, id, settings,
-      rendering_stats_instrumentation, task_runner_provider_);
+  proxy_impl_ =
+      std::make_unique<ProxyImpl>(weak_factory_.GetWeakPtr(), layer_tree_host_,
+                                  id, settings, task_runner_provider_);
   completion_event->Signal();
 }
 
@@ -753,12 +749,10 @@
     DebugScopedSetMainThreadBlocked main_thread_blocked(task_runner_provider_);
     CompletionEvent completion;
     ImplThreadTaskRunner()->PostTask(
-        FROM_HERE,
-        base::BindOnce(&ProxyMain::InitializeOnImplThread,
-                       base::Unretained(this), &completion,
-                       layer_tree_host_->GetId(),
-                       &layer_tree_host_->GetSettings(),
-                       layer_tree_host_->rendering_stats_instrumentation()));
+        FROM_HERE, base::BindOnce(&ProxyMain::InitializeOnImplThread,
+                                  base::Unretained(this), &completion,
+                                  layer_tree_host_->GetId(),
+                                  &layer_tree_host_->GetSettings()));
     completion.Wait();
   }
 
diff --git a/cc/trees/proxy_main.h b/cc/trees/proxy_main.h
index b572b6d..8b8f6f3 100644
--- a/cc/trees/proxy_main.h
+++ b/cc/trees/proxy_main.h
@@ -149,11 +149,9 @@
   bool IsImplThread() const;
   base::SingleThreadTaskRunner* ImplThreadTaskRunner();
 
-  void InitializeOnImplThread(
-      CompletionEvent* completion_event,
-      int id,
-      const LayerTreeSettings* settings,
-      RenderingStatsInstrumentation* rendering_stats_instrumentation);
+  void InitializeOnImplThread(CompletionEvent* completion_event,
+                              int id,
+                              const LayerTreeSettings* settings);
   void DestroyProxyImplOnImplThread(CompletionEvent* completion_event);
 
   raw_ptr<LayerTreeHost> layer_tree_host_;
diff --git a/cc/trees/single_thread_proxy.cc b/cc/trees/single_thread_proxy.cc
index 3519ed5..21bb178 100644
--- a/cc/trees/single_thread_proxy.cc
+++ b/cc/trees/single_thread_proxy.cc
@@ -94,9 +94,7 @@
     scheduler_settings.commit_to_active_tree = true;
 
     std::unique_ptr<CompositorTimingHistory> compositor_timing_history(
-        new CompositorTimingHistory(
-            CompositorTimingHistory::BROWSER_UMA,
-            layer_tree_host_->rendering_stats_instrumentation()));
+        new CompositorTimingHistory(CompositorTimingHistory::BROWSER_UMA));
     scheduler_on_impl_thread_ = std::make_unique<Scheduler>(
         this, scheduler_settings, layer_tree_host_->GetId(),
         task_runner_provider_->MainThreadTaskRunner(),
diff --git a/chrome/VERSION b/chrome/VERSION
index 82941a09..2fe67a9 100644
--- a/chrome/VERSION
+++ b/chrome/VERSION
@@ -1,4 +1,4 @@
 MAJOR=138
 MINOR=0
-BUILD=7177
+BUILD=7178
 PATCH=0
diff --git a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGroupListRenderTest.java b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGroupListRenderTest.java
index 5e6ad71..1c8f49a 100644
--- a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGroupListRenderTest.java
+++ b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGroupListRenderTest.java
@@ -26,7 +26,6 @@
 import org.chromium.chrome.browser.ChromeTabbedActivity;
 import org.chromium.chrome.browser.flags.ChromeFeatureList;
 import org.chromium.chrome.browser.flags.ChromeSwitches;
-import org.chromium.chrome.browser.hub.PaneId;
 import org.chromium.chrome.browser.tab.Tab;
 import org.chromium.chrome.browser.tab.TabLaunchType;
 import org.chromium.chrome.browser.tabmodel.TabGroupModelFilter;
@@ -70,8 +69,7 @@
         createGroupProgrammatic("Group 1", /* wait= */ false);
 
         RegularTabSwitcherStation tabSwitcher = firstPage.openRegularTabSwitcher();
-        TabGroupPaneStation tabGroupPane =
-                tabSwitcher.selectPane(PaneId.TAB_GROUPS, TabGroupPaneStation.class);
+        TabGroupPaneStation tabGroupPane = tabSwitcher.selectTabGroupsPane();
 
         RecyclerView recyclerView = tabGroupPane.recyclerViewElement.get();
         mRenderTestRule.render(recyclerView, "1_group");
@@ -82,7 +80,7 @@
         createGroupProgrammatic("Group 3", /* wait= */ true);
         mRenderTestRule.render(recyclerView, "3_groups");
 
-        tabSwitcher = tabGroupPane.selectPane(PaneId.TAB_SWITCHER, RegularTabSwitcherStation.class);
+        tabSwitcher = tabGroupPane.selectRegularTabsPane();
 
         // Exit to reset.
         TabSwitcherAppMenuFacility appMenu = tabSwitcher.openAppMenu();
diff --git a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabSwitcherPanePublicTransitTest.java b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabSwitcherPanePublicTransitTest.java
index 223515a4..b4e76258 100644
--- a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabSwitcherPanePublicTransitTest.java
+++ b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabSwitcherPanePublicTransitTest.java
@@ -102,13 +102,13 @@
         IncognitoTabSwitcherStation incognitoTabSwitcher = incognitoNtp.openIncognitoTabSwitcher();
         onView(RegularTabSwitcherStation.EMPTY_STATE_TEXT.getViewMatcher()).check(doesNotExist());
 
-        RegularTabSwitcherStation regularTabSwitcher = incognitoTabSwitcher.selectRegularTabList();
+        RegularTabSwitcherStation regularTabSwitcher = incognitoTabSwitcher.selectRegularTabsPane();
 
         regularTabSwitcher = regularTabSwitcher.closeTabAtIndex(0, RegularTabSwitcherStation.class);
         onView(RegularTabSwitcherStation.EMPTY_STATE_TEXT.getViewMatcher())
                 .check(matches(isDisplayed()));
 
-        incognitoTabSwitcher = regularTabSwitcher.selectIncognitoTabList();
+        incognitoTabSwitcher = regularTabSwitcher.selectIncognitoTabsPane();
         onView(RegularTabSwitcherStation.EMPTY_STATE_TEXT.getViewMatcher()).check(doesNotExist());
 
         regularTabSwitcher =
diff --git a/chrome/android/features/tab_ui/javatests/src/org/chromium/chrome/browser/tasks/tab_management/TabSwitcherLayoutPTTest.java b/chrome/android/features/tab_ui/javatests/src/org/chromium/chrome/browser/tasks/tab_management/TabSwitcherLayoutPTTest.java
index e29b617..4aa989a0 100644
--- a/chrome/android/features/tab_ui/javatests/src/org/chromium/chrome/browser/tasks/tab_management/TabSwitcherLayoutPTTest.java
+++ b/chrome/android/features/tab_ui/javatests/src/org/chromium/chrome/browser/tasks/tab_management/TabSwitcherLayoutPTTest.java
@@ -689,13 +689,19 @@
         IncognitoTabSwitcherStation incognitoTabSwitcherStation = incognitoPage.openIncognitoTabSwitcher();
         // Load URL in Regular Model
         mCtaTestRule.loadUrlInTab(
-                mCtaTestRule.getTestServer().getURL(TEST_URL), PageTransition.TYPED | PageTransition.FROM_ADDRESS_BAR, regularTab);
+                mCtaTestRule.getTestServer().getURL(TEST_URL),
+                PageTransition.TYPED | PageTransition.FROM_ADDRESS_BAR,
+                regularTab);
 
-        RegularTabSwitcherStation regularTabSwitcherStation = incognitoTabSwitcherStation.selectRegularTabList();
+        RegularTabSwitcherStation regularTabSwitcherStation =
+                incognitoTabSwitcherStation.selectRegularTabsPane();
         // Load URL in Incognito Model
         mCtaTestRule.loadUrlInTab(
-                mCtaTestRule.getTestServer().getURL(TEST_URL), PageTransition.TYPED | PageTransition.FROM_ADDRESS_BAR, incognitoTab);
+                mCtaTestRule.getTestServer().getURL(TEST_URL),
+                PageTransition.TYPED | PageTransition.FROM_ADDRESS_BAR,
+                incognitoTab);
 
-        regularTabSwitcherStation.selectTabAtIndex(0, WebPageStation.newBuilder().withExpectedUrlSubstring(TEST_URL));
+        regularTabSwitcherStation.selectTabAtIndex(
+                0, WebPageStation.newBuilder().withExpectedUrlSubstring(TEST_URL));
     }
 }
diff --git a/chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/FeedSurfaceCoordinator.java b/chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/FeedSurfaceCoordinator.java
index a9783c4..d456d96 100644
--- a/chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/FeedSurfaceCoordinator.java
+++ b/chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/FeedSurfaceCoordinator.java
@@ -1108,7 +1108,6 @@
             TextView titleView = (TextView) mHeaderView.findViewById(R.id.header_title);
             if (titleView != null) {
                 titleView.setText(headerText);
-                titleView.setContentDescription(headerText);
             }
         }
     }
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/hub/HubLayoutPublicTransitTest.java b/chrome/android/java/src/org/chromium/chrome/browser/hub/HubLayoutPublicTransitTest.java
index 2c43ab85..8d97b15e 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/hub/HubLayoutPublicTransitTest.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/hub/HubLayoutPublicTransitTest.java
@@ -86,12 +86,8 @@
                         .openNewIncognitoTabFast()
                         .openIncognitoTabSwitcher();
 
-        RegularTabSwitcherStation regularTabSwitcher =
-                incognitoTabSwitcher.selectPane(
-                        PaneId.TAB_SWITCHER, RegularTabSwitcherStation.class);
-        incognitoTabSwitcher =
-                regularTabSwitcher.selectPane(
-                        PaneId.INCOGNITO_TAB_SWITCHER, IncognitoTabSwitcherStation.class);
+        RegularTabSwitcherStation regularTabSwitcher = incognitoTabSwitcher.selectRegularTabsPane();
+        incognitoTabSwitcher = regularTabSwitcher.selectIncognitoTabsPane();
 
         // Go back to a PageStation for BlankCTATabInitialStateRule to reset state.
         incognitoTabSwitcher.selectTabAtIndex(0, IncognitoNewTabPageStation.newBuilder());
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/ntp/RecentTabCountDrawable.java b/chrome/android/java/src/org/chromium/chrome/browser/ntp/RecentTabCountDrawable.java
index bcecbf0e..7613fdd 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/ntp/RecentTabCountDrawable.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/ntp/RecentTabCountDrawable.java
@@ -17,6 +17,8 @@
 
 import androidx.appcompat.content.res.AppCompatResources;
 
+import org.chromium.build.annotations.EnsuresNonNull;
+import org.chromium.build.annotations.NullMarked;
 import org.chromium.chrome.R;
 import org.chromium.ui.UiUtils;
 
@@ -25,8 +27,9 @@
 /**
  * Class for drawing a tab count icon on the Recent Tabs Page for bulk tab closures.
  *
- * Loosely based on {@link TabSwitcherDrawable} and modified to handle an SVG asset.
+ * <p>Loosely based on {@link TabSwitcherDrawable} and modified to handle an SVG asset.
  */
+@NullMarked
 public class RecentTabCountDrawable extends DrawableWrapper {
     // Avoid allocations during draw by pre-allocating a rect.
     private final Rect mTextBounds = new Rect();
@@ -69,6 +72,7 @@
         invalidateSelf();
     }
 
+    @EnsuresNonNull("mTint")
     public void setTint(ColorStateList tint) {
         if (mTint == tint) return;
         mTint = tint;
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/ntp/RecentTabsExpandableListView.java b/chrome/android/java/src/org/chromium/chrome/browser/ntp/RecentTabsExpandableListView.java
index ec2f386ea..5874326 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/ntp/RecentTabsExpandableListView.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/ntp/RecentTabsExpandableListView.java
@@ -9,12 +9,14 @@
 import android.view.View;
 import android.widget.ExpandableListView;
 
+import org.chromium.build.annotations.NullMarked;
 import org.chromium.ui.base.DeviceFormFactor;
 
 /**
- * Customized ExpandableListView for the recent tabs page. This class handles tablet-specific
- * layout implementation.
+ * Customized ExpandableListView for the recent tabs page. This class handles tablet-specific layout
+ * implementation.
  */
+@NullMarked
 public class RecentTabsExpandableListView extends ExpandableListView {
     private static final int MAX_LIST_VIEW_WIDTH_DP = 550;
 
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/ntp/RecentTabsGroupView.java b/chrome/android/java/src/org/chromium/chrome/browser/ntp/RecentTabsGroupView.java
index fcd1d93..046d024 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/ntp/RecentTabsGroupView.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/ntp/RecentTabsGroupView.java
@@ -13,15 +13,17 @@
 import android.widget.RelativeLayout;
 import android.widget.TextView;
 
+import org.chromium.build.annotations.NullMarked;
 import org.chromium.chrome.R;
 import org.chromium.chrome.browser.recent_tabs.ForeignSessionHelper.ForeignSession;
 import org.chromium.components.browser_ui.widget.TintedDrawable;
 
 /**
- * Header view shown above each group of items on the Recent Tabs page. Shows the name of the
- * group (e.g. "Recently closed" or "Jim's Laptop"), an icon, last synced time, and a button to
- * expand or collapse the group.
+ * Header view shown above each group of items on the Recent Tabs page. Shows the name of the group
+ * (e.g. "Recently closed" or "Jim's Laptop"), an icon, last synced time, and a button to expand or
+ * collapse the group.
  */
+@NullMarked
 public class RecentTabsGroupView extends RelativeLayout {
 
     /** Drawable levels for the device type icon and the expand/collapse arrow. */
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/ntp/RecentTabsPagePrefs.java b/chrome/android/java/src/org/chromium/chrome/browser/ntp/RecentTabsPagePrefs.java
index 5687186..b2e67d22 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/ntp/RecentTabsPagePrefs.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/ntp/RecentTabsPagePrefs.java
@@ -7,10 +7,12 @@
 import org.jni_zero.JniType;
 import org.jni_zero.NativeMethods;
 
+import org.chromium.build.annotations.NullMarked;
 import org.chromium.chrome.browser.profiles.Profile;
 import org.chromium.chrome.browser.recent_tabs.ForeignSessionHelper.ForeignSession;
 
 /** Allows Java code to read and modify preferences related to the {@link RecentTabsPage}. */
+@NullMarked
 class RecentTabsPagePrefs {
     private long mNativePrefs;
 
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/ntp/RecentlyClosedEntry.java b/chrome/android/java/src/org/chromium/chrome/browser/ntp/RecentlyClosedEntry.java
index 59c2b2a5..09ef539 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/ntp/RecentlyClosedEntry.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/ntp/RecentlyClosedEntry.java
@@ -4,9 +4,12 @@
 
 package org.chromium.chrome.browser.ntp;
 
+import org.chromium.build.annotations.NullMarked;
+
 import java.util.Date;
 
 /** Represents a recently closed entry from TabRestoreService. */
+@NullMarked
 public class RecentlyClosedEntry {
     private final int mSessionId;
     private final Date mDate;
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/tabmodel/TabModelImpl.java b/chrome/android/java/src/org/chromium/chrome/browser/tabmodel/TabModelImpl.java
index e3ce040..3a32ae9 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/tabmodel/TabModelImpl.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/tabmodel/TabModelImpl.java
@@ -663,6 +663,12 @@
 
     @Override
     public boolean closeTabs(TabClosureParams tabClosureParams) {
+        if (!tabClosureParams.allowUndo) {
+            // The undo stacks assumes that previous actions in the stack are undoable. If an entry
+            // is not undoable then the reversal of the operations may fail or yield an invalid
+            // state. Commit the rest of the closures now to ensure that doesn't occur.
+            commitAllTabClosures();
+        }
         // TODO(crbug.com/356445932): Respect the provided params more broadly.
         switch (tabClosureParams.tabCloseType) {
             case TabCloseType.SINGLE:
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/app/appmenu/OverviewAppMenuTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/app/appmenu/OverviewAppMenuTest.java
index 76cf98b5..04d13f3 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/app/appmenu/OverviewAppMenuTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/app/appmenu/OverviewAppMenuTest.java
@@ -137,7 +137,7 @@
             menu.verifyPresentItems();
         } finally {
             menu.closeProgrammatically();
-            incognitoTabSwitcher.selectRegularTabList();
+            incognitoTabSwitcher.selectRegularTabsPane();
         }
     }
 
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/history/HistoryPaneTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/history/HistoryPaneTest.java
index c8ee19c..3a3f283 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/history/HistoryPaneTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/history/HistoryPaneTest.java
@@ -4,20 +4,7 @@
 
 package org.chromium.chrome.browser.history;
 
-import static androidx.test.espresso.Espresso.onView;
-import static androidx.test.espresso.action.ViewActions.click;
-import static androidx.test.espresso.action.ViewActions.replaceText;
-import static androidx.test.espresso.assertion.ViewAssertions.matches;
-import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
-import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription;
-import static androidx.test.espresso.matcher.ViewMatchers.withId;
-import static androidx.test.espresso.matcher.ViewMatchers.withText;
-
-import static org.hamcrest.CoreMatchers.allOf;
-import static org.hamcrest.CoreMatchers.containsString;
-
 import static org.chromium.base.ThreadUtils.runOnUiThreadBlocking;
-import static org.chromium.ui.test.util.ViewUtils.onViewWaiting;
 
 import androidx.test.filters.MediumTest;
 
@@ -28,9 +15,7 @@
 
 import org.chromium.base.test.util.Batch;
 import org.chromium.base.test.util.CommandLineFlags;
-import org.chromium.base.test.util.CriteriaHelper;
 import org.chromium.base.test.util.Features.EnableFeatures;
-import org.chromium.chrome.R;
 import org.chromium.chrome.browser.ChromeTabbedActivity;
 import org.chromium.chrome.browser.browsing_data.BrowsingDataBridge;
 import org.chromium.chrome.browser.browsing_data.BrowsingDataType;
@@ -41,8 +26,10 @@
 import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
 import org.chromium.chrome.test.transit.AutoResetCtaTransitTestRule;
 import org.chromium.chrome.test.transit.ChromeTransitTestRules;
+import org.chromium.chrome.test.transit.hub.HistoryPaneStation;
+import org.chromium.chrome.test.transit.hub.HistoryPaneStation.HistorySearchFacility;
+import org.chromium.chrome.test.transit.hub.HistoryPaneStation.HistoryWithEntriesFacility;
 import org.chromium.chrome.test.transit.hub.RegularTabSwitcherStation;
-import org.chromium.chrome.test.transit.hub.TabSwitcherStation;
 import org.chromium.chrome.test.transit.page.WebPageStation;
 
 /** Public transit tests for the Hub's history pane. */
@@ -69,14 +56,7 @@
     @MediumTest
     public void testEmptyView() {
         RegularTabSwitcherStation tabSwitcher = mStartingPage.openRegularTabSwitcher();
-        enterHistoryPane(tabSwitcher);
-
-        onViewWaiting(withText("You’ll find your history here")).check(matches(isDisplayed()));
-        onViewWaiting(
-                        withText(
-                                "You can see the pages you’ve visited or delete them from your"
-                                        + " history"))
-                .check(matches(isDisplayed()));
+        tabSwitcher.selectHistoryPane().expectEmptyState();
     }
 
     @Test
@@ -91,10 +71,9 @@
                         .loadWebPageProgrammatically(urlOne)
                         .loadWebPageProgrammatically(urlTwo)
                         .openRegularTabSwitcher();
-        enterHistoryPane(tabSwitcher);
-
-        onViewWaiting(withText("One")).check(matches(isDisplayed()));
-        onViewWaiting(withText("Two")).check(matches(isDisplayed()));
+        HistoryWithEntriesFacility history = tabSwitcher.selectHistoryPane().expectEntries();
+        history.expectEntry("One");
+        history.expectEntry("Two");
     }
 
     @Test
@@ -109,17 +88,18 @@
                         .loadWebPageProgrammatically(urlOne)
                         .loadWebPageProgrammatically(urlTwo)
                         .openRegularTabSwitcher();
-        enterHistoryPane(tabSwitcher);
-
-        onViewWaiting(withText("One")).check(matches(isDisplayed()));
-        onViewWaiting(withText("Two")).check(matches(isDisplayed()));
+        HistoryPaneStation historyPaneStation = tabSwitcher.selectHistoryPane();
+        HistoryWithEntriesFacility history = historyPaneStation.expectEntries();
+        history.expectEntry("One");
+        history.expectEntry("Two");
 
         // Search for "One" in the history search box.
-        onView(withId(R.id.search_menu_id)).perform(click());
-        onView(withId(R.id.search_text)).perform(replaceText("One"));
+        HistorySearchFacility search = history.openSearch();
+        search.typeSearchTerm("One");
 
         // Verify that "One" is displayed as a match.
-        onViewWaiting(allOf(withText("One"), withId(R.id.title))).check(matches(isDisplayed()));
+        history.expectEntry("One");
+        history.expectNoEntry("Two");
     }
 
     @Test
@@ -129,33 +109,13 @@
                 mCtaTestRule.getTestServer().getURL("/chrome/test/data/android/navigate/one.html");
         String urlTwo =
                 mCtaTestRule.getTestServer().getURL("/chrome/test/data/android/navigate/two.html");
-        RegularTabSwitcherStation tabSwitcher =
+        WebPageStation page =
                 mStartingPage
                         .loadWebPageProgrammatically(urlOne)
-                        .loadWebPageProgrammatically(urlTwo)
-                        .openRegularTabSwitcher();
-        enterHistoryPane(tabSwitcher);
-
-        onViewWaiting(withText("One")).perform(click());
-        // When the history view is clicked, it should replace the current tab's URL.
-        CriteriaHelper.pollUiThread(
-                () ->
-                        urlOne.equals(
-                                mCtaTestRule
-                                        .getActivity()
-                                        .getTabModelSelector()
-                                        .getCurrentTab()
-                                        .getUrl()
-                                        .getSpec()));
-    }
-
-    private void enterHistoryPane(TabSwitcherStation tabSwitcher) {
-        onView(
-                        tabSwitcher
-                                .paneSwitcherElement
-                                .descendant(withContentDescription(containsString("History")))
-                                .getViewMatcher())
-                .perform(click());
+                        .loadWebPageProgrammatically(urlTwo);
+        HistoryWithEntriesFacility history =
+                page.openRegularTabSwitcher().selectHistoryPane().expectEntries();
+        history.expectEntry("One").selectToOpenWebPage(page, urlOne);
     }
 
     private void clearHistory(Profile profile) {
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/toolbar/top/TabSwitcherActionMenuBatchedPTTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/toolbar/top/TabSwitcherActionMenuBatchedPTTest.java
index 996916a..dc6db5f 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/toolbar/top/TabSwitcherActionMenuBatchedPTTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/toolbar/top/TabSwitcherActionMenuBatchedPTTest.java
@@ -131,7 +131,7 @@
 
         // Return to one non-incognito blank tab
         IncognitoTabSwitcherStation incognitoTabSwitcher =
-                regularTabSwitcher.selectIncognitoTabList();
+                regularTabSwitcher.selectIncognitoTabsPane();
         regularTabSwitcher =
                 incognitoTabSwitcher.closeTabAtIndex(0, RegularTabSwitcherStation.class);
         blankPage = regularTabSwitcher.openNewTab().loadAboutBlank();
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/tabmodel/UndoTabModelUnitTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/tabmodel/UndoTabModelUnitTest.java
index a755e8b..6175c1b 100644
--- a/chrome/android/junit/src/org/chromium/chrome/browser/tabmodel/UndoTabModelUnitTest.java
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/tabmodel/UndoTabModelUnitTest.java
@@ -690,6 +690,28 @@
         checkState(model, sEmptyList, null, sEmptyList, sEmptyList, null);
     }
 
+    @Test
+    @SmallTest
+    public void testTwoTabsOneNonUndoableOperation() throws TimeoutException {
+        final boolean isIncognito = false;
+        final TabModel model = createTabModel(isIncognito);
+        createTab(model, isIncognito);
+        createTab(model, isIncognito);
+
+        Tab tab0 = model.getTabAt(0);
+        Tab tab1 = model.getTabAt(1);
+
+        Tab[] fullList = new Tab[] {tab0, tab1};
+
+        checkState(model, new Tab[] {tab0, tab1}, tab1, sEmptyList, fullList, tab1);
+
+        closeTab(model, tab0, true);
+        checkState(model, new Tab[] {tab1}, tab1, new Tab[] {tab0}, fullList, tab1);
+
+        closeTab(model, tab1, false);
+        checkState(model, sEmptyList, null, sEmptyList, sEmptyList, null);
+    }
+
     /**
      * Test restoring in the same order of closing with the following actions/expected states:
      *     Action                     Model List         Close List        Comprehensive List
diff --git a/chrome/app/profiles_strings.grdp b/chrome/app/profiles_strings.grdp
index 10c6aa4..d9c8e5b4 100644
--- a/chrome/app/profiles_strings.grdp
+++ b/chrome/app/profiles_strings.grdp
@@ -1128,7 +1128,12 @@
     <message translateable="false" name="IDS_HISTORY_SYNC_OPT_IN_DESCRIPTION" desc="">
       You can stop syncing anytime in settings. Google may personalize Search and other services based on your history.
     </message>
-
+    <message translateable="false" name="IDS_HISTORY_SYNC_OPT_IN_ACCEPT_BUTTON" desc="">
+      Yes, I 'm in
+    </message>
+    <message translateable="false" name="IDS_HISTORY_SYNC_OPT_IN_CANCEL_BUTTON" desc="">
+      No thanks
+    </message>
     <!-- History Sync Opt-in Expansion Pill Desktop -->
     <message name="IDS_AVATAR_BUTTON_BROWSE_ACROSS_DEVICES" desc="The avatar button label for the history sync promo. When clicked, it opens the profile menu with the sync button.">
       Browse across devices?
diff --git a/chrome/browser/BUILD.gn b/chrome/browser/BUILD.gn
index 153faee8..c5a28069 100644
--- a/chrome/browser/BUILD.gn
+++ b/chrome/browser/BUILD.gn
@@ -1419,7 +1419,6 @@
     "serial/serial_blocklist.cc",
     "serial/serial_blocklist.h",
     "serial/serial_chooser_context.cc",
-    "serial/serial_chooser_context.h",
     "serial/serial_chooser_context_factory.cc",
     "serial/serial_chooser_context_factory.h",
     "serial/serial_chooser_histograms.h",
@@ -1831,6 +1830,9 @@
     # be removed.
     "//chrome/browser/ui/cookie_controls:impl",
 
+    # TODO(417228688): Remove this dependency when chrome/browser/serial get modularized.
+    "//chrome/browser/ui/serial:impl",
+
     # TODO(crbug.com/40110173): Eliminate usages of browser.h from Media Router.
     "//chrome/browser/media/router",
 
@@ -1998,6 +2000,7 @@
     "//chrome/browser/search_engine_choice:impl",
     "//chrome/browser/search_engines",
     "//chrome/browser/send_tab_to_self",
+    "//chrome/browser/serial",
     "//chrome/browser/share",
     "//chrome/browser/sharing:buildflags",
     "//chrome/browser/signin",
@@ -2048,6 +2051,11 @@
     "//chrome/browser/ui/safety_hub",
     "//chrome/browser/ui/safety_hub:impl",
     "//chrome/browser/ui/search_engines",
+
+    # TODO(417228688): Remove this dependency when both chrome/browser/serial and
+    # chrome/browser/profiles/profile_manager.h get modularized.
+    "//chrome/browser/ui/serial",
+    "//chrome/browser/ui/serial:impl",
     "//chrome/browser/ui/startup:startup_tab",
     "//chrome/browser/ui/tab_contents",
     "//chrome/browser/ui/tab_contents:impl",
@@ -4384,6 +4392,7 @@
       "//chrome/browser/ui/tabs:tab_strip_impl",
       "//chrome/browser/ui/tabs/tab_group_home",
       "//chrome/browser/ui/tabs/tab_group_home:impl",
+      "//chrome/browser/ui/tabs/tab_strip_api",
       "//chrome/browser/ui/tabs/tab_strip_api:impl",
       "//chrome/browser/ui/toasts",
       "//chrome/browser/ui/toasts/api:toasts",
@@ -4813,6 +4822,8 @@
       "metrics/chromeos_metrics_provider.h",
       "metrics/chromeos_system_profile_provider.cc",
       "metrics/chromeos_system_profile_provider.h",
+      "metrics/class_management_enabled_metrics_provider.cc",
+      "metrics/class_management_enabled_metrics_provider.h",
       "metrics/cros_healthd_metrics_provider.cc",
       "metrics/cros_healthd_metrics_provider.h",
       "metrics/cros_pre_consent_metrics_manager.cc",
@@ -8564,6 +8575,7 @@
     "//chrome/browser/v8_compile_hints/proto",
     "//chrome/browser/web_applications/mojom:mojom_web_apps_enum",
     "//components/data_sharing/data_sharing_internals/webui:mojo_bindings",
+    "//components/webui/chrome_urls/mojom:mojo_bindings",
   ]
   if (is_android) {
     public_deps += [
@@ -8586,6 +8598,7 @@
       "//chrome/browser/support_tool:support_tool_proto",
       "//chrome/browser/sync_file_system/drive_backend:sync_file_system_drive_proto",
       "//chrome/browser/ui:webui_name_variants",
+      "//chrome/browser/ui/tabs/tab_strip_api:mojom",
       "//chrome/browser/ui/webui/access_code_cast:mojo_bindings",
       "//chrome/browser/ui/webui/app_service_internals:mojo_bindings",
       "//chrome/browser/ui/webui/data_sharing:mojo_bindings",
diff --git a/chrome/browser/about_flags.cc b/chrome/browser/about_flags.cc
index f0c304a..84d3f6e 100644
--- a/chrome/browser/about_flags.cc
+++ b/chrome/browser/about_flags.cc
@@ -6641,13 +6641,6 @@
      FEATURE_VALUE_TYPE(omnibox_feature_configs::ContextualSearch::
                             kContextualSearchBoxUsesContextualSearchProvider)},
 
-    {"omnibox-contextual-search-actions-at-top",
-     flag_descriptions::kOmniboxContextualSearchActionsAtTopName,
-     flag_descriptions::kOmniboxContextualSearchActionsAtTopDescription,
-     kOsDesktop,
-     FEATURE_VALUE_TYPE(omnibox_feature_configs::ContextualSearch::
-                            kOmniboxContextualSearchActionsAtTop)},
-
     {"omnibox-contextual-search-on-focus-suggestions",
      flag_descriptions::kOmniboxContextualSearchOnFocusSuggestionsName,
      flag_descriptions::kOmniboxContextualSearchOnFocusSuggestionsDescription,
@@ -6658,13 +6651,6 @@
          kOmniboxContextualSearchOnFocusSuggestionsVariations,
          "OmniboxContextualSearchOnFocusSuggestions")},
 
-    {"omnibox-contextual-search-single-lens-action",
-     flag_descriptions::kOmniboxContextualSearchSingleLensActionName,
-     flag_descriptions::kOmniboxContextualSearchSingleLensActionDescription,
-     kOsDesktop,
-     FEATURE_VALUE_TYPE(omnibox_feature_configs::ContextualSearch::
-                            kOmniboxContextualSearchSingleLensAction)},
-
     {"omnibox-contextual-suggestions",
      flag_descriptions::kOmniboxContextualSuggestionsName,
      flag_descriptions::kOmniboxContextualSuggestionsDescription, kOsDesktop,
diff --git a/chrome/browser/ai/ai_create_on_device_session_task.cc b/chrome/browser/ai/ai_create_on_device_session_task.cc
index 1f545ac..2a3096d 100644
--- a/chrome/browser/ai/ai_create_on_device_session_task.cc
+++ b/chrome/browser/ai/ai_create_on_device_session_task.cc
@@ -165,36 +165,3 @@
   DCHECK_STATE_TRANSITION(transitions, state_, state);
   state_ = state;
 }
-
-CreateLanguageModelOnDeviceSessionTask::CreateLanguageModelOnDeviceSessionTask(
-    AIManager& ai_manager,
-    AIContextBoundObjectSet& context_bound_object_set,
-    content::BrowserContext* browser_context,
-    optimization_guide::SamplingParams sampling_params,
-    on_device_model::Capabilities capabilities,
-    base::OnceCallback<
-        void(std::unique_ptr<
-             optimization_guide::OptimizationGuideModelExecutor::Session>)>
-        completion_callback)
-    : CreateOnDeviceSessionTask(
-          context_bound_object_set,
-          browser_context,
-          optimization_guide::ModelBasedCapabilityKey::kPromptApi),
-      sampling_params_(std::move(sampling_params)),
-      capabilities_(capabilities),
-      completion_callback_(std::move(completion_callback)) {}
-
-CreateLanguageModelOnDeviceSessionTask::
-    ~CreateLanguageModelOnDeviceSessionTask() = default;
-
-void CreateLanguageModelOnDeviceSessionTask::OnFinish(
-    std::unique_ptr<optimization_guide::OptimizationGuideModelExecutor::Session>
-        session) {
-  std::move(completion_callback_).Run(std::move(session));
-}
-
-void CreateLanguageModelOnDeviceSessionTask::UpdateSessionConfigParams(
-    optimization_guide::SessionConfigParams* config_params) {
-  config_params->sampling_params = sampling_params_;
-  config_params->capabilities = capabilities_;
-}
diff --git a/chrome/browser/ai/ai_create_on_device_session_task.h b/chrome/browser/ai/ai_create_on_device_session_task.h
index 2cdd465..71250068 100644
--- a/chrome/browser/ai/ai_create_on_device_session_task.h
+++ b/chrome/browser/ai/ai_create_on_device_session_task.h
@@ -11,8 +11,6 @@
 #include "components/optimization_guide/core/optimization_guide_model_executor.h"
 #include "services/on_device_model/public/cpp/capabilities.h"
 
-class AIManager;
-
 // A base class for tasks which create an on-device session.
 class CreateOnDeviceSessionTask
     : public AIContextBoundObject,
@@ -99,44 +97,4 @@
   base::WeakPtrFactory<CreateOnDeviceSessionTask> weak_factory_{this};
 };
 
-// Implementation of the `CreateOnDeviceSessionTask` base class for
-// `AILanguageModel`.
-class CreateLanguageModelOnDeviceSessionTask
-    : public CreateOnDeviceSessionTask {
- public:
-  CreateLanguageModelOnDeviceSessionTask(
-      AIManager& ai_manager,
-      AIContextBoundObjectSet& context_bound_object_set,
-      content::BrowserContext* browser_context,
-      optimization_guide::SamplingParams sampling_params,
-      on_device_model::Capabilities capabilities,
-      base::OnceCallback<
-          void(std::unique_ptr<
-               optimization_guide::OptimizationGuideModelExecutor::Session>)>
-          completion_callback);
-  ~CreateLanguageModelOnDeviceSessionTask() override;
-
-  CreateLanguageModelOnDeviceSessionTask(
-      const CreateLanguageModelOnDeviceSessionTask&) = delete;
-  CreateLanguageModelOnDeviceSessionTask& operator=(
-      const CreateLanguageModelOnDeviceSessionTask&) = delete;
-
- protected:
-  // `CreateOnDeviceSessionTask` implementation.
-  void OnFinish(std::unique_ptr<
-                optimization_guide::OptimizationGuideModelExecutor::Session>
-                    session) override;
-
-  void UpdateSessionConfigParams(
-      optimization_guide::SessionConfigParams* config_params) override;
-
- private:
-  const optimization_guide::SamplingParams sampling_params_;
-  const on_device_model::Capabilities capabilities_;
-  base::OnceCallback<void(
-      std::unique_ptr<
-          optimization_guide::OptimizationGuideModelExecutor::Session>)>
-      completion_callback_;
-};
-
 #endif  // CHROME_BROWSER_AI_AI_CREATE_ON_DEVICE_SESSION_TASK_H_
diff --git a/chrome/browser/ai/ai_language_model.cc b/chrome/browser/ai/ai_language_model.cc
index f32d6a9..79f48bbe 100644
--- a/chrome/browser/ai/ai_language_model.cc
+++ b/chrome/browser/ai/ai_language_model.cc
@@ -20,6 +20,7 @@
 #include "chrome/browser/ai/ai_utils.h"
 #include "components/optimization_guide/core/model_execution/multimodal_message.h"
 #include "components/optimization_guide/core/model_execution/optimization_guide_model_execution_error.h"
+#include "components/optimization_guide/core/model_execution/substitution.h"
 #include "components/optimization_guide/core/optimization_guide_enums.h"
 #include "components/optimization_guide/core/optimization_guide_features.h"
 #include "components/optimization_guide/core/optimization_guide_model_executor.h"
@@ -27,6 +28,7 @@
 #include "components/optimization_guide/proto/common_types.pb.h"
 #include "components/optimization_guide/proto/features/prompt_api.pb.h"
 #include "components/optimization_guide/proto/string_value.pb.h"
+#include "mojo/public/cpp/bindings/callback_helpers.h"
 #include "mojo/public/cpp/bindings/message.h"
 #include "services/on_device_model/public/cpp/capabilities.h"
 #include "third_party/blink/public/common/features_generated.h"
@@ -37,110 +39,328 @@
 
 namespace {
 
-using optimization_guide::MultimodalMessage;
-using optimization_guide::MultimodalMessageReadView;
-using optimization_guide::proto::PromptApiMetadata;
-using optimization_guide::proto::PromptApiPrompt;
-using optimization_guide::proto::PromptApiRequest;
-using optimization_guide::proto::PromptApiRole;
+using ::optimization_guide::proto::PromptApiMetadata;
 
-PromptApiRole ConvertRole(blink::mojom::AILanguageModelPromptRole role) {
+ml::Token ConvertToToken(blink::mojom::AILanguageModelPromptRole role) {
   switch (role) {
     case blink::mojom::AILanguageModelPromptRole::kSystem:
-      return PromptApiRole::PROMPT_API_ROLE_SYSTEM;
+      return ml::Token::kSystem;
     case blink::mojom::AILanguageModelPromptRole::kUser:
-      return PromptApiRole::PROMPT_API_ROLE_USER;
+      return ml::Token::kUser;
     case blink::mojom::AILanguageModelPromptRole::kAssistant:
-      return PromptApiRole::PROMPT_API_ROLE_ASSISTANT;
+      return ml::Token::kModel;
   }
 }
 
-blink::mojom::AILanguageModelPromptPtr MakeTextPrompt(
-    blink::mojom::AILanguageModelPromptRole role,
-    const std::string& text) {
-  return blink::mojom::AILanguageModelPrompt::New(
-      role, blink::mojom::AILanguageModelPromptContent::NewText(text));
-}
-
-// Construct an empty multimodal PromptApiRequest message.
-MultimodalMessage EmptyMessage() {
-  return MultimodalMessage((PromptApiRequest()));
-}
-
-void AddPromptToField(
-    const blink::mojom::AILanguageModelPrompt& prompt,
-    optimization_guide::RepeatedMultimodalMessageEditView view,
+on_device_model::mojom::InputPtr ConvertToInput(
+    const std::vector<blink::mojom::AILanguageModelPromptPtr>& prompts,
     const on_device_model::Capabilities& capabilities) {
-  PromptApiPrompt prompt_proto;
-  prompt_proto.set_role(ConvertRole(prompt.role));
-  auto prompt_view = view.Add(prompt_proto);
-  if (prompt.content->is_text()) {
-    prompt_view.Set(PromptApiPrompt::kTextFieldNumber,
-                    prompt.content->get_text());
-  } else if (prompt.content->is_bitmap()) {
-    if (!capabilities.Has(on_device_model::CapabilityFlags::kImageInput)) {
-      mojo::ReportBadMessage("Image input is not supported.");
-      return;
+  auto input = on_device_model::mojom::Input::New();
+  for (const auto& prompt : prompts) {
+    input->pieces.push_back(ConvertToToken(prompt->role));
+    switch (prompt->content->which()) {
+      case blink::mojom::AILanguageModelPromptContent::Tag::kText:
+        input->pieces.push_back(prompt->content->get_text());
+        break;
+      case blink::mojom::AILanguageModelPromptContent::Tag::kBitmap:
+        if (!capabilities.Has(on_device_model::CapabilityFlags::kImageInput)) {
+          return nullptr;
+        }
+        input->pieces.push_back(prompt->content->get_bitmap());
+        break;
+      case blink::mojom::AILanguageModelPromptContent::Tag::kAudio:
+        if (!capabilities.Has(on_device_model::CapabilityFlags::kAudioInput)) {
+          return nullptr;
+        }
+        // TODO: Export services/on_device_model/ml/chrome_ml_types_traits.cc.
+        const on_device_model::mojom::AudioDataPtr& audio_data =
+            prompt->content->get_audio();
+        ml::AudioBuffer audio_buffer;
+        audio_buffer.sample_rate_hz = audio_data->sample_rate;
+        audio_buffer.num_channels = audio_data->channel_count;
+        audio_buffer.num_frames = audio_data->frame_count;
+        audio_buffer.data = audio_data->data;
+        input->pieces.push_back(std::move(audio_buffer));
+        break;
     }
-    prompt_view.Set(PromptApiPrompt::kMediaFieldNumber,
-                    prompt.content->get_bitmap());
-  } else if (prompt.content->is_audio()) {
-    if (!capabilities.Has(on_device_model::CapabilityFlags::kAudioInput)) {
-      mojo::ReportBadMessage("Audio input is not supported.");
-      return;
-    }
-    // TODO: Export services/on_device_model/ml/chrome_ml_types_traits.cc.
-    const on_device_model::mojom::AudioDataPtr& audio_data =
-        prompt.content->get_audio();
-    ml::AudioBuffer audio_buffer;
-    audio_buffer.sample_rate_hz = audio_data->sample_rate;
-    audio_buffer.num_channels = audio_data->channel_count;
-    audio_buffer.num_frames = audio_data->frame_count;
-    audio_buffer.data = audio_data->data;
-    prompt_view.Set(PromptApiPrompt::kMediaFieldNumber,
-                    std::move(audio_buffer));
-  } else {
-    NOTREACHED();
+    input->pieces.push_back(ml::Token::kEnd);
   }
+  return input;
 }
 
-// Fill the 'view'ed Repeated<PromptApiPrompt> field with the prompts of 'item'.
-void AddPrompts(optimization_guide::RepeatedMultimodalMessageEditView view,
-                const AILanguageModel::Context::ContextItem& item,
-                const on_device_model::Capabilities& capabilities) {
-  for (const auto& prompt : item.prompts) {
-    AddPromptToField(*prompt, view, capabilities);
-  }
-}
-
-// Construct an multimodal PromptApiRequest with initial prompts from 'item'.
-MultimodalMessage MakeInitialPrompt(
-    const AILanguageModel::Context::ContextItem& item,
+on_device_model::mojom::InputPtr ConvertToInputForExecute(
+    const std::vector<blink::mojom::AILanguageModelPromptPtr>& prompts,
     const on_device_model::Capabilities& capabilities) {
-  MultimodalMessage request = EmptyMessage();
-  AddPrompts(request.edit().MutableRepeatedField(
-                 PromptApiRequest::kInitialPromptsFieldNumber),
-             item, capabilities);
-  return request;
+  auto input = ConvertToInput(prompts, capabilities);
+  if (!input) {
+    return nullptr;
+  }
+  input->pieces.push_back(ml::Token::kModel);
+  return input;
 }
 
-// Add the prompts from 'item' to the current_prompts field of 'request'.
-void AddCurrentRequest(MultimodalMessage& request,
-                       const AILanguageModel::Context::ContextItem& item,
-                       const on_device_model::Capabilities& capabilities) {
-  AddPrompts(request.edit().MutableRepeatedField(
-                 PromptApiRequest::kCurrentPromptsFieldNumber),
-             item, capabilities);
+on_device_model::mojom::AppendOptionsPtr MakeAppendOptions(
+    on_device_model::mojom::InputPtr input) {
+  auto append_options = on_device_model::mojom::AppendOptions::New();
+  append_options->input = std::move(input);
+  return append_options;
+}
+
+optimization_guide::MultimodalMessage CreateStringMessage(
+    const on_device_model::mojom::Input& input) {
+  optimization_guide::proto::StringValue value;
+  value.set_value(optimization_guide::OnDeviceInputToString(input));
+  return optimization_guide::MultimodalMessage(value);
 }
 
 }  // namespace
 
+// Contains state for a currently active prompt call. Makes sure everything is
+// properly cancelled if needed.
+class AILanguageModel::PromptState
+    : public on_device_model::mojom::StreamingResponder,
+      public on_device_model::mojom::ContextClient {
+ public:
+  // Constructor used for input+output generation.
+  PromptState(
+      mojo::PendingRemote<blink::mojom::ModelStreamingResponder> responder,
+      on_device_model::mojom::InputPtr input,
+      on_device_model::mojom::ResponseConstraintPtr constraint,
+      optimization_guide::SafetyChecker& safety_checker)
+      : responder_(std::move(responder)),
+        input_(std::move(input)),
+        constraint_(std::move(constraint)),
+        safety_checker_(safety_checker) {
+    responder_.set_disconnect_handler(
+        base::BindOnce(&PromptState::OnDisconnect, base::Unretained(this)));
+  }
+
+  // Constructor used for just input processing.
+  PromptState(mojo::PendingRemote<blink::mojom::AILanguageModelAppendClient>
+                  append_client,
+              on_device_model::mojom::InputPtr input,
+              optimization_guide::SafetyChecker& safety_checker)
+      : append_client_(std::move(append_client)),
+        input_(std::move(input)),
+        safety_checker_(safety_checker) {
+    append_client_.set_disconnect_handler(
+        base::BindOnce(&PromptState::OnDisconnect, base::Unretained(this)));
+  }
+
+  ~PromptState() override {
+    OnError(blink::mojom::ModelStreamingResponseStatus::kErrorCancelled);
+  }
+
+  // Appends input and generates a response on `session`. `callback` will be
+  // called on completion or error, with the full response and number of
+  // input+output tokens. `callback` may delete this object.
+  void AppendAndGenerate(
+      mojo::PendingRemote<on_device_model::mojom::Session> session,
+      base::OnceClosure callback) {
+    callback_ = std::move(callback);
+    safety_checker_->RunRequestChecks(
+        CreateStringMessage(*input_),
+        base::BindOnce(&PromptState::RequestSafetyChecksComplete,
+                       weak_factory_.GetWeakPtr(), std::move(session)));
+  }
+
+  void OnError(blink::mojom::ModelStreamingResponseStatus error) {
+    if (responder_) {
+      responder_->OnError(error);
+    }
+    if (append_client_) {
+      append_client_->OnError(error);
+    }
+    session_.reset();
+    responder_.reset();
+    context_receiver_.reset();
+    response_receiver_.reset();
+    if (callback_) {
+      std::move(callback_).Run();
+      // `this` may be deleted.
+    }
+  }
+
+  void OnQuotaOverflow() {
+    if (responder_) {
+      responder_->OnQuotaOverflow();
+    }
+    if (append_client_) {
+      append_client_->OnQuotaOverflow();
+    }
+  }
+
+  void SetPriority(on_device_model::mojom::Priority priority) {
+    if (session_) {
+      session_->SetPriority(priority);
+    }
+  }
+
+  bool IsValid() const { return responder_ || append_client_; }
+
+  mojo::Remote<on_device_model::mojom::Session> TakeSession() {
+    return std::move(session_);
+  }
+
+  mojo::Remote<blink::mojom::ModelStreamingResponder> TakeResponder() {
+    return std::move(responder_);
+  }
+
+  on_device_model::mojom::InputPtr TakeInput() { return std::move(input_); }
+  const std::string& response() const { return full_response_; }
+  // The total token count for this request including input and output tokens.
+  uint32_t token_count() const { return token_count_; }
+
+ private:
+  void OnDisconnect() {
+    OnError(blink::mojom::ModelStreamingResponseStatus::kErrorGenericFailure);
+  }
+
+  // on_device_model::mojom::ContextClient:
+  void OnComplete(uint32_t tokens_processed) override {
+    context_receiver_.reset();
+    token_count_ = tokens_processed;
+    if (append_client_) {
+      append_client_->OnAppendComplete();
+      if (callback_) {
+        std::move(callback_).Run();
+        // `this` may be deleted.
+      }
+    }
+  }
+
+  // on_device_model::mojom::StreamingResponder:
+  void OnResponse(on_device_model::mojom::ResponseChunkPtr chunk) override {
+    output_tokens_++;
+    full_response_ += chunk->text;
+
+    unchecked_output_tokens_++;
+    unchecked_response_ += chunk->text;
+
+    if (!safety_checker_->safety_cfg().CanCheckPartialOutput(
+            output_tokens_, unchecked_output_tokens_)) {
+      return;
+    }
+    safety_checker_->RunRawOutputCheck(
+        full_response_, optimization_guide::ResponseCompleteness::kPartial,
+        base::BindOnce(&PromptState::OnPartialResponseCheckComplete,
+                       weak_factory_.GetWeakPtr(),
+                       std::move(unchecked_response_)));
+    unchecked_output_tokens_ = 0;
+    unchecked_response_ = "";
+  }
+
+  void OnComplete(on_device_model::mojom::ResponseSummaryPtr summary) override {
+    // The `OnComplete()` method on `responder_` will be called in
+    // `AILanguageModel::PromptOutputComplete()` after adding the response to
+    // the session and handling overflow.
+    response_receiver_.reset();
+    safety_checker_->RunRawOutputCheck(
+        full_response_, optimization_guide::ResponseCompleteness::kComplete,
+        base::BindOnce(&PromptState::OnFullResponseCheckComplete,
+                       weak_factory_.GetWeakPtr(), std::move(summary)));
+  }
+
+  void RequestSafetyChecksComplete(
+      mojo::PendingRemote<on_device_model::mojom::Session> session,
+      optimization_guide::SafetyChecker::Result safety_result) {
+    if (HandleSafetyError(std::move(safety_result))) {
+      return;
+    }
+    session_.Bind(std::move(session));
+    session_.set_disconnect_handler(
+        base::BindOnce(&PromptState::OnDisconnect, base::Unretained(this)));
+
+    session_->Append(MakeAppendOptions(input_.Clone()),
+                     context_receiver_.BindNewPipeAndPassRemote());
+    context_receiver_.set_disconnect_handler(
+        base::BindOnce(&PromptState::OnDisconnect, base::Unretained(this)));
+
+    if (responder_) {
+      auto generate_options = on_device_model::mojom::GenerateOptions::New();
+      generate_options->constraint = std::move(constraint_);
+      session_->Generate(std::move(generate_options),
+                         response_receiver_.BindNewPipeAndPassRemote());
+      response_receiver_.set_disconnect_handler(
+          base::BindOnce(&PromptState::OnDisconnect, base::Unretained(this)));
+    }
+  }
+
+  void OnPartialResponseCheckComplete(
+      const std::string& response,
+      optimization_guide::SafetyChecker::Result safety_result) {
+    if (HandleSafetyError(std::move(safety_result))) {
+      return;
+    }
+    responder_->OnStreaming(response);
+  }
+
+  void OnFullResponseCheckComplete(
+      on_device_model::mojom::ResponseSummaryPtr summary,
+      optimization_guide::SafetyChecker::Result safety_result) {
+    if (HandleSafetyError(std::move(safety_result))) {
+      return;
+    }
+    token_count_ += summary->output_token_count;
+    if (callback_) {
+      std::move(callback_).Run();
+    }
+    // `this` may be deleted.
+  }
+
+  // Returns true if there was a safety error and the response was stopped.
+  bool HandleSafetyError(
+      optimization_guide::SafetyChecker::Result safety_result) {
+    if (safety_result.failed_to_run) {
+      OnError(blink::mojom::ModelStreamingResponseStatus::kErrorGenericFailure);
+      return true;
+    }
+    if (safety_result.is_unsafe) {
+      OnError(blink::mojom::ModelStreamingResponseStatus::kErrorFiltered);
+      return true;
+    }
+    if (safety_result.is_unsupported_language) {
+      OnError(blink::mojom::ModelStreamingResponseStatus::
+                  kErrorUnsupportedLanguage);
+      return true;
+    }
+    return false;
+  }
+
+  // One of `responder_` or `append_client_` will be set. If `append_client_` is
+  // set, output will not be generated.
+  mojo::Remote<blink::mojom::ModelStreamingResponder> responder_;
+  mojo::Remote<blink::mojom::AILanguageModelAppendClient> append_client_;
+
+  mojo::Remote<on_device_model::mojom::Session> session_;
+  mojo::Receiver<on_device_model::mojom::ContextClient> context_receiver_{this};
+  mojo::Receiver<on_device_model::mojom::StreamingResponder> response_receiver_{
+      this};
+  on_device_model::mojom::InputPtr input_;
+  on_device_model::mojom::ResponseConstraintPtr constraint_;
+
+  // Called when the full operation has completed or an error has occurred.
+  base::OnceClosure callback_;
+  base::raw_ref<optimization_guide::SafetyChecker> safety_checker_;
+
+  // Total number of tokens in input and output.
+  uint32_t token_count_ = 0;
+  // The full response so far.
+  std::string full_response_;
+  // Number of tokens in the response.
+  uint32_t output_tokens_ = 0;
+  // The response since safety check was last run.
+  std::string unchecked_response_;
+  // Number of tokens since safety check was last run.
+  uint32_t unchecked_output_tokens_ = 0;
+
+  base::WeakPtrFactory<PromptState> weak_factory_{this};
+};
+
 AILanguageModel::Context::ContextItem::ContextItem() = default;
 AILanguageModel::Context::ContextItem::ContextItem(const ContextItem& other) {
   tokens = other.tokens;
-  for (const auto& prompt : other.prompts) {
-    prompts.emplace_back(prompt.Clone());
-  }
+  input = other.input.Clone();
 }
 AILanguageModel::Context::ContextItem::ContextItem(ContextItem&&) = default;
 AILanguageModel::Context::ContextItem::~ContextItem() = default;
@@ -148,14 +368,8 @@
 using ModelExecutionError = optimization_guide::
     OptimizationGuideModelExecutionError::ModelExecutionError;
 
-AILanguageModel::Context::Context(uint32_t max_tokens,
-                                  ContextItem initial_prompts)
-    : max_tokens_(max_tokens), initial_prompts_(std::move(initial_prompts)) {
-  CHECK_GE(max_tokens_, initial_prompts_.tokens)
-      << "the caller shouldn't create an AILanguageModel with the initial "
-         "prompts containing more tokens than the limit.";
-  current_tokens_ += initial_prompts.tokens;
-}
+AILanguageModel::Context::Context(uint32_t max_tokens)
+    : max_tokens_(max_tokens) {}
 
 AILanguageModel::Context::Context(const Context& context) = default;
 
@@ -163,9 +377,9 @@
 
 AILanguageModel::Context::SpaceReservationResult
 AILanguageModel::Context::ReserveSpace(uint32_t num_tokens) {
-  // If there is no enough space to hold the `initial_prompts_` as well as the
-  // newly requested `num_tokens`,  return `kInsufficientSpace`.
-  if (num_tokens + initial_prompts_.tokens > max_tokens_) {
+  // If there is not enough space to hold the newly requested `num_tokens`,
+  // return `kInsufficientSpace`.
+  if (num_tokens > max_tokens_) {
     return AILanguageModel::Context::SpaceReservationResult::kInsufficientSpace;
   }
 
@@ -193,49 +407,34 @@
   return result;
 }
 
-MultimodalMessage AILanguageModel::Context::MakeRequest(
-    const on_device_model::Capabilities& capabilities) {
-  MultimodalMessage request = MakeInitialPrompt(initial_prompts_, capabilities);
-  auto history_field = request.edit().MutableRepeatedField(
-      PromptApiRequest::kPromptHistoryFieldNumber);
-  for (auto& context_item : context_items_) {
-    AddPrompts(history_field, context_item, capabilities);
+on_device_model::mojom::InputPtr
+AILanguageModel::Context::GetNonInitialPrompts() {
+  auto input = on_device_model::mojom::Input::New();
+  for (const auto& item : context_items_) {
+    input->pieces.insert(input->pieces.end(), item.input->pieces.begin(),
+                         item.input->pieces.end());
   }
-  return request;
-}
-
-bool AILanguageModel::Context::HasContextItem() {
-  return current_tokens_;
+  return input;
 }
 
 AILanguageModel::AILanguageModel(
-    std::unique_ptr<optimization_guide::OptimizationGuideModelExecutor::Session>
-        session,
-    base::WeakPtr<content::BrowserContext> browser_context,
-    mojo::PendingRemote<blink::mojom::AILanguageModel> pending_remote,
     AIContextBoundObjectSet& context_bound_object_set,
-    AIManager& ai_manager,
-    const std::optional<const Context>& context)
+    on_device_model::mojom::SessionParamsPtr session_params,
+    base::WeakPtr<optimization_guide::ModelClient> model_client,
+    mojo::PendingRemote<on_device_model::mojom::Session> session)
     : AIContextBoundObject(context_bound_object_set),
-      session_(std::move(session)),
-      browser_context_(browser_context),
+      initial_session_(std::move(session)),
+      session_params_(std::move(session_params)),
       context_bound_object_set_(context_bound_object_set),
-      ai_manager_(ai_manager),
-      pending_remote_(std::move(pending_remote)),
-      receiver_(this, pending_remote_.InitWithNewPipeAndPassReceiver()) {
-  receiver_.set_disconnect_handler(base::BindOnce(
-      &AIContextBoundObject::RemoveFromSet, base::Unretained(this)));
-
-  if (context.has_value()) {
-    // If the context is provided, it will be used in this session.
-    context_ = std::make_unique<Context>(context.value());
-    return;
-  }
-
-  // If the context is not provided, initialize a new context
-  // with the default configuration.
+      model_client_(std::move(model_client)) {
   context_ = std::make_unique<Context>(
-      session_->GetTokenLimits().max_context_tokens, Context::ContextItem());
+      model_client_->feature_adapter().GetTokenLimits().max_context_tokens);
+  // TODO(crbug.com/415808003): Should we handle crashes?
+  initial_session_.reset_on_disconnect();
+
+  safety_checker_ = std::make_unique<optimization_guide::SafetyChecker>(
+      weak_ptr_factory_.GetWeakPtr(),
+      optimization_guide::SafetyConfig(model_client_->safety_config()));
 }
 
 AILanguageModel::~AILanguageModel() = default;
@@ -251,210 +450,33 @@
   return metadata;
 }
 
-void AILanguageModel::SetInitialPrompts(
+void AILanguageModel::Initialize(
     std::vector<blink::mojom::AILanguageModelPromptPtr> initial_prompts,
-    CreateLanguageModelCallback callback) {
-  Context::ContextItem item;
-  for (auto& prompt : initial_prompts) {
-    item.prompts.emplace_back(std::move(prompt));
-  }
-  MultimodalMessage request =
-      MakeInitialPrompt(item, session_->GetCapabilities());
-  session_->GetContextSizeInTokens(
-      request.read(),
-      base::BindOnce(&AILanguageModel::InitializeContextWithInitialPrompts,
-                     weak_ptr_factory_.GetWeakPtr(), std::move(item),
-                     std::move(callback)));
-}
-
-void AILanguageModel::InitializeContextWithInitialPrompts(
-    Context::ContextItem initial_prompts,
-    CreateLanguageModelCallback callback,
-    std::optional<uint32_t> result) {
-  if (!result.has_value()) {
-    std::move(callback).Run(
-        base::unexpected(blink::mojom::AIManagerCreateClientError::
-                             kUnableToCalculateTokenSize),
-        /*info=*/nullptr);
-    return;
-  }
-
-  uint32_t size = result.value();
-  uint32_t max_token = context_->max_tokens();
-  if (size > max_token) {
-    // Session creation fails if initial prompts exceed the token limit.
-    std::move(callback).Run(
-        base::unexpected(
-            blink::mojom::AIManagerCreateClientError::kInitialInputTooLarge),
-        /*info=*/nullptr);
-    return;
-  }
-
-  initial_prompts.tokens = size;
-  context_ = std::make_unique<Context>(max_token, std::move(initial_prompts));
-
-  // Begin processing the initial prompts immediately.
-  session_->SetInput(context_->MakeRequest(session_->GetCapabilities()), {});
-
-  std::move(callback).Run(TakePendingRemote(), GetLanguageModelInstanceInfo());
-}
-
-void AILanguageModel::ModelExecutionCallback(
-    const Context::ContextItem& item,
-    mojo::RemoteSetElementId responder_id,
-    optimization_guide::OptimizationGuideModelStreamingExecutionResult result) {
-  blink::mojom::ModelStreamingResponder* responder =
-      responder_set_.Get(responder_id);
-  if (!responder) {
-    // It might be possible for the responder mojo connection to be closed
-    // before this callback is invoked, in this case, we can't do anything.
-    return;
-  }
-
-  if (!result.response.has_value()) {
-    responder->OnError(
-        AIUtils::ConvertModelExecutionError(result.response.error().error()));
-    return;
-  }
-
-  auto response = optimization_guide::ParsedAnyMetadata<
-      optimization_guide::proto::StringValue>(result.response->response);
-  if (response->has_value()) {
-    std::string chunk = response->value();
-    current_response_ += chunk;
-    responder->OnStreaming(chunk);
-  }
-
-  if (result.response->is_complete) {
-    uint32_t token_count = result.response->input_token_count +
-                           result.response->output_token_count;
-    // If the on device model service fails to calculate the size, it will be 0.
-    // TODO(crbug.com/351935691): make sure the error is explicitly returned
-    // and handled accordingly.
-    if (token_count) {
-      Context::ContextItem copy = item;
-      copy.tokens = token_count;
-      copy.prompts.emplace_back(
-          MakeTextPrompt(blink::mojom::AILanguageModelPromptRole::kAssistant,
-                         current_response_));
-      if (context_->AddContextItem(std::move(copy)) ==
-          Context::SpaceReservationResult::kSpaceMadeAvailable) {
-        responder->OnQuotaOverflow();
-      }
-    }
-    responder->OnCompletion(blink::mojom::ModelExecutionContextInfo::New(
-        context_->current_tokens()));
-  }
-}
-
-void AILanguageModel::AppendItemGetInputSizeCompletion(
-    std::vector<blink::mojom::AILanguageModelPromptPtr> prompts,
-    mojo::Remote<blink::mojom::AILanguageModelAppendClient> client_remote,
-    std::optional<uint32_t> result) {
-  if (!client_remote.is_connected()) {
-    return;
-  }
-  if (!session_) {
-    // If the session is destroyed before this callback is invoked, we should
-    // not do anything further.
-    client_remote->OnError(
-        blink::mojom::ModelStreamingResponseStatus::kErrorSessionDestroyed);
-    return;
-  }
-
-  if (!result.has_value()) {
-    client_remote->OnError(
-        blink::mojom::ModelStreamingResponseStatus::kErrorRetryableError);
-    return;
-  }
-
-  Context::ContextItem item;
-  item.prompts = std::move(prompts);
-  item.tokens = result.value();
-  switch (context_->ReserveSpace(item.tokens)) {
-    case Context::SpaceReservationResult::kSufficientSpace:
-      context_->AddContextItem(std::move(item));
-      break;
-    case Context::SpaceReservationResult::kSpaceMadeAvailable:
-      client_remote->OnQuotaOverflow();
-      context_->AddContextItem(std::move(item));
-      break;
-    case Context::SpaceReservationResult::kInsufficientSpace:
-      client_remote->OnError(
-          blink::mojom::ModelStreamingResponseStatus::kErrorInputTooLarge);
+    mojo::PendingRemote<blink::mojom::AIManagerCreateLanguageModelClient>
+        create_client) {
+  if (initial_prompts.empty()) {
+    InitializeGetInputSizeComplete(nullptr, std::move(create_client), 0);
+  } else {
+    auto input = ConvertToInput(initial_prompts, session_params_->capabilities);
+    if (!input) {
+      mojo::Remote<blink::mojom::AIManagerCreateLanguageModelClient>(
+          std::move(create_client))
+          ->OnError(
+              blink::mojom::AIManagerCreateClientError::kUnableToCreateSession);
       return;
+    }
+    // This does not need to be queued because the AILanguageModel receiver has
+    // not been bound yet, so mojo calls cannot be received.
+    // TODO(crbug.com/415808003): May be able to avoid GetSizeInTokens() and
+    // directly use the token result from ContextClient if the backend can
+    // gracefully handle sending >max_tokens and giving an error.
+    auto cloned_input = input.Clone();
+    GetSizeInTokens(
+        std::move(cloned_input),
+        base::BindOnce(&AILanguageModel::InitializeGetInputSizeComplete,
+                       weak_ptr_factory_.GetWeakPtr(), std::move(input),
+                       std::move(create_client)));
   }
-
-  // TODO(crbug.com/409355678): currently the append cannot be aborted once it's
-  // sent to the `session_`, we should improve this so the `SetInput` call can
-  // be cancelled.
-  session_->SetInput(
-      context_->MakeRequest(session_->GetCapabilities()),
-      base::BindOnce(
-          [](mojo::Remote<blink::mojom::AILanguageModelAppendClient>
-                 client_remote,
-             base::expected<
-                 size_t,
-                 optimization_guide::OptimizationGuideModelExecutionError>
-                 result) {
-            if (!result.has_value()) {
-              client_remote->OnError(
-                  AIUtils::ConvertModelExecutionError(result.error().error()));
-            } else {
-              client_remote->OnAppendComplete();
-            }
-          },
-          std::move(client_remote)));
-}
-
-void AILanguageModel::PromptGetInputSizeCompletion(
-    mojo::RemoteSetElementId responder_id,
-    Context::ContextItem current_item,
-    on_device_model::mojom::ResponseConstraintPtr constraint,
-    std::optional<uint32_t> result) {
-  if (!session_) {
-    // If the session is destroyed before this callback is invoked, we should
-    // not do anything further.
-    return;
-  }
-
-  blink::mojom::ModelStreamingResponder* responder =
-      responder_set_.Get(responder_id);
-  if (!responder) {
-    // It might be possible for the responder mojo connection to be closed
-    // before this callback is invoked, in this case, we can't do anything.
-    return;
-  }
-
-  if (!result.has_value()) {
-    responder->OnError(
-        blink::mojom::ModelStreamingResponseStatus::kErrorGenericFailure);
-    return;
-  }
-
-  uint32_t number_of_tokens = result.value();
-  auto space_reserved = context_->ReserveSpace(number_of_tokens);
-  if (space_reserved == Context::SpaceReservationResult::kInsufficientSpace) {
-    responder->OnError(
-        blink::mojom::ModelStreamingResponseStatus::kErrorInputTooLarge);
-    return;
-  }
-
-  if (space_reserved == Context::SpaceReservationResult::kSpaceMadeAvailable) {
-    responder->OnQuotaOverflow();
-  }
-  current_item.tokens = number_of_tokens;
-
-  const on_device_model::Capabilities& capabilities =
-      session_->GetCapabilities();
-  MultimodalMessage request = context_->MakeRequest(capabilities);
-  AddCurrentRequest(request, current_item, capabilities);
-  session_->SetInput(std::move(request), {});
-  session_->ExecuteModelWithResponseConstraint(
-      PromptApiRequest(), std::move(constraint),
-      base::BindRepeating(&AILanguageModel::ModelExecutionCallback,
-                          weak_ptr_factory_.GetWeakPtr(),
-                          std::move(current_item), responder_id));
 }
 
 void AILanguageModel::Prompt(
@@ -462,95 +484,81 @@
     on_device_model::mojom::ResponseConstraintPtr constraint,
     mojo::PendingRemote<blink::mojom::ModelStreamingResponder>
         pending_responder) {
-  if (!session_) {
-    mojo::Remote<blink::mojom::ModelStreamingResponder> responder(
-        std::move(pending_responder));
-    responder->OnError(
-        blink::mojom::ModelStreamingResponseStatus::kErrorSessionDestroyed);
-    return;
-  }
-
-  // Clear the response from the previous execution.
-  current_response_ = "";
-  mojo::RemoteSetElementId responder_id =
-      responder_set_.Add(std::move(pending_responder));
-
-  Context::ContextItem item;
-  item.prompts = std::move(prompts);
-
-  MultimodalMessage request = EmptyMessage();
-  AddCurrentRequest(request, item, session_->GetCapabilities());
-  session_->GetExecutionInputSizeInTokens(
-      request.read(),
-      base::BindOnce(&AILanguageModel::PromptGetInputSizeCompletion,
-                     weak_ptr_factory_.GetWeakPtr(), responder_id,
-                     std::move(item), std::move(constraint)));
+  AddToQueue(base::BindOnce(
+      &AILanguageModel::PromptInternal, weak_ptr_factory_.GetWeakPtr(),
+      std::move(prompts), std::move(constraint), std::move(pending_responder)));
 }
 
 void AILanguageModel::Append(
     std::vector<blink::mojom::AILanguageModelPromptPtr> prompts,
     mojo::PendingRemote<blink::mojom::AILanguageModelAppendClient> client) {
-  // Build the request to calculate the token size, the prompts will only be
-  // added to the context in the completion callback.
-  MultimodalMessage request = EmptyMessage();
-  for (auto& prompt : prompts) {
-    AddPromptToField(*prompt,
-                     request.edit().MutableRepeatedField(
-                         PromptApiRequest::kPromptHistoryFieldNumber),
-                     session_->GetCapabilities());
-  }
-
-  mojo::Remote<blink::mojom::AILanguageModelAppendClient> client_remote(
-      std::move(client));
-  session_->GetContextSizeInTokens(
-      request.read(),
-      base::BindOnce(&AILanguageModel::AppendItemGetInputSizeCompletion,
-                     weak_ptr_factory_.GetWeakPtr(), std::move(prompts),
-                     std::move(client_remote)));
+  AddToQueue(base::BindOnce(&AILanguageModel::AppendInternal,
+                            weak_ptr_factory_.GetWeakPtr(), std::move(prompts),
+                            std::move(client)));
 }
 
 void AILanguageModel::Fork(
     mojo::PendingRemote<blink::mojom::AIManagerCreateLanguageModelClient>
         client) {
-  mojo::Remote<blink::mojom::AIManagerCreateLanguageModelClient> client_remote(
-      std::move(client));
-  if (!browser_context_) {
-    // The `browser_context_` is already destroyed before the renderer owner
-    // is gone.
-    client_remote->OnError(
-        blink::mojom::AIManagerCreateClientError::kUnableToCreateSession);
-    return;
-  }
-
-  const optimization_guide::SamplingParams sampling_param =
-      session_->GetSamplingParams();
-
-  ai_manager_->CreateLanguageModelForCloning(
-      base::PassKey<AILanguageModel>(),
-      blink::mojom::AILanguageModelSamplingParams::New(
-          sampling_param.top_k, sampling_param.temperature),
-      session_->GetCapabilities(), context_bound_object_set_.get(), *context_,
-      std::move(client_remote));
+  AddToQueue(base::BindOnce(&AILanguageModel::ForkInternal,
+                            weak_ptr_factory_.GetWeakPtr(), std::move(client)));
 }
 
 void AILanguageModel::Destroy() {
-  session_.reset();
-  for (auto& responder : responder_set_) {
-    responder->OnError(
-        blink::mojom::ModelStreamingResponseStatus::kErrorSessionDestroyed);
+  RemoveFromSet();
+}
+
+void AILanguageModel::MeasureInputUsage(
+    std::vector<blink::mojom::AILanguageModelPromptPtr> prompts,
+    mojo::PendingRemote<blink::mojom::AILanguageModelMeasureInputUsageClient>
+        client) {
+  mojo::Remote<blink::mojom::AILanguageModelMeasureInputUsageClient> remote(
+      std::move(client));
+  auto input = ConvertToInputForExecute(std::move(prompts),
+                                        session_params_->capabilities);
+  if (!input) {
+    remote->OnResult(0);
+    return;
   }
-  responder_set_.Clear();
+  GetSizeInTokens(
+      std::move(input),
+      base::BindOnce(
+          [](mojo::Remote<blink::mojom::AILanguageModelMeasureInputUsageClient>
+                 client_remote,
+             std::optional<uint32_t> result) {
+            // TODO(crbug.com/351935691): Explicitly return an error. Consider
+            // introducing a callback instead of remote client, as it's done
+            // for Writing Assistance APIs.
+            client_remote->OnResult(result.value_or(0));
+          },
+          std::move(remote)));
+}
+
+void AILanguageModel::SetPriority(on_device_model::mojom::Priority priority) {
+  if (initial_session_) {
+    initial_session_->SetPriority(priority);
+  }
+  if (current_session_) {
+    current_session_->SetPriority(priority);
+  }
+  if (prompt_state_) {
+    prompt_state_->SetPriority(priority);
+  }
+}
+
+void AILanguageModel::StartSession(
+    mojo::PendingReceiver<on_device_model::mojom::TextSafetySession> session) {
+  if (model_client_) {
+    model_client_->StartSession(std::move(session));
+  }
 }
 
 blink::mojom::AILanguageModelInstanceInfoPtr
 AILanguageModel::GetLanguageModelInstanceInfo() {
-  const optimization_guide::SamplingParams session_sampling_params =
-      session_->GetSamplingParams();
   base::flat_set<blink::mojom::AILanguageModelPromptType> input_types = {
-      blink::mojom::AILanguageModelPromptType::kText  // Text is always
-                                                      // supported.
+      blink::mojom::AILanguageModelPromptType::kText  // Text always supported.
   };
-  for (const auto capability : session_->GetCapabilities()) {
+  for (const auto capability : session_params_->capabilities) {
     switch (capability) {
       case on_device_model::CapabilityFlags::kImageInput:
         input_types.insert(blink::mojom::AILanguageModelPromptType::kImage);
@@ -561,45 +569,300 @@
     }
   }
 
+  uint32_t max_tokens = 0;
+  if (model_client_) {
+    max_tokens =
+        model_client_->feature_adapter().GetTokenLimits().max_context_tokens;
+  }
   return blink::mojom::AILanguageModelInstanceInfo::New(
-      context_->max_tokens(), context_->current_tokens(),
+      max_tokens, max_tokens - context_->max_tokens(),
       blink::mojom::AILanguageModelSamplingParams::New(
-          session_sampling_params.top_k, session_sampling_params.temperature),
+          session_params_->top_k, session_params_->temperature),
       std::move(input_types).extract());
 }
 
-void AILanguageModel::MeasureInputUsage(
-    std::vector<blink::mojom::AILanguageModelPromptPtr> input,
-    mojo::PendingRemote<blink::mojom::AILanguageModelMeasureInputUsageClient>
-        client) {
-  Context::ContextItem item;
-  item.prompts = std::move(input);
-
-  MultimodalMessage request = EmptyMessage();
-  AddCurrentRequest(request, item, session_->GetCapabilities());
-
-  session_->GetExecutionInputSizeInTokens(
-      request.read(),
-      base::BindOnce(
-          [](mojo::Remote<blink::mojom::AILanguageModelMeasureInputUsageClient>
-                 client_remote,
-             std::optional<uint32_t> result) {
-            // TODO(crbug.com/351935691): Explicitly return an error. Consider
-            // introducing a callback instead of remote client, as it's done
-            // for Writing Assistance APIs.
-            client_remote->OnResult(result.value_or(0));
-          },
-          mojo::Remote<blink::mojom::AILanguageModelMeasureInputUsageClient>(
-              std::move(client))));
+mojo::PendingRemote<blink::mojom::AILanguageModel>
+AILanguageModel::BindRemote() {
+  auto remote = receiver_.BindNewPipeAndPassRemote();
+  receiver_.set_disconnect_handler(base::BindOnce(
+      &AIContextBoundObject::RemoveFromSet, base::Unretained(this)));
+  return remote;
 }
 
-void AILanguageModel::SetPriority(on_device_model::mojom::Priority priority) {
-  if (session_) {
-    session_->SetPriority(priority);
+void AILanguageModel::InitializeGetInputSizeComplete(
+    on_device_model::mojom::InputPtr input,
+    mojo::PendingRemote<blink::mojom::AIManagerCreateLanguageModelClient>
+        create_client,
+    std::optional<uint32_t> token_count) {
+  mojo::Remote<blink::mojom::AIManagerCreateLanguageModelClient> client(
+      std::move(create_client));
+  if (!initial_session_ || !token_count) {
+    client->OnError(
+        blink::mojom::AIManagerCreateClientError::kUnableToCreateSession);
+    return;
+  }
+
+  uint32_t max_tokens = context_->max_tokens();
+  if (*token_count > max_tokens) {
+    client->OnError(
+        blink::mojom::AIManagerCreateClientError::kInitialInputTooLarge);
+    return;
+  }
+
+  if (input) {
+    // `context_` will track how many tokens are remaining after the initial
+    // prompts. The initial prompts cannot be evicted.
+    context_ = std::make_unique<Context>(max_tokens - *token_count);
+
+    // No ContextClient is passed here since this operation should never be
+    // cancelled unless the session is destroyed.
+    initial_session_->Append(MakeAppendOptions(std::move(input)), {});
+  }
+  initial_session_->Clone(current_session_.BindNewPipeAndPassReceiver());
+
+  client->OnResult(BindRemote(), GetLanguageModelInstanceInfo());
+}
+
+void AILanguageModel::ForkInternal(
+    mojo::PendingRemote<blink::mojom::AIManagerCreateLanguageModelClient>
+        client,
+    base::OnceClosure on_complete) {
+  mojo::Remote<blink::mojom::AIManagerCreateLanguageModelClient> remote(
+      std::move(client));
+  if (!initial_session_ || !model_client_) {
+    remote->OnError(
+        blink::mojom::AIManagerCreateClientError::kUnableToCreateSession);
+    return;
+  }
+
+  mojo::PendingRemote<on_device_model::mojom::Session> session;
+  initial_session_->Clone(session.InitWithNewPipeAndPassReceiver());
+  auto clone = std::make_unique<AILanguageModel>(
+      *context_bound_object_set_, session_params_.Clone(), model_client_,
+      std::move(session));
+  clone->context_ = std::make_unique<Context>(*context_);
+  current_session_->Clone(clone->current_session_.BindNewPipeAndPassReceiver());
+
+  remote->OnResult(clone->BindRemote(), clone->GetLanguageModelInstanceInfo());
+
+  context_bound_object_set_->AddContextBoundObject(std::move(clone));
+}
+
+void AILanguageModel::PromptInternal(
+    std::vector<blink::mojom::AILanguageModelPromptPtr> prompts,
+    on_device_model::mojom::ResponseConstraintPtr constraint,
+    mojo::PendingRemote<blink::mojom::ModelStreamingResponder>
+        pending_responder,
+    base::OnceClosure on_complete) {
+  if (!initial_session_) {
+    mojo::Remote<blink::mojom::ModelStreamingResponder>(
+        std::move(pending_responder))
+        ->OnError(
+            blink::mojom::ModelStreamingResponseStatus::kErrorSessionDestroyed);
+    return;
+  }
+
+  auto input = ConvertToInputForExecute(prompts, session_params_->capabilities);
+  if (!input) {
+    mojo::Remote<blink::mojom::ModelStreamingResponder>(
+        std::move(pending_responder))
+        ->OnError(
+            blink::mojom::ModelStreamingResponseStatus::kErrorInvalidRequest);
+    return;
+  }
+  prompt_state_ =
+      std::make_unique<PromptState>(std::move(pending_responder), input.Clone(),
+                                    std::move(constraint), *safety_checker_);
+  GetSizeInTokens(
+      std::move(input),
+      base::BindOnce(&AILanguageModel::PromptGetInputSizeComplete,
+                     weak_ptr_factory_.GetWeakPtr(),
+                     base::BindOnce(&AILanguageModel::PromptOutputComplete,
+                                    weak_ptr_factory_.GetWeakPtr())
+                         .Then(std::move(on_complete))));
+}
+
+void AILanguageModel::PromptGetInputSizeComplete(
+    base::OnceClosure on_complete,
+    std::optional<uint32_t> token_count) {
+  if (!prompt_state_ || !prompt_state_->IsValid()) {
+    return;
+  }
+
+  if (!token_count) {
+    prompt_state_->OnError(
+        blink::mojom::ModelStreamingResponseStatus::kErrorGenericFailure);
+    return;
+  }
+
+  auto space_reserved = context_->ReserveSpace(*token_count);
+  if (space_reserved == Context::SpaceReservationResult::kInsufficientSpace) {
+    prompt_state_->OnError(
+        blink::mojom::ModelStreamingResponseStatus::kErrorInputTooLarge);
+    return;
+  }
+
+  if (space_reserved == Context::SpaceReservationResult::kSpaceMadeAvailable) {
+    HandleOverflow();
+    prompt_state_->OnQuotaOverflow();
+  }
+
+  // Use a cloned version of the current session so it is easy to restore to
+  // the previous state if a prompt is cancelled.
+  mojo::PendingRemote<on_device_model::mojom::Session> session;
+  current_session_->Clone(session.InitWithNewPipeAndPassReceiver());
+  prompt_state_->AppendAndGenerate(std::move(session), std::move(on_complete));
+}
+
+void AILanguageModel::PromptOutputComplete() {
+  if (!prompt_state_ || !prompt_state_->IsValid()) {
+    return;
+  }
+
+  if (!initial_session_) {
+    prompt_state_->OnError(
+        blink::mojom::ModelStreamingResponseStatus::kErrorSessionDestroyed);
+    return;
+  }
+
+  auto model_output = on_device_model::mojom::Input::New();
+  model_output->pieces = {prompt_state_->response(), ml::Token::kEnd};
+
+  // The prompt has completed successfully, replace the current session.
+  current_session_ = prompt_state_->TakeSession();
+  // Add the output to the session since this is not added automatically from
+  // the Generate() call. The previous token will be a kModel token from
+  // ConvertToInputForExecute().
+  current_session_->Append(MakeAppendOptions(model_output.Clone()), {});
+
+  Context::ContextItem item;
+  // One extra token for the end token on the model output.
+  item.tokens = prompt_state_->token_count() + 1;
+  item.input = prompt_state_->TakeInput();
+  item.input->pieces.insert(item.input->pieces.end(),
+                            model_output->pieces.begin(),
+                            model_output->pieces.end());
+
+  auto responder = prompt_state_->TakeResponder();
+  // The context's session history may be modified when adding a new item. In
+  // this case, the session history is replayed on the session and the output is
+  // still sent to the responder.
+  if (context_->AddContextItem(std::move(item)) ==
+      Context::SpaceReservationResult::kSpaceMadeAvailable) {
+    HandleOverflow();
+    responder->OnQuotaOverflow();
+  }
+  responder->OnCompletion(
+      blink::mojom::ModelExecutionContextInfo::New(context_->current_tokens()));
+  if (model_client_) {
+    model_client_->solution().ReportHealthyCompletion();
   }
 }
 
-mojo::PendingRemote<blink::mojom::AILanguageModel>
-AILanguageModel::TakePendingRemote() {
-  return std::move(pending_remote_);
+void AILanguageModel::AppendComplete() {
+  if (!prompt_state_ || !prompt_state_->IsValid()) {
+    return;
+  }
+
+  current_session_ = prompt_state_->TakeSession();
+
+  Context::ContextItem item;
+  item.tokens = prompt_state_->token_count();
+  item.input = prompt_state_->TakeInput();
+  if (context_->AddContextItem(std::move(item)) ==
+      Context::SpaceReservationResult::kSpaceMadeAvailable) {
+    HandleOverflow();
+    prompt_state_->OnQuotaOverflow();
+  }
+}
+
+void AILanguageModel::AppendInternal(
+    std::vector<blink::mojom::AILanguageModelPromptPtr> prompts,
+    mojo::PendingRemote<blink::mojom::AILanguageModelAppendClient> client,
+    base::OnceClosure on_complete) {
+  if (!initial_session_) {
+    mojo::Remote<blink::mojom::AILanguageModelAppendClient>(std::move(client))
+        ->OnError(
+            blink::mojom::ModelStreamingResponseStatus::kErrorSessionDestroyed);
+    return;
+  }
+
+  auto input = ConvertToInput(prompts, session_params_->capabilities);
+  if (!input) {
+    mojo::Remote<blink::mojom::AILanguageModelAppendClient>(std::move(client))
+        ->OnError(
+            blink::mojom::ModelStreamingResponseStatus::kErrorInvalidRequest);
+    return;
+  }
+  prompt_state_ = std::make_unique<PromptState>(
+      std::move(client), input.Clone(), *safety_checker_);
+  // The rest of the logic can be shared with Prompt() since PromptState() will
+  // handle correctly calling the append client.
+  // TODO(crbug.com/409355678): Can this share more logic with Prompt()?
+  GetSizeInTokens(
+      std::move(input),
+      base::BindOnce(&AILanguageModel::PromptGetInputSizeComplete,
+                     weak_ptr_factory_.GetWeakPtr(),
+                     base::BindOnce(&AILanguageModel::AppendComplete,
+                                    weak_ptr_factory_.GetWeakPtr())
+                         .Then(std::move(on_complete))));
+}
+
+void AILanguageModel::HandleOverflow() {
+  // On overflow the prompt history has been modified. This happens if
+  // Context::AddContextItem() returns kSpaceMadeAvailable. Create a clone of
+  // the initial session, then replay the modified history on top of that.
+  current_session_.reset();
+  initial_session_->Clone(current_session_.BindNewPipeAndPassReceiver());
+
+  auto input = context_->GetNonInitialPrompts();
+  if (!input->pieces.empty()) {
+    // No ContextClient is passed here since this operation should never be
+    // cancelled unless the session is destroyed.
+    current_session_->Append(MakeAppendOptions(std::move(input)), {});
+  }
+}
+
+void AILanguageModel::GetSizeInTokens(
+    on_device_model::mojom::InputPtr input,
+    base::OnceCallback<void(std::optional<uint32_t>)> callback) {
+  if (!initial_session_) {
+    std::move(callback).Run(std::nullopt);
+    return;
+  }
+  initial_session_->GetSizeInTokens(
+      std::move(input),
+      base::BindOnce(
+          [](base::OnceCallback<void(std::optional<uint32_t>)> callback,
+             uint32_t num_tokens) { std::move(callback).Run(num_tokens); },
+          mojo::WrapCallbackWithDefaultInvokeIfNotRun(std::move(callback),
+                                                      std::nullopt)));
+}
+
+void AILanguageModel::AddToQueue(QueueCallback task) {
+  queue_.push(std::move(task));
+  RunNext();
+}
+
+void AILanguageModel::TaskComplete() {
+  task_running_ = false;
+  RunNext();
+}
+
+void AILanguageModel::RunNext() {
+  if (task_running_) {
+    return;
+  }
+  prompt_state_ = nullptr;
+  if (queue_.empty()) {
+    return;
+  }
+  task_running_ = true;
+  auto task = std::move(queue_.front());
+  queue_.pop();
+  // Wrap the completion callback in a default invoke to allow tasks to avoid
+  // having to explicitly call in every error code path.
+  std::move(task).Run(
+      mojo::WrapCallbackWithDefaultInvokeIfNotRun(base::BindOnce(
+          &AILanguageModel::TaskComplete, weak_ptr_factory_.GetWeakPtr())));
 }
diff --git a/chrome/browser/ai/ai_language_model.h b/chrome/browser/ai/ai_language_model.h
index dae537a..36cc38e 100644
--- a/chrome/browser/ai/ai_language_model.h
+++ b/chrome/browser/ai/ai_language_model.h
@@ -8,15 +8,19 @@
 #include <deque>
 #include <optional>
 
+#include "base/containers/queue.h"
 #include "base/functional/callback_forward.h"
 #include "base/memory/weak_ptr.h"
 #include "base/types/expected.h"
 #include "chrome/browser/ai/ai_context_bound_object.h"
 #include "chrome/browser/ai/ai_context_bound_object_set.h"
 #include "chrome/browser/ai/ai_utils.h"
+#include "components/optimization_guide/core/model_execution/model_broker_client.h"
 #include "components/optimization_guide/core/model_execution/multimodal_message.h"
+#include "components/optimization_guide/core/model_execution/safety_checker.h"
 #include "components/optimization_guide/core/optimization_guide_model_executor.h"
 #include "components/optimization_guide/proto/features/prompt_api.pb.h"
+#include "components/optimization_guide/public/mojom/model_broker.mojom.h"
 #include "content/public/browser/browser_context.h"
 #include "mojo/public/cpp/bindings/pending_remote.h"
 #include "mojo/public/cpp/bindings/receiver.h"
@@ -27,29 +31,21 @@
 #include "third_party/blink/public/mojom/ai/ai_manager.mojom-forward.h"
 #include "third_party/blink/public/mojom/ai/model_streaming_responder.mojom.h"
 
-class AIManager;
-
 // The implementation of `blink::mojom::AILanguageModel`, which exposes the APIs
 // for model execution.
 class AILanguageModel : public AIContextBoundObject,
-                        public blink::mojom::AILanguageModel {
+                        public blink::mojom::AILanguageModel,
+                        public optimization_guide::TextSafetyClient {
  public:
-  using PromptApiRole = optimization_guide::proto::PromptApiRole;
-  using PromptApiPrompt = optimization_guide::proto::PromptApiPrompt;
-  using PromptApiRequest = optimization_guide::proto::PromptApiRequest;
   using PromptApiMetadata = optimization_guide::proto::PromptApiMetadata;
-  using CreateLanguageModelCallback = base::OnceCallback<void(
-      base::expected<mojo::PendingRemote<blink::mojom::AILanguageModel>,
-                     blink::mojom::AIManagerCreateClientError>,
-      blink::mojom::AILanguageModelInstanceInfoPtr)>;
 
   // The minimum version of the model execution config for prompt API that
   // starts using proto instead of string value for the request.
   static constexpr uint32_t kMinVersionUsingProto = 2;
 
-  // The Context class manages the history of prompt input and output, which are
-  // used to build the context when performing the next execution. Context is
-  // stored in a FIFO and kept below a limited number of tokens.
+  // The Context class manages the history of prompt input and output. Context
+  // is stored in a FIFO and kept below a limited number of tokens when overflow
+  // occurs.
   class Context {
    public:
     // A piece of the prompt history and it's size.
@@ -59,11 +55,12 @@
       ContextItem(ContextItem&&);
       ~ContextItem();
 
-      std::vector<blink::mojom::AILanguageModelPromptPtr> prompts;
+      on_device_model::mojom::InputPtr input;
       uint32_t tokens = 0;
     };
 
-    Context(uint32_t max_tokens, ContextItem initial_prompts);
+    // `max_tokens` is the number of tokens remaining after the initial prompts.
+    explicit Context(uint32_t max_tokens);
     Context(const Context&);
     ~Context();
 
@@ -88,34 +85,25 @@
     // result from the space reservation.
     SpaceReservationResult AddContextItem(ContextItem context_item);
 
-    // Combines the initial prompts and all current items into a request.
-    // The type of request produced is a PromptApiRequest. `capabilities`
-    // contains the capabilities of the target model.
-    optimization_guide::MultimodalMessage MakeRequest(
-        const on_device_model::Capabilities& capabilities);
+    // Returns an input containing all of the current prompt history excluding
+    // the initial prompts. This does not include prompts removed due to
+    // overflow handling.
+    on_device_model::mojom::InputPtr GetNonInitialPrompts();
 
-    // Returns true if the system prompt is set or there is at least one context
-    // item.
-    bool HasContextItem();
-
+    // The number of tokens remaining after the initial prompts.
     uint32_t max_tokens() const { return max_tokens_; }
     uint32_t current_tokens() const { return current_tokens_; }
 
    private:
     uint32_t max_tokens_;
     uint32_t current_tokens_ = 0;
-    ContextItem initial_prompts_;
     std::deque<ContextItem> context_items_;
   };
 
-  AILanguageModel(
-      std::unique_ptr<
-          optimization_guide::OptimizationGuideModelExecutor::Session> session,
-      base::WeakPtr<content::BrowserContext> browser_context,
-      mojo::PendingRemote<blink::mojom::AILanguageModel> pending_remote,
-      AIContextBoundObjectSet& session_set,
-      AIManager& ai_manager,
-      const std::optional<const Context>& context = std::nullopt);
+  AILanguageModel(AIContextBoundObjectSet& context_bound_object_set,
+                  on_device_model::mojom::SessionParamsPtr session_params,
+                  base::WeakPtr<optimization_guide::ModelClient> model_client,
+                  mojo::PendingRemote<on_device_model::mojom::Session> session);
   AILanguageModel(const AILanguageModel&) = delete;
   AILanguageModel& operator=(const AILanguageModel&) = delete;
 
@@ -125,6 +113,13 @@
   static PromptApiMetadata ParseMetadata(
       const optimization_guide::proto::Any& any);
 
+  // Format the initial prompts, gets the token count, updates the session,
+  // and reports to `create_client`.
+  void Initialize(
+      std::vector<blink::mojom::AILanguageModelPromptPtr> initial_prompts,
+      mojo::PendingRemote<blink::mojom::AIManagerCreateLanguageModelClient>
+          create_client);
+
   // `blink::mojom::AILanguageModel` implementation.
   void Prompt(std::vector<blink::mojom::AILanguageModelPromptPtr> prompts,
               on_device_model::mojom::ResponseConstraintPtr constraint,
@@ -138,60 +133,93 @@
           client) override;
   void Destroy() override;
   void MeasureInputUsage(
-      std::vector<blink::mojom::AILanguageModelPromptPtr> input,
+      std::vector<blink::mojom::AILanguageModelPromptPtr> prompts,
       mojo::PendingRemote<blink::mojom::AILanguageModelMeasureInputUsageClient>
           client) override;
 
   // AIContextBoundObject:
   void SetPriority(on_device_model::mojom::Priority priority) override;
 
-  // Format the initial prompts, gets the token count, updates the session,
-  // and passes the session information back through the callback.
-  void SetInitialPrompts(
-      std::vector<blink::mojom::AILanguageModelPromptPtr> initial_prompts,
-      CreateLanguageModelCallback callback);
+  // optimization_guide::TextSafetyClient:
+  void StartSession(
+      mojo::PendingReceiver<on_device_model::mojom::TextSafetySession> session)
+      override;
+
   blink::mojom::AILanguageModelInstanceInfoPtr GetLanguageModelInstanceInfo();
-  mojo::PendingRemote<blink::mojom::AILanguageModel> TakePendingRemote();
 
  private:
-  void AppendItemGetInputSizeCompletion(
+  mojo::PendingRemote<blink::mojom::AILanguageModel> BindRemote();
+
+  class PromptState;
+  void InitializeGetInputSizeComplete(
+      on_device_model::mojom::InputPtr input,
+      mojo::PendingRemote<blink::mojom::AIManagerCreateLanguageModelClient>
+          create_client,
+      std::optional<uint32_t> token_count);
+
+  void ForkInternal(
+      mojo::PendingRemote<blink::mojom::AIManagerCreateLanguageModelClient>
+          client,
+      base::OnceClosure on_complete);
+
+  void PromptInternal(
       std::vector<blink::mojom::AILanguageModelPromptPtr> prompts,
-      mojo::Remote<blink::mojom::AILanguageModelAppendClient> client,
-      std::optional<uint32_t> result);
-  void PromptGetInputSizeCompletion(
-      mojo::RemoteSetElementId responder_id,
-      Context::ContextItem current_item,
       on_device_model::mojom::ResponseConstraintPtr constraint,
-      std::optional<uint32_t> result);
-  void ModelExecutionCallback(
-      const Context::ContextItem& current_item,
-      mojo::RemoteSetElementId responder_id,
-      optimization_guide::OptimizationGuideModelStreamingExecutionResult
-          result);
+      mojo::PendingRemote<blink::mojom::ModelStreamingResponder>
+          pending_responder,
+      base::OnceClosure on_complete);
+  void PromptGetInputSizeComplete(base::OnceClosure on_complete,
+                                  std::optional<uint32_t> result);
+  void PromptOutputComplete();
+  void AppendComplete();
 
-  void InitializeContextWithInitialPrompts(Context::ContextItem initial_prompts,
-                                           CreateLanguageModelCallback callback,
-                                           std::optional<uint32_t> result);
+  void AppendInternal(
+      std::vector<blink::mojom::AILanguageModelPromptPtr> prompts,
+      mojo::PendingRemote<blink::mojom::AILanguageModelAppendClient> client,
+      base::OnceClosure on_complete);
 
-  // The underlying session provided by optimization guide component.
-  std::unique_ptr<optimization_guide::OptimizationGuideModelExecutor::Session>
-      session_;
-  // The `RemoteSet` storing all the responders, each of them corresponds to one
-  // `Execute()` call.
-  mojo::RemoteSet<blink::mojom::ModelStreamingResponder> responder_set_;
-  base::WeakPtr<content::BrowserContext> browser_context_;
+  void HandleOverflow();
+  void GetSizeInTokens(
+      on_device_model::mojom::InputPtr input,
+      base::OnceCallback<void(std::optional<uint32_t>)> callback);
+
+  // These methods are used for implementing queueing.
+  using QueueCallback = base::OnceCallback<void(base::OnceClosure)>;
+  void AddToQueue(QueueCallback task);
+  void TaskComplete();
+  void RunNext();
+
+  // Contains just the initial prompts. This should not change throughout the
+  // lifetime of this object. If this object is valid, `current_session_` can
+  // also be assumed to be valid, as any disconnects should apply to both
+  // remotes (e.g. a service crash).
+  mojo::Remote<on_device_model::mojom::Session> initial_session_;
+
+  // Contains the current committed session state. This will be replaced after a
+  // successful prompt with the latest session state.
+  mojo::Remote<on_device_model::mojom::Session> current_session_;
+
+  // The session params the initial session was created with.
+  on_device_model::mojom::SessionParamsPtr session_params_;
+
   // Holds all the input and output from the previous prompt.
   std::unique_ptr<Context> context_;
   // It's safe to store `raw_ref` here since both `this` and `ai_manager_` are
   // owned by `context_bound_object_set_`, and they will be destroyed together.
   base::raw_ref<AIContextBoundObjectSet> context_bound_object_set_;
-  base::raw_ref<AIManager> ai_manager_;
 
-  // The accumulated response generated so far.
-  std::string current_response_;
+  // Holds state for any currently active prompt.
+  std::unique_ptr<PromptState> prompt_state_;
 
-  mojo::PendingRemote<blink::mojom::AILanguageModel> pending_remote_;
-  mojo::Receiver<blink::mojom::AILanguageModel> receiver_;
+  // Holds the queue of operations to be run.
+  base::queue<QueueCallback> queue_;
+  // Whether a task is currently running.
+  bool task_running_ = false;
+
+  std::unique_ptr<optimization_guide::SafetyChecker> safety_checker_;
+  base::WeakPtr<optimization_guide::ModelClient> model_client_;
+
+  mojo::Receiver<blink::mojom::AILanguageModel> receiver_{this};
 
   base::WeakPtrFactory<AILanguageModel> weak_ptr_factory_{this};
 };
diff --git a/chrome/browser/ai/ai_language_model_unittest.cc b/chrome/browser/ai/ai_language_model_unittest.cc
index 5308735..a78bed3 100644
--- a/chrome/browser/ai/ai_language_model_unittest.cc
+++ b/chrome/browser/ai/ai_language_model_unittest.cc
@@ -27,6 +27,8 @@
 #include "chrome/browser/component_updater/optimization_guide_on_device_model_installer.h"
 #include "components/optimization_guide/core/mock_optimization_guide_model_executor.h"
 #include "components/optimization_guide/core/model_execution/multimodal_message.h"
+#include "components/optimization_guide/core/model_execution/test/fake_model_assets.h"
+#include "components/optimization_guide/core/model_execution/test/fake_model_broker.h"
 #include "components/optimization_guide/core/optimization_guide_features.h"
 #include "components/optimization_guide/core/optimization_guide_model_executor.h"
 #include "components/optimization_guide/core/optimization_guide_proto_util.h"
@@ -50,47 +52,25 @@
 
 namespace {
 
-using ::optimization_guide::MultimodalMessage;
-using ::optimization_guide::MultimodalMessageReadView;
-using ::optimization_guide::proto::PromptApiPrompt;
-using ::optimization_guide::proto::PromptApiRequest;
-using ::optimization_guide::proto::PromptApiRole;
-using ::optimization_guide::proto::ProtoField;
+using ::optimization_guide::FieldSubstitution;
+using ::optimization_guide::ForbidUnsafe;
+using ::optimization_guide::StringValueField;
 using ::testing::_;
+using ::testing::ElementsAre;
+using ::testing::ElementsAreArray;
 using ::testing::Return;
-using ::testing::ReturnRef;
-using ::testing::Test;
 using Role = ::blink::mojom::AILanguageModelPromptRole;
-using SetInputCallback = ::optimization_guide::OptimizationGuideModelExecutor::
-    Session::SetInputCallback;
 
 constexpr uint32_t kTestMaxContextToken = 10u;
-constexpr uint32_t kTestInitialPromptsToken = 5u;
 constexpr uint32_t kTestDefaultTopK = 1u;
-constexpr float kTestDefaultTemperature = 0.3;
+constexpr float kTestDefaultTemperature = 0.0f;
 constexpr uint32_t kTestMaxTopK = 5u;
 constexpr float kTestMaxTemperature = 1.5;
+constexpr uint32_t kTestMaxTokens = 100u;
 constexpr uint64_t kTestModelDownloadSize = 572u;
 static_assert(kTestDefaultTopK <= kTestMaxTopK);
 static_assert(kTestDefaultTemperature <= kTestMaxTemperature);
 
-const char kTestPrompt[] = "Test prompt";
-const char kExpectedFormattedTestPrompt[] = "U: Test prompt\nM: ";
-const char kTestResponse[] = "Test response";
-
-const char kTestInitialPromptSystem1[] = "Test system prompt";
-const char kTestInitialPromptsUser1[] = "How are you?";
-const char kTestInitialPromptsModel1[] = "I'm fine, thank you, and you?";
-const char kTestInitialPromptsUser2[] = "I'm fine too.";
-const char kExpectedFormattedInitialPrompts[] =
-    ("S: Test system prompt\n"
-     "U: How are you?\n"
-     "M: I'm fine, thank you, and you?\n"
-     "U: I'm fine too.\n");
-
-const char kTestAppendPrompt[] = "Test append prompt";
-const char kExpectedFormattedAppendPrompt[] = "U: Test append prompt\n";
-
 SkBitmap CreateTestBitmap(int width, int height) {
   SkBitmap bitmap;
   bitmap.allocN32Pixels(width, height);
@@ -102,6 +82,14 @@
   return on_device_model::mojom::AudioData::New();
 }
 
+optimization_guide::proto::FeatureTextSafetyConfiguration CreateSafetyConfig() {
+  optimization_guide::proto::FeatureTextSafetyConfiguration safety_config;
+  safety_config.set_feature(
+      optimization_guide::proto::MODEL_EXECUTION_FEATURE_PROMPT_API);
+  safety_config.mutable_safety_category_thresholds()->Add(ForbidUnsafe());
+  return safety_config;
+}
+
 // Build a mojo prompt struct with the specified `role` and `text`
 blink::mojom::AILanguageModelPromptPtr MakePrompt(Role role,
                                                   const std::string& text) {
@@ -117,814 +105,895 @@
   return prompts;
 }
 
-// Build a mojo prompt struct array with a simple set of initial prompts.
-std::vector<blink::mojom::AILanguageModelPromptPtr> GetTestInitialPrompts() {
-  std::vector<blink::mojom::AILanguageModelPromptPtr> prompts;
-  prompts.push_back(MakePrompt(Role::kSystem, kTestInitialPromptSystem1));
-  prompts.push_back(MakePrompt(Role::kUser, kTestInitialPromptsUser1));
-  prompts.push_back(MakePrompt(Role::kAssistant, kTestInitialPromptsModel1));
-  prompts.push_back(MakePrompt(Role::kUser, kTestInitialPromptsUser2));
-  return prompts;
-}
-
 // Construct a ContextItem with system prompt text.
 AILanguageModel::Context::ContextItem SimpleContextItem(std::string text,
                                                         uint32_t size) {
   auto item = AILanguageModel::Context::ContextItem();
   item.tokens = size;
-  item.prompts.emplace_back(
-      MakePrompt(blink::mojom::AILanguageModelPromptRole::kSystem, text));
+  item.input = on_device_model::mojom::Input::New();
+  item.input->pieces = {ml::Token::kSystem, text};
   return item;
 }
 
-// Convert a PromptApiRole to a string for expectation matching.
-const char* FormatPromptRole(PromptApiRole role) {
-  switch (role) {
-    case PromptApiRole::PROMPT_API_ROLE_SYSTEM:
+// Convert a ml::Token to a string for expectation matching.
+const char* FormatToken(ml::Token token) {
+  switch (token) {
+    case ml::Token::kSystem:
       return "S: ";
-    case PromptApiRole::PROMPT_API_ROLE_USER:
+    case ml::Token::kUser:
       return "U: ";
-    case PromptApiRole::PROMPT_API_ROLE_ASSISTANT:
+    case ml::Token::kModel:
       return "M: ";
     default:
       NOTREACHED();
   }
 }
 
-// Construct a ProtoField message that selects a field from it's tag path.
-ProtoField FieldWithTags(std::initializer_list<int32_t> tags) {
-  ProtoField result;
-  for (int32_t tag : tags) {
-    result.add_proto_descriptors()->set_tag_number(tag);
+// Convert an Input to a string for expectation matching.
+std::string FormatInput(const on_device_model::mojom::Input& input) {
+  std::string str;
+  for (const auto& piece : input.pieces) {
+    if (std::holds_alternative<ml::Token>(piece)) {
+      str += FormatToken(std::get<ml::Token>(piece));
+    } else if (std::holds_alternative<std::string>(piece)) {
+      str += std::get<std::string>(piece);
+    } else if (std::holds_alternative<SkBitmap>(piece)) {
+      str += "<image>";
+    } else if (std::holds_alternative<ml::AudioBuffer>(piece)) {
+      str += "<audio>";
+    }
   }
-  return result;
-}
-
-// Convert a MultimodalMessageReadView of PromptApiPrompt to string for
-// expectation matching.
-void FormatPrompt(std::ostringstream& oss, MultimodalMessageReadView view) {
-  PromptApiRole role = static_cast<PromptApiRole>(
-      view.GetValue(FieldWithTags({PromptApiPrompt::kRoleFieldNumber}))
-          ->int32_value());
-  oss << FormatPromptRole(role);
-  oss << view.GetValue(FieldWithTags({PromptApiPrompt::kTextFieldNumber}))
-             ->string_value();
-  if (view.GetImage(FieldWithTags({PromptApiPrompt::kMediaFieldNumber}))) {
-    oss << "<image>";
-  }
-  if (view.GetAudio(FieldWithTags({PromptApiPrompt::kMediaFieldNumber}))) {
-    oss << "<audio>";
-  }
-  oss << "\n";
-}
-
-// Convert a RepeatedMultimodalMessageReadView of PromptApiPrompts to string for
-// expectation matching.
-void FormatPrompts(std::ostringstream& oss,
-                   optimization_guide::RepeatedMultimodalMessageReadView view) {
-  int size = view.Size();
-  for (int i = 0; i < size; i++) {
-    FormatPrompt(oss, view.Get(i));
-  }
-}
-
-// Convert a MultimodalMessageReadView of PromptApiRequest to string for
-// expectation matching.
-void FormatRequest(std::ostringstream& oss, MultimodalMessageReadView view) {
-  FormatPrompts(oss, *view.GetRepeated(FieldWithTags(
-                         {PromptApiRequest::kInitialPromptsFieldNumber})));
-  FormatPrompts(oss, *view.GetRepeated(FieldWithTags(
-                         {PromptApiRequest::kPromptHistoryFieldNumber})));
-  FormatPrompts(oss, *view.GetRepeated(FieldWithTags(
-                         {PromptApiRequest::kCurrentPromptsFieldNumber})));
-  if (view.GetRepeated(
-              FieldWithTags({PromptApiRequest::kCurrentPromptsFieldNumber}))
-          ->Size() > 0) {
-    oss << FormatPromptRole(PromptApiRole::PROMPT_API_ROLE_ASSISTANT);
-  }
-}
-
-// Convert a MultimodalMessage to string for expectation matching.
-std::string ToString(const optimization_guide::MultimodalMessage& request) {
-  if (request.GetTypeName() == "optimization_guide.proto.PromptApiRequest") {
-    std::ostringstream oss;
-    FormatRequest(oss, request.read());
-    return oss.str();
-  }
-  return "unexpected type";
+  return str;
 }
 
 // Convert a Context to string for expectation matching.
 std::string GetContextString(AILanguageModel::Context& ctx) {
-  return ToString(ctx.MakeRequest(on_device_model::Capabilities()));
+  return FormatInput(*ctx.GetNonInitialPrompts());
 }
 
-const optimization_guide::proto::Any& GetPromptApiMetadata() {
-  static base::NoDestructor<optimization_guide::proto::Any> data([]() {
-    optimization_guide::proto::PromptApiMetadata metadata;
-    metadata.set_version(AILanguageModel::kMinVersionUsingProto);
-    return optimization_guide::AnyWrapProto(metadata);
-  }());
-  return *data;
+class TestStreamingResponder
+    : public blink::mojom::ModelStreamingResponder,
+      public blink::mojom::AILanguageModelAppendClient {
+ public:
+  TestStreamingResponder() = default;
+  ~TestStreamingResponder() override = default;
+
+  mojo::PendingRemote<blink::mojom::ModelStreamingResponder> BindRemote() {
+    return receiver_.BindNewPipeAndPassRemote();
+  }
+
+  mojo::PendingRemote<blink::mojom::AILanguageModelAppendClient>
+  BindAppendRemote() {
+    return append_receiver_.BindNewPipeAndPassRemote();
+  }
+
+  // Returns true on successful completion and false on error.
+  bool WaitForCompletion() {
+    run_loop_.Run();
+    return !error_status_.has_value();
+  }
+
+  void WaitForQuotaOverflow() { quota_overflow_run_loop_.Run(); }
+
+  blink::mojom::ModelStreamingResponseStatus error_status() const {
+    EXPECT_TRUE(error_status_.has_value());
+    return *error_status_;
+  }
+
+  const std::vector<std::string> responses() const { return responses_; }
+  uint64_t current_tokens() const { return current_tokens_; }
+
+ private:
+  // blink::mojom::ModelStreamingResponder:
+  void OnError(blink::mojom::ModelStreamingResponseStatus status) override {
+    error_status_ = status;
+    run_loop_.Quit();
+  }
+
+  void OnStreaming(const std::string& text) override {
+    responses_.push_back(text);
+  }
+
+  void OnCompletion(
+      blink::mojom::ModelExecutionContextInfoPtr context_info) override {
+    current_tokens_ = context_info->current_tokens;
+    run_loop_.Quit();
+  }
+
+  // blink::mojom::AILanguageModelAppendClient:
+  void OnAppendComplete() override { run_loop_.Quit(); }
+
+  void OnQuotaOverflow() override { quota_overflow_run_loop_.Quit(); }
+
+  std::optional<blink::mojom::ModelStreamingResponseStatus> error_status_;
+  std::vector<std::string> responses_;
+  uint64_t current_tokens_ = 0;
+  base::RunLoop run_loop_;
+  base::RunLoop quota_overflow_run_loop_;
+  mojo::Receiver<blink::mojom::ModelStreamingResponder> receiver_{this};
+  mojo::Receiver<blink::mojom::AILanguageModelAppendClient> append_receiver_{
+      this};
+};
+
+class TestMeasureInputUsageClient
+    : public blink::mojom::AILanguageModelMeasureInputUsageClient {
+ public:
+  TestMeasureInputUsageClient() = default;
+  ~TestMeasureInputUsageClient() override = default;
+
+  mojo::PendingRemote<blink::mojom::AILanguageModelMeasureInputUsageClient>
+  BindRemote() {
+    return receiver_.BindNewPipeAndPassRemote();
+  }
+
+  uint32_t Wait() {
+    run_loop_.Run();
+    return number_of_tokens_;
+  }
+
+ private:
+  // blink::mojom::AILanguageModelMeasureInputUsageClient::
+  void OnResult(uint32_t number_of_tokens) override {
+    number_of_tokens_ = number_of_tokens;
+    run_loop_.Quit();
+  }
+
+  base::RunLoop run_loop_;
+  uint32_t number_of_tokens_ = 0;
+  mojo::Receiver<blink::mojom::AILanguageModelMeasureInputUsageClient>
+      receiver_{this};
+};
+
+optimization_guide::proto::OnDeviceModelExecutionFeatureConfig CreateConfig() {
+  optimization_guide::proto::OnDeviceModelExecutionFeatureConfig config;
+  config.set_can_skip_text_safety(true);
+  optimization_guide::proto::SamplingParams sampling_params;
+  sampling_params.set_top_k(kTestMaxTopK);
+  sampling_params.set_temperature(kTestMaxTemperature);
+  *config.mutable_sampling_params() = sampling_params;
+
+  config.mutable_input_config()->set_max_context_tokens(kTestMaxTokens);
+
+  optimization_guide::proto::PromptApiMetadata metadata;
+  *metadata.mutable_max_sampling_params() = sampling_params;
+  *config.mutable_feature_metadata() =
+      optimization_guide::AnyWrapProto(metadata);
+
+  config.set_feature(optimization_guide::proto::ModelExecutionFeature::
+                         MODEL_EXECUTION_FEATURE_PROMPT_API);
+  return config;
 }
 
-optimization_guide::OptimizationGuideModelStreamingExecutionResult
-CreateExecutionResult(const std::string& output,
-                      bool is_complete,
-                      uint32_t input_token_count,
-                      uint32_t output_token_count) {
-  optimization_guide::proto::StringValue response;
-  response.set_value(output);
-
-  return optimization_guide::OptimizationGuideModelStreamingExecutionResult(
-      optimization_guide::StreamingResponse{
-          .response = optimization_guide::AnyWrapProto(response),
-          .is_complete = is_complete,
-          .input_token_count = input_token_count,
-          .output_token_count = output_token_count},
-      /*provided_by_on_device=*/true);
+// Formats responses to match what the fake on device model service will return.
+// The fake service keeps track of all previous inputs to a session, and will
+// spit them all back out during a Generate() call. This gets a bit complicated
+// for the language model, which also adds back the output as input to the
+// session. An example language model session using the default behavior of the
+// fake service would look something like this:
+// - s1.Prompt("foo")
+//   - Adds "UfooEM" to the session
+//   - Gets output of ["Context: UfooEM\n"] from fake service
+//   - Adds "Context: UfooEM\nE" to the session (fake response + end token)
+// - s1.Prompt("bar")
+//   - Adds "UbarEM" to the session
+//   - Gets output of ["Context: UfooEM\n", "Context: Context: UfooEM\nE\n",
+//     "Context: UbarEM\n"].
+//   - Adds "Context: UfooEM\nContext: Context: UfooEM\nE\nContext: UbarEM\n"
+//     (concatenated output from fake service) to the session
+// This behavior verifies the correct inputs and outputs are being returned from
+// the model, and this helper makes it easier to construct these expectations.
+// TODO(crbug.com/415808003): Simplify this in the fake service.
+std::vector<std::string> FormatResponses(
+    const std::vector<std::string>& responses) {
+  std::vector<std::string> formatted;
+  std::string last_output;
+  for (const std::string& response : responses) {
+    if (!last_output.empty()) {
+      formatted.push_back("Context: " + last_output + "E\n");
+      last_output += formatted.back();
+    }
+    formatted.push_back("Context: " + response + "\n");
+    last_output += formatted.back();
+  }
+  return formatted;
 }
 
 class AILanguageModelTest : public AITestUtils::AITestBase {
  public:
-  struct Options {
-    blink::mojom::AILanguageModelSamplingParamsPtr sampling_params = nullptr;
-    std::vector<blink::mojom::AILanguageModelPromptPtr> initial_prompts;
-    std::string prompt_input = kTestPrompt;
-    std::string expected_context = "";
-    std::string expected_cloned_context =
-        base::StrCat({kExpectedFormattedTestPrompt, kTestResponse, "\n"});
-    std::string expected_prompt = kExpectedFormattedTestPrompt;
-    bool should_overflow_context = false;
-    bool should_use_supported_language = true;
-  };
+  AILanguageModelTest()
+      : fake_broker_(optimization_guide::FakeAdaptationAsset(
+            {.config = CreateConfig()})) {}
+
+  void SetUp() override {
+    AITestBase::SetUp();
+    SetupMockOptimizationGuideKeyedService();
+    ai_manager_ =
+        std::make_unique<AIManager>(main_rfh()->GetBrowserContext(),
+                                    &component_update_service_, main_rfh());
+  }
 
  protected:
   void SetupMockOptimizationGuideKeyedService() override {
     AITestUtils::AITestBase::SetupMockOptimizationGuideKeyedService();
-    ON_CALL(*mock_optimization_guide_keyed_service_, GetSamplingParamsConfig(_))
-        .WillByDefault([](optimization_guide::ModelBasedCapabilityKey feature) {
+    ON_CALL(*mock_optimization_guide_keyed_service_, CreateModelBrokerClient())
+        .WillByDefault([&]() {
+          return std::make_unique<optimization_guide::ModelBrokerClient>(
+              fake_broker_.BindAndPassRemote(),
+              optimization_guide::CreateSessionArgs(nullptr, {}));
+        });
+    ON_CALL(*mock_optimization_guide_keyed_service_,
+            GetSamplingParamsConfig(
+                optimization_guide::ModelBasedCapabilityKey::kPromptApi))
+        .WillByDefault([]() {
           return optimization_guide::SamplingParamsConfig{
               .default_top_k = kTestDefaultTopK,
               .default_temperature = kTestDefaultTemperature};
         });
-
-    ON_CALL(*mock_optimization_guide_keyed_service_, GetFeatureMetadata(_))
-        .WillByDefault([](optimization_guide::ModelBasedCapabilityKey feature) {
-          optimization_guide::proto::SamplingParams sampling_params;
-          sampling_params.set_top_k(kTestMaxTopK);
-          sampling_params.set_temperature(kTestMaxTemperature);
-          optimization_guide::proto::PromptApiMetadata metadata;
-          *metadata.mutable_max_sampling_params() = sampling_params;
-          optimization_guide::proto::Any any;
-          any.set_value(metadata.SerializeAsString());
-          any.set_type_url(
-              base::StrCat({"type.googleapis.com/", metadata.GetTypeName()}));
-          return any;
-        });
+    ON_CALL(*mock_optimization_guide_keyed_service_,
+            GetFeatureMetadata(
+                optimization_guide::ModelBasedCapabilityKey::kPromptApi))
+        .WillByDefault([]() { return CreateConfig().feature_metadata(); });
+    ON_CALL(*mock_optimization_guide_keyed_service_, GetOnDeviceCapabilities())
+        .WillByDefault(Return(on_device_model::Capabilities(
+            {on_device_model::CapabilityFlags::kImageInput,
+             on_device_model::CapabilityFlags::kAudioInput})));
   }
 
-  // The helper function that creates a `AILanguageModel` and executes the
-  // prompt.
-  void RunPromptTest(Options options) {
-    blink::mojom::AILanguageModelSamplingParamsPtr sampling_params_copy;
-    if (options.sampling_params) {
-      sampling_params_copy = options.sampling_params->Clone();
-    }
-
-    // Set up mock service.
-    SetupMockOptimizationGuideKeyedService();
-
-    if (options.should_use_supported_language) {
-      // `StartSession()` will run twice when creating and cloning the session.
-      EXPECT_CALL(*mock_optimization_guide_keyed_service_, StartSession(_, _))
-          .Times(2)
-          .WillOnce([&](optimization_guide::ModelBasedCapabilityKey feature,
-                        const std::optional<
-                            optimization_guide::SessionConfigParams>&
-                            config_params) {
-            auto session = std::make_unique<
-                testing::NiceMock<optimization_guide::MockSession>>();
-            if (sampling_params_copy) {
-              EXPECT_EQ(config_params->sampling_params->top_k,
-                        std::min(kTestMaxTopK, sampling_params_copy->top_k));
-              EXPECT_EQ(config_params->sampling_params->temperature,
-                        std::min(kTestMaxTemperature,
-                                 sampling_params_copy->temperature));
-            }
-
-            SetUpMockSession(*session);
-
-            ON_CALL(*session, GetContextSizeInTokens(_, _))
-                .WillByDefault([&](MultimodalMessageReadView request_metadata,
-                                   optimization_guide::
-                                       OptimizationGuideModelSizeInTokenCallback
-                                           callback) {
-                  std::move(callback).Run(
-                      options.should_overflow_context
-                          ? AITestUtils::GetFakeTokenLimits()
-                                    .max_context_tokens +
-                                1
-                          : 1);
-                });
-            ON_CALL(*session, SetInput(_, _))
-                .WillByDefault(
-                    [&, initial = true](MultimodalMessage request_metadata,
-                                        SetInputCallback callback) mutable {
-                      if (initial && !options.expected_context.empty()) {
-                        initial = false;
-                        EXPECT_THAT(ToString(request_metadata),
-                                    options.expected_context);
-                      } else {
-                        EXPECT_THAT(
-                            ToString(request_metadata),
-                            options.expected_context + options.expected_prompt);
-                      }
-                    });
-
-            EXPECT_CALL(*session, ExecuteModelWithResponseConstraint(_, _, _))
-                .WillOnce(
-                    [&](const google::protobuf::MessageLite& request_metadata,
-                        on_device_model::mojom::ResponseConstraintPtr
-                            constraint,
-                        optimization_guide::
-                            OptimizationGuideModelExecutionResultStreamingCallback
-                                callback) {
-                      EXPECT_THAT(request_metadata.ByteSizeLong(), 0);
-                      StreamResponse(callback);
-                    });
-            return session;
-          })
-          .WillOnce([&](optimization_guide::ModelBasedCapabilityKey feature,
-                        const std::optional<
-                            optimization_guide::SessionConfigParams>&
-                            config_params) {
-            auto session = std::make_unique<
-                testing::NiceMock<optimization_guide::MockSession>>();
-
-            SetUpMockSession(*session);
-
-            ON_CALL(*session, SetInput(_, _))
-                .WillByDefault([&](MultimodalMessage request_metadata,
-                                   SetInputCallback callback) {
-                  EXPECT_THAT(ToString(request_metadata),
-                              options.expected_cloned_context +
-                                  options.expected_prompt);
-                });
-            EXPECT_CALL(*session, ExecuteModelWithResponseConstraint(_, _, _))
-                .WillOnce(
-                    [&](const google::protobuf::MessageLite& request_metadata,
-                        on_device_model::mojom::ResponseConstraintPtr
-                            constraint,
-                        optimization_guide::
-                            OptimizationGuideModelExecutionResultStreamingCallback
-                                callback) {
-                      EXPECT_THAT(request_metadata.ByteSizeLong(), 0);
-                      StreamResponse(callback);
-                    });
-            return session;
-          });
-    }
-
-    // Test session creation.
-    mojo::Remote<blink::mojom::AILanguageModel> mock_session;
-    AITestUtils::MockCreateLanguageModelClient
-        mock_create_language_model_client;
-    base::RunLoop creation_run_loop;
-    const bool had_initial_prompts = !options.initial_prompts.empty();
-    if (options.should_use_supported_language) {
-      EXPECT_CALL(mock_create_language_model_client, OnResult(_, _))
-          .WillOnce([&](mojo::PendingRemote<blink::mojom::AILanguageModel>
-                            language_model,
-                        blink::mojom::AILanguageModelInstanceInfoPtr info) {
-            EXPECT_TRUE(language_model);
-            EXPECT_EQ(info->input_quota,
-                      AITestUtils::GetFakeTokenLimits().max_context_tokens);
-            EXPECT_EQ(info->input_usage > 0, had_initial_prompts);
-            mock_session = mojo::Remote<blink::mojom::AILanguageModel>(
-                std::move(language_model));
-            creation_run_loop.Quit();
-          });
-    } else {
-      EXPECT_CALL(mock_create_language_model_client, OnError(_))
-          .WillOnce([&](blink::mojom::AIManagerCreateClientError error) {
-            EXPECT_EQ(
-                error,
-                blink::mojom::AIManagerCreateClientError::kUnsupportedLanguage);
-            creation_run_loop.Quit();
-          });
-    }
-
-    mojo::Remote<blink::mojom::AIManager> mock_remote = GetAIManagerRemote();
-
-    if (options.should_use_supported_language) {
-      EXPECT_EQ(GetAIManagerDownloadProgressObserversSize(), 0u);
-      AITestUtils::FakeMonitor mock_monitor;
-
-      EXPECT_CALL(component_update_service_, GetComponentIDs()).Times(1);
-      mock_remote->AddModelDownloadProgressObserver(
-          mock_monitor.BindNewPipeAndPassRemote());
-
-      ASSERT_TRUE(base::test::RunUntil([this] {
-        return GetAIManagerDownloadProgressObserversSize() == 1u;
-      }));
-
-      // This is the component id of the on device model. The `AIManager` sends
-      // updates for it to the `CreateMonitor`s.
-      std::string model_component_id =
-          component_updater::OptimizationGuideOnDeviceModelInstallerPolicy::
-              GetOnDeviceModelExtensionId();
-      AITestUtils::FakeComponent model_component(model_component_id,
-                                                 kTestModelDownloadSize);
-
-      component_update_service_.SendUpdate(model_component.CreateUpdateItem(
-          update_client::ComponentState::kDownloading, kTestModelDownloadSize));
-
-      mock_monitor.ExpectReceivedNormalizedUpdate(0, kTestModelDownloadSize);
-      mock_monitor.ExpectReceivedNormalizedUpdate(kTestModelDownloadSize,
-                                                  kTestModelDownloadSize);
-    }
-
-    std::vector<blink::mojom::AILanguageModelExpectedInputPtr> expected_inputs;
-    if (!options.should_use_supported_language) {
-      expected_inputs.push_back(blink::mojom::AILanguageModelExpectedInput::New(
-          blink::mojom::AILanguageModelPromptType::kText,
-          AITestUtils::ToMojoLanguageCodes({"ja"})));
-    }
-
-    mock_remote->CreateLanguageModel(
-        mock_create_language_model_client.BindNewPipeAndPassRemote(),
-        blink::mojom::AILanguageModelCreateOptions::New(
-            std::move(options.sampling_params),
-            std::move(options.initial_prompts), std::move(expected_inputs)));
-    creation_run_loop.Run();
-
-    if (!options.should_use_supported_language) {
-      return;
-    }
-
-    AITestUtils::MockModelStreamingResponder mock_responder;
-
-    TestPromptCall(mock_session, options.prompt_input,
-                   options.should_overflow_context);
-
-    // Test session cloning.
-    mojo::Remote<blink::mojom::AILanguageModel> mock_cloned_session;
-    AITestUtils::MockCreateLanguageModelClient mock_clone_language_model_client;
-    base::RunLoop clone_run_loop;
-    EXPECT_CALL(mock_clone_language_model_client, OnResult(_, _))
-        .WillOnce(testing::Invoke(
-            [&](mojo::PendingRemote<blink::mojom::AILanguageModel>
-                    language_model,
-                blink::mojom::AILanguageModelInstanceInfoPtr info) {
-              EXPECT_TRUE(language_model);
-              mock_cloned_session = mojo::Remote<blink::mojom::AILanguageModel>(
-                  std::move(language_model));
-              clone_run_loop.Quit();
-            }));
-
-    mock_session->Fork(
-        mock_clone_language_model_client.BindNewPipeAndPassRemote());
-    clone_run_loop.Run();
-
-    TestPromptCall(mock_cloned_session, options.prompt_input,
-                   /*should_overflow_context=*/false);
-  }
-
-  void TestSessionDestroy(
-      base::OnceCallback<void(
-          mojo::Remote<blink::mojom::AILanguageModel> mock_session,
-          AITestUtils::MockModelStreamingResponder& mock_responder)> callback) {
-    SetupMockOptimizationGuideKeyedService();
-    base::OnceClosure size_in_token_callback;
-    EXPECT_CALL(*mock_optimization_guide_keyed_service_, StartSession(_, _))
-        .WillOnce(
-            [&](optimization_guide::ModelBasedCapabilityKey feature,
-                const std::optional<optimization_guide::SessionConfigParams>&
-                    config_params) {
-              auto session = std::make_unique<
-                  testing::NiceMock<optimization_guide::MockSession>>();
-
-              SetUpMockSession(*session);
-              ON_CALL(*session, GetExecutionInputSizeInTokens(_, _))
-                  .WillByDefault(
-                      [&](MultimodalMessageReadView request_metadata,
-                          optimization_guide::
-                              OptimizationGuideModelSizeInTokenCallback
-                                  callback) {
-                        size_in_token_callback =
-                            base::BindOnce(std::move(callback), 1);
-                      });
-
-              // The model should not be executed.
-              EXPECT_CALL(*session, ExecuteModelWithResponseConstraint(_, _, _))
-                  .Times(0);
-              return session;
-            });
-
-    mojo::Remote<blink::mojom::AILanguageModel> mock_session =
-        CreateMockSession();
-
-    AITestUtils::MockModelStreamingResponder mock_responder;
-
-    base::RunLoop responder_run_loop;
-
-    EXPECT_CALL(mock_responder, OnError(_))
-        .WillOnce(testing::Invoke(
-            [&](blink::mojom::ModelStreamingResponseStatus status) {
-              EXPECT_EQ(status, blink::mojom::ModelStreamingResponseStatus::
-                                    kErrorSessionDestroyed);
-              responder_run_loop.Quit();
-            }));
-
-    std::move(callback).Run(std::move(mock_session), mock_responder);
-    // Defers the `size_in_token_callback` until the testing callback which
-    // destroys the session is run.
-    if (size_in_token_callback) {
-      std::move(size_in_token_callback).Run();
-    }
-    responder_run_loop.Run();
-  }
-
-  void TestSessionAddContext(bool should_overflow_context) {
-    SetupMockOptimizationGuideKeyedService();
-    // Use `max_context_token / 2 + 1` to ensure the
-    // context overflow on the second prompt.
-    uint32_t mock_size_in_tokens =
-        should_overflow_context
-            ? 1 + AITestUtils::GetFakeTokenLimits().max_context_tokens / 2
-            : 1;
-
-    EXPECT_CALL(*mock_optimization_guide_keyed_service_, StartSession(_, _))
-        .WillOnce([&](optimization_guide::ModelBasedCapabilityKey feature,
-                      const std::optional<
-                          optimization_guide::SessionConfigParams>&
-                          config_params) {
-          auto session = std::make_unique<
-              testing::NiceMock<optimization_guide::MockSession>>();
-
-          SetUpMockSession(*session);
-
-          ON_CALL(*session, GetContextSizeInTokens(_, _))
-              .WillByDefault(
-                  [&](MultimodalMessageReadView request_metadata,
-                      optimization_guide::
-                          OptimizationGuideModelSizeInTokenCallback callback) {
-                    std::move(callback).Run(mock_size_in_tokens);
-                  });
-
-          ON_CALL(*session, GetExecutionInputSizeInTokens(_, _))
-              .WillByDefault(
-                  [&](MultimodalMessageReadView request_metadata,
-                      optimization_guide::
-                          OptimizationGuideModelSizeInTokenCallback callback) {
-                    std::move(callback).Run(mock_size_in_tokens);
-                  });
-
-          EXPECT_CALL(*session, SetInput(_, _))
-              .Times(2)
-              .WillOnce(
-                  [&](MultimodalMessage request, SetInputCallback callback) {
-                    EXPECT_THAT(ToString(request), "U: A\nM: ");
-                  })
-              .WillOnce([&](MultimodalMessage request,
-                            SetInputCallback callback) {
-                // Prompt history should be omitted if it would overflow.
-                EXPECT_THAT(ToString(request), should_overflow_context
-                                                   ? "U: B\nM: "
-                                                   : "U: A\nM: OK\nU: B\nM: ");
-              });
-
-          EXPECT_CALL(*session, ExecuteModelWithResponseConstraint(_, _, _))
-              .Times(2)
-              .WillRepeatedly(
-                  [&](const google::protobuf::MessageLite& request_metadata,
-                      on_device_model::mojom::ResponseConstraintPtr constraint,
-                      optimization_guide::
-                          OptimizationGuideModelExecutionResultStreamingCallback
-                              callback) {
-                    EXPECT_THAT(request_metadata.ByteSizeLong(), 0);
-                    callback.Run(CreateExecutionResult(
-                        "OK", /*is_complete=*/true, /*input_token_count=*/1u,
-                        /*output_token_count=*/mock_size_in_tokens));
-                  });
-          return session;
-        });
-
-    mojo::Remote<blink::mojom::AILanguageModel> mock_session =
-        CreateMockSession();
-
-    AITestUtils::MockModelStreamingResponder mock_responder_1;
-    AITestUtils::MockModelStreamingResponder mock_responder_2;
-
-    base::RunLoop responder_run_loop_1;
-    base::RunLoop responder_run_loop_2;
-
-    EXPECT_CALL(mock_responder_1, OnStreaming(_))
-        .WillOnce(testing::Invoke(
-            [&](const std::string& text) { EXPECT_THAT(text, "OK"); }));
-    EXPECT_CALL(mock_responder_2, OnStreaming(_))
-        .WillOnce(testing::Invoke(
-            [&](const std::string& text) { EXPECT_THAT(text, "OK"); }));
-
-    EXPECT_CALL(mock_responder_2, OnQuotaOverflow())
-        .Times(should_overflow_context ? 1 : 0);
-
-    EXPECT_CALL(mock_responder_1, OnCompletion(_))
-        .WillOnce(testing::Invoke(
-            [&](blink::mojom::ModelExecutionContextInfoPtr context_info) {
-              responder_run_loop_1.Quit();
-            }));
-    EXPECT_CALL(mock_responder_2, OnCompletion(_))
-        .WillOnce(testing::Invoke(
-            [&](blink::mojom::ModelExecutionContextInfoPtr context_info) {
-              responder_run_loop_2.Quit();
-            }));
-
-    mock_session->Prompt(MakeInput("A"), /*constraint=*/nullptr,
-                         mock_responder_1.BindNewPipeAndPassRemote());
-    responder_run_loop_1.Run();
-    mock_session->Prompt(MakeInput("B"), /*constraint=*/nullptr,
-                         mock_responder_2.BindNewPipeAndPassRemote());
-    responder_run_loop_2.Run();
-  }
-
-  void SetUpMockSession(
-      testing::NiceMock<optimization_guide::MockSession>& session) {
-    ON_CALL(session, GetTokenLimits())
-        .WillByDefault(AITestUtils::GetFakeTokenLimits);
-
-    ON_CALL(session, GetOnDeviceFeatureMetadata())
-        .WillByDefault(ReturnRef(GetPromptApiMetadata()));
-    ON_CALL(session, GetSamplingParams()).WillByDefault([]() {
-      // We don't need to use these value, so just mock it with defaults.
-      return optimization_guide::SamplingParams{
-          /*top_k=*/kTestDefaultTopK,
-          /*temperature=*/kTestDefaultTemperature};
-    });
-    ON_CALL(session, GetSizeInTokens(_, _))
-        .WillByDefault(
-            [](const std::string& text,
-               optimization_guide::OptimizationGuideModelSizeInTokenCallback
-                   callback) { std::move(callback).Run(1); });
-    ON_CALL(session, GetExecutionInputSizeInTokens(_, _))
-        .WillByDefault(
-            [](MultimodalMessageReadView request_metadata,
-               optimization_guide::OptimizationGuideModelSizeInTokenCallback
-                   callback) { std::move(callback).Run(1); });
-    ON_CALL(session, GetContextSizeInTokens(_, _))
-        .WillByDefault(
-            [](MultimodalMessageReadView request_metadata,
-               optimization_guide::OptimizationGuideModelSizeInTokenCallback
-                   callback) { std::move(callback).Run(1); });
-  }
-
-  void StreamResponse(
-      optimization_guide::OptimizationGuideModelExecutionResultStreamingCallback
-          callback) {
-    std::string responses[3];
-    std::string response = std::string(kTestResponse);
-    responses[0] = response.substr(0, 1);
-    responses[1] = response.substr(1);
-    responses[2] = "";
-    callback.Run(CreateExecutionResult(responses[0],
-                                       /*is_complete=*/false,
-                                       /*input_token_count=*/1u,
-                                       /*output_token_count=*/1u));
-    callback.Run(CreateExecutionResult(responses[1],
-                                       /*is_complete=*/false,
-                                       /*input_token_count=*/1u,
-                                       /*output_token_count=*/1u));
-    callback.Run(CreateExecutionResult(responses[2],
-                                       /*is_complete=*/true,
-                                       /*input_token_count=*/1u,
-                                       /*output_token_count=*/1u));
-  }
-
-  void TestPromptCall(mojo::Remote<blink::mojom::AILanguageModel>& mock_session,
-                      const std::string& prompt,
-                      bool should_overflow_context) {
-    AITestUtils::MockModelStreamingResponder mock_responder;
-
-    base::RunLoop responder_run_loop;
-    std::string response = std::string(kTestResponse);
-    EXPECT_CALL(mock_responder, OnStreaming(_))
-        .Times(3)
-        .WillOnce(testing::Invoke([&](const std::string& text) {
-          EXPECT_THAT(text, response.substr(0, 1));
-        }))
-        .WillOnce(testing::Invoke([&](const std::string& text) {
-          EXPECT_THAT(text, response.substr(1));
-        }))
-        .WillOnce(testing::Invoke(
-            [&](const std::string& text) { EXPECT_THAT(text, ""); }));
-
-    EXPECT_CALL(mock_responder, OnCompletion(_))
-        .WillOnce(testing::Invoke(
-            [&](blink::mojom::ModelExecutionContextInfoPtr context_info) {
-              responder_run_loop.Quit();
-            }));
-
-    mock_session->Prompt(MakeInput(prompt),
-                         /*constraint=*/nullptr,
-                         mock_responder.BindNewPipeAndPassRemote());
-    responder_run_loop.Run();
-  }
-
-  mojo::Remote<blink::mojom::AILanguageModel> CreateMockSession() {
-    mojo::Remote<blink::mojom::AILanguageModel> mock_session;
-    AITestUtils::MockCreateLanguageModelClient
-        mock_create_language_model_client;
-    base::RunLoop creation_run_loop;
-    EXPECT_CALL(mock_create_language_model_client, OnResult(_, _))
+  mojo::Remote<blink::mojom::AILanguageModel> CreateSession(
+      blink::mojom::AILanguageModelCreateOptionsPtr options =
+          blink::mojom::AILanguageModelCreateOptions::New()) {
+    base::test::TestFuture<mojo::Remote<blink::mojom::AILanguageModel>> future;
+    AITestUtils::MockCreateLanguageModelClient language_model_client;
+    EXPECT_CALL(language_model_client, OnResult(_, _))
         .WillOnce([&](mojo::PendingRemote<blink::mojom::AILanguageModel>
                           language_model,
                       blink::mojom::AILanguageModelInstanceInfoPtr info) {
-          EXPECT_TRUE(language_model);
-          mock_session = mojo::Remote<blink::mojom::AILanguageModel>(
-              std::move(language_model));
-          creation_run_loop.Quit();
+          future.SetValue(mojo::Remote<blink::mojom::AILanguageModel>(
+              std::move(language_model)));
         });
 
-    mojo::Remote<blink::mojom::AIManager> mock_remote = GetAIManagerRemote();
-
-    mock_remote->CreateLanguageModel(
-        mock_create_language_model_client.BindNewPipeAndPassRemote(),
-        blink::mojom::AILanguageModelCreateOptions::New());
-    creation_run_loop.Run();
-
-    return mock_session;
+    GetAIManagerRemote()->CreateLanguageModel(
+        language_model_client.BindNewPipeAndPassRemote(), std::move(options));
+    return future.Take();
   }
 
- private:
-  base::test::ScopedFeatureList scoped_feature_list_;
+  std::vector<std::string> Prompt(
+      blink::mojom::AILanguageModel& model,
+      std::vector<blink::mojom::AILanguageModelPromptPtr> input,
+      on_device_model::mojom::ResponseConstraintPtr constraint = nullptr) {
+    TestStreamingResponder responder;
+    model.Prompt(std::move(input), std::move(constraint),
+                 responder.BindRemote());
+    EXPECT_TRUE(responder.WaitForCompletion());
+    return responder.responses();
+  }
+
+  void Append(blink::mojom::AILanguageModel& model,
+              std::vector<blink::mojom::AILanguageModelPromptPtr> input) {
+    TestStreamingResponder responder;
+    model.Append(std::move(input), responder.BindAppendRemote());
+    EXPECT_TRUE(responder.WaitForCompletion());
+  }
+
+  mojo::Remote<blink::mojom::AILanguageModel> Fork(
+      blink::mojom::AILanguageModel& model) {
+    base::test::TestFuture<mojo::Remote<blink::mojom::AILanguageModel>> future;
+    AITestUtils::MockCreateLanguageModelClient language_model_client;
+    EXPECT_CALL(language_model_client, OnResult(_, _))
+        .WillOnce([&](mojo::PendingRemote<blink::mojom::AILanguageModel>
+                          language_model,
+                      blink::mojom::AILanguageModelInstanceInfoPtr info) {
+          future.SetValue(mojo::Remote<blink::mojom::AILanguageModel>(
+              std::move(language_model)));
+        });
+
+    model.Fork(language_model_client.BindNewPipeAndPassRemote());
+    return future.Take();
+  }
+
+ protected:
+  base::test::ScopedFeatureList scoped_feature_list_{
+      blink::features::kAIPromptAPIMultimodalInput};
+  optimization_guide::FakeModelBroker fake_broker_;
 };
 
-TEST_F(AILanguageModelTest, PromptDefaultSession) {
-  RunPromptTest(AILanguageModelTest::Options{
-      .prompt_input = kTestPrompt,
-      .expected_prompt = kExpectedFormattedTestPrompt,
-  });
+TEST_F(AILanguageModelTest, MultiplePrompts) {
+  auto session = CreateSession();
+  EXPECT_THAT(Prompt(*session, MakeInput("foo")),
+              ElementsAreArray(FormatResponses({"UfooEM"})));
+  EXPECT_THAT(Prompt(*session, MakeInput("bar")),
+              ElementsAreArray(FormatResponses({"UfooEM", "UbarEM"})));
+  EXPECT_THAT(
+      Prompt(*session, MakeInput("baz")),
+      ElementsAreArray(FormatResponses({"UfooEM", "UbarEM", "UbazEM"})));
 }
 
-TEST_F(AILanguageModelTest, PromptSessionWithSamplingParams) {
-  RunPromptTest(AILanguageModelTest::Options{
-      .sampling_params = blink::mojom::AILanguageModelSamplingParams::New(
-          /*top_k=*/kTestMaxTopK - 1,
-          /*temperature=*/kTestMaxTemperature * 0.9),
-      .prompt_input = kTestPrompt,
-      .expected_prompt = kExpectedFormattedTestPrompt,
-  });
+TEST_F(AILanguageModelTest, Append) {
+  auto session = CreateSession();
+  Append(*session, MakeInput("foo"));
+  EXPECT_THAT(Prompt(*session, MakeInput("bar")),
+              ElementsAre("Context: UfooE\n", "Context: UbarEM\n"));
 }
 
-TEST_F(AILanguageModelTest, PromptSessionWithSamplingParams_ExceedMaxTopK) {
-  RunPromptTest(AILanguageModelTest::Options{
-      .sampling_params = blink::mojom::AILanguageModelSamplingParams::New(
-          /*top_k=*/kTestMaxTopK + 1,
-          /*temperature=*/kTestMaxTemperature * 0.9),
-      .prompt_input = kTestPrompt,
-      .expected_prompt = kExpectedFormattedTestPrompt,
-  });
+TEST_F(AILanguageModelTest, PromptTokenCounts) {
+  fake_broker_.settings().set_execute_result({"hi"});
+  auto session = CreateSession();
+
+  std::string expected_tokens = "UfooEMhiE";
+  {
+    TestStreamingResponder responder;
+    session->Prompt(MakeInput("foo"), nullptr, responder.BindRemote());
+    EXPECT_TRUE(responder.WaitForCompletion());
+    EXPECT_EQ(responder.current_tokens(), expected_tokens.size());
+  }
+  expected_tokens += "UbarEMhiE";
+  {
+    TestStreamingResponder responder;
+    session->Prompt(MakeInput("bar"), nullptr, responder.BindRemote());
+    EXPECT_TRUE(responder.WaitForCompletion());
+    EXPECT_EQ(responder.current_tokens(), expected_tokens.size());
+  }
+  auto fork = Fork(*session);
+  expected_tokens += "UbazEMhiE";
+  {
+    TestStreamingResponder responder;
+    fork->Prompt(MakeInput("baz"), nullptr, responder.BindRemote());
+    EXPECT_TRUE(responder.WaitForCompletion());
+    EXPECT_EQ(responder.current_tokens(), expected_tokens.size());
+  }
 }
 
-TEST_F(AILanguageModelTest,
-       PromptSessionWithSamplingParams_ExceedMaxTemperature) {
-  RunPromptTest(AILanguageModelTest::Options{
-      .sampling_params = blink::mojom::AILanguageModelSamplingParams::New(
-          /*top_k=*/kTestMaxTopK - 1,
-          /*temperature=*/kTestMaxTemperature + 0.1),
-      .prompt_input = kTestPrompt,
-      .expected_prompt = kExpectedFormattedTestPrompt,
-  });
+TEST_F(AILanguageModelTest, Roles) {
+  auto session = CreateSession();
+  std::vector<blink::mojom::AILanguageModelPromptPtr> prompts;
+  prompts.push_back(MakePrompt(Role::kUser, "user"));
+  prompts.push_back(MakePrompt(Role::kSystem, "system"));
+  prompts.push_back(MakePrompt(Role::kAssistant, "model"));
+  EXPECT_THAT(Prompt(*session, std::move(prompts)),
+              ElementsAreArray(FormatResponses({"UuserESsystemEMmodelEM"})));
 }
 
-TEST_F(AILanguageModelTest, PromptSessionWithInitialPrompts) {
-  RunPromptTest(AILanguageModelTest::Options{
-      .initial_prompts = GetTestInitialPrompts(),
-      .prompt_input = kTestPrompt,
-      .expected_context = kExpectedFormattedInitialPrompts,
-      .expected_cloned_context =
-          base::StrCat({kExpectedFormattedInitialPrompts,
-                        kExpectedFormattedTestPrompt, kTestResponse, "\n"}),
-      .expected_prompt = kExpectedFormattedTestPrompt,
-  });
+TEST_F(AILanguageModelTest, Fork) {
+  auto session = CreateSession();
+  auto fork1 = Fork(*session);
+
+  EXPECT_THAT(Prompt(*session, MakeInput("foo")),
+              ElementsAreArray(FormatResponses({"UfooEM"})));
+  auto fork2 = Fork(*session);
+
+  EXPECT_THAT(Prompt(*session, MakeInput("bar")),
+              ElementsAreArray(FormatResponses({"UfooEM", "UbarEM"})));
+  auto fork3 = Fork(*session);
+
+  EXPECT_THAT(Prompt(*fork1, MakeInput("fork")),
+              ElementsAreArray(FormatResponses({"UforkEM"})));
+  EXPECT_THAT(Prompt(*fork2, MakeInput("fork")),
+              ElementsAreArray(FormatResponses({"UfooEM", "UforkEM"})));
+  auto fork4 = Fork(*fork2);
+  EXPECT_THAT(
+      Prompt(*fork3, MakeInput("fork")),
+      ElementsAreArray(FormatResponses({"UfooEM", "UbarEM", "UforkEM"})));
+  EXPECT_THAT(
+      Prompt(*session, MakeInput("baz")),
+      ElementsAreArray(FormatResponses({"UfooEM", "UbarEM", "UbazEM"})));
+
+  EXPECT_THAT(
+      Prompt(*fork4, MakeInput("more")),
+      ElementsAreArray(FormatResponses({"UfooEM", "UforkEM", "UmoreEM"})));
 }
 
-TEST_F(AILanguageModelTest, PromptSessionWithPromptApiRequests) {
-  RunPromptTest(AILanguageModelTest::Options{
-      .initial_prompts = GetTestInitialPrompts(),
-      .prompt_input = "Test prompt",
-      .expected_context = ("S: Test system prompt\n"
-                           "U: How are you?\n"
-                           "M: I'm fine, thank you, and you?\n"
-                           "U: I'm fine too.\n"),
-      .expected_cloned_context = ("S: Test system prompt\n"
-                                  "U: How are you?\n"
-                                  "M: I'm fine, thank you, and you?\n"
-                                  "U: I'm fine too.\n"
-                                  "U: Test prompt\n"
-                                  "M: Test response\n"),
-      .expected_prompt = "U: Test prompt\nM: ",
-  });
+TEST_F(AILanguageModelTest, SamplingParams) {
+  auto sampling_params = blink::mojom::AILanguageModelSamplingParams::New();
+  sampling_params->top_k = 2;
+  sampling_params->temperature = 1.0;
+
+  auto options = blink::mojom::AILanguageModelCreateOptions::New();
+  options->sampling_params = std::move(sampling_params);
+  auto session = CreateSession(std::move(options));
+  auto fork = Fork(*session);
+
+  EXPECT_THAT(Prompt(*session, MakeInput("foo")),
+              ElementsAre("Context: UfooEM\n", "TopK: 2, Temp: 1\n"));
+  EXPECT_THAT(Prompt(*fork, MakeInput("bar")),
+              ElementsAre("Context: UbarEM\n", "TopK: 2, Temp: 1\n"));
 }
 
-TEST_F(AILanguageModelTest, PromptSessionWithQuotaOverflow) {
-  RunPromptTest({.prompt_input = kTestPrompt,
-                 .expected_prompt = kExpectedFormattedTestPrompt,
-                 .should_overflow_context = true});
+TEST_F(AILanguageModelTest, SamplingParamsTopKOutOfRange) {
+  auto sampling_params = blink::mojom::AILanguageModelSamplingParams::New();
+  sampling_params->top_k = 0;
+  sampling_params->temperature = 1.5f;
+
+  auto options = blink::mojom::AILanguageModelCreateOptions::New();
+  options->sampling_params = std::move(sampling_params);
+  auto session = CreateSession(std::move(options));
+
+  EXPECT_THAT(Prompt(*session, MakeInput("foo")),
+              ElementsAre("Context: UfooEM\n", "TopK: 1, Temp: 1.5\n"));
 }
 
-TEST_F(AILanguageModelTest, PromptSessionWithUnsupportedLanguage) {
-  RunPromptTest({.should_use_supported_language = true});
+TEST_F(AILanguageModelTest, SamplingParamsTemperatureOutOfRange) {
+  auto sampling_params = blink::mojom::AILanguageModelSamplingParams::New();
+  sampling_params->top_k = 2;
+  sampling_params->temperature = -1.0f;
+
+  auto options = blink::mojom::AILanguageModelCreateOptions::New();
+  options->sampling_params = std::move(sampling_params);
+  auto session = CreateSession(std::move(options));
+
+  EXPECT_THAT(Prompt(*session, MakeInput("foo")),
+              ElementsAre("Context: UfooEM\n", "TopK: 2, Temp: 0\n"));
 }
 
-// Tests that sending `Prompt()` after destroying the session won't make a real
-// call to the model.
-TEST_F(AILanguageModelTest, PromptAfterDestroy) {
-  TestSessionDestroy(base::BindOnce(
-      [](mojo::Remote<blink::mojom::AILanguageModel> mock_session,
-         AITestUtils::MockModelStreamingResponder& mock_responder) {
-        mock_session->Destroy();
-        mock_session->Prompt(MakeInput(kTestPrompt),
-                             /*constraint=*/nullptr,
-                             mock_responder.BindNewPipeAndPassRemote());
-      }));
+TEST_F(AILanguageModelTest, MaxSamplingParams) {
+  auto sampling_params = blink::mojom::AILanguageModelSamplingParams::New();
+  sampling_params->top_k = kTestMaxTopK + 1;
+  sampling_params->temperature = kTestMaxTemperature + 1;
+
+  auto options = blink::mojom::AILanguageModelCreateOptions::New();
+  options->sampling_params = std::move(sampling_params);
+  auto session = CreateSession(std::move(options));
+
+  EXPECT_THAT(Prompt(*session, MakeInput("foo")),
+              ElementsAre("Context: UfooEM\n", "TopK: 5, Temp: 1.5\n"));
 }
 
-// Tests that sending `Prompt()` right before destroying the session won't make
-// a real call to the model.
-TEST_F(AILanguageModelTest, PromptBeforeDestroy) {
-  TestSessionDestroy(base::BindOnce(
-      [](mojo::Remote<blink::mojom::AILanguageModel> mock_session,
-         AITestUtils::MockModelStreamingResponder& mock_responder) {
-        mock_session->Prompt(MakeInput(kTestPrompt),
-                             /*constraint=*/nullptr,
-                             mock_responder.BindNewPipeAndPassRemote());
-        mock_session->Destroy();
-      }));
+TEST_F(AILanguageModelTest, InitialPrompts) {
+  auto options = blink::mojom::AILanguageModelCreateOptions::New();
+  options->initial_prompts.push_back(MakePrompt(Role::kSystem, "hi"));
+  options->initial_prompts.push_back(MakePrompt(Role::kUser, "bye"));
+  auto session = CreateSession(std::move(options));
+
+  EXPECT_THAT(Prompt(*session, MakeInput("foo")),
+              ElementsAre("Context: ShiEUbyeE\n", "Context: UfooEM\n"));
 }
 
-// Tests that the session will call `AddContext()` from the second prompt when
-// there is no context overflow.
-TEST_F(AILanguageModelTest, PromptWithHistoryWithoutQuotaOverflow) {
-  TestSessionAddContext(/*should_overflow_context=*/false);
-}
-
-// Tests that the session will not call `AddContext()` from the second prompt
-// when there is context overflow.
-TEST_F(AILanguageModelTest, PromptWithHistoryWithQuotaOverflow) {
-  TestSessionAddContext(/*should_overflow_context=*/true);
-}
-
-// TODO(crbug.com/414632884): This test is flaky on Linux TSAN.
-#if BUILDFLAG(IS_LINUX) && defined(THREAD_SANITIZER)
-#define MAYBE_CreateLanguageModel_WaitsForEligibility \
-  DISABLED_CreateLanguageModel_WaitsForEligibility
-#else
-#define MAYBE_CreateLanguageModel_WaitsForEligibility \
-  CreateLanguageModel_WaitsForEligibility
-#endif
-TEST_F(AILanguageModelTest, MAYBE_CreateLanguageModel_WaitsForEligibility) {
-  SetupMockOptimizationGuideKeyedService();
-  EXPECT_CALL(*mock_optimization_guide_keyed_service_, StartSession(_, _))
-      .WillOnce(
-          [&](optimization_guide::ModelBasedCapabilityKey feature,
-              const std::optional<optimization_guide::SessionConfigParams>&
-                  config_params) {
-            auto session = std::make_unique<
-                testing::NiceMock<optimization_guide::MockSession>>();
-            SetUpMockSession(*session);
-            return session;
-          });
-
-  base::test::TestFuture<base::OnceCallback<void(
-      optimization_guide::OnDeviceModelEligibilityReason)>>
-      eligibility_future;
-  EXPECT_CALL(*mock_optimization_guide_keyed_service_,
-              GetOnDeviceModelEligibilityAsync(_, _))
-      .WillOnce(testing::Invoke([&](auto feature, auto callback) {
-        eligibility_future.SetValue(std::move(callback));
-      }));
-
-  AITestUtils::MockCreateLanguageModelClient create_language_model_client;
-  base::test::TestFuture<mojo::PendingRemote<blink::mojom::AILanguageModel>>
-      session_future;
-  EXPECT_CALL(create_language_model_client, OnResult(_, _))
+TEST_F(AILanguageModelTest, InitialPromptsInstanceInfo) {
+  base::test::TestFuture<blink::mojom::AILanguageModelInstanceInfoPtr> future;
+  AITestUtils::MockCreateLanguageModelClient language_model_client;
+  EXPECT_CALL(language_model_client, OnResult(_, _))
       .WillOnce(
           [&](mojo::PendingRemote<blink::mojom::AILanguageModel> language_model,
               blink::mojom::AILanguageModelInstanceInfoPtr info) {
-            session_future.SetValue(std::move(language_model));
+            future.SetValue(std::move(info));
           });
 
+  auto options = blink::mojom::AILanguageModelCreateOptions::New();
+  options->initial_prompts.push_back(MakePrompt(Role::kSystem, "hi"));
   GetAIManagerRemote()->CreateLanguageModel(
-      create_language_model_client.BindNewPipeAndPassRemote(),
-      blink::mojom::AILanguageModelCreateOptions::New());
+      language_model_client.BindNewPipeAndPassRemote(), std::move(options));
+  auto info = future.Take();
+  EXPECT_EQ(info->input_quota, kTestMaxTokens);
+  EXPECT_EQ(info->input_usage, std::strlen("ShiE"));
+}
 
-  // Session should not be ready until eligibility callback has run.
-  EXPECT_FALSE(session_future.IsReady());
-  eligibility_future.Take().Run(
-      optimization_guide::OnDeviceModelEligibilityReason::kSuccess);
-  EXPECT_TRUE(session_future.Get());
+TEST_F(AILanguageModelTest, InitialPromptsTooLarge) {
+  base::test::TestFuture<blink::mojom::AIManagerCreateClientError> future;
+  AITestUtils::MockCreateLanguageModelClient language_model_client;
+  EXPECT_CALL(language_model_client, OnError(_)).WillOnce([&](auto error) {
+    future.SetValue(error);
+  });
+
+  auto options = blink::mojom::AILanguageModelCreateOptions::New();
+  options->initial_prompts.push_back(
+      MakePrompt(Role::kSystem, std::string(kTestMaxTokens + 1, 'a')));
+
+  GetAIManagerRemote()->CreateLanguageModel(
+      language_model_client.BindNewPipeAndPassRemote(), std::move(options));
+  EXPECT_EQ(future.Take(),
+            blink::mojom::AIManagerCreateClientError::kInitialInputTooLarge);
+}
+
+TEST_F(AILanguageModelTest, InputTooLarge) {
+  auto session = CreateSession();
+
+  TestStreamingResponder responder;
+  session->Prompt(MakeInput(std::string(kTestMaxTokens + 1, 'a')), nullptr,
+                  responder.BindRemote());
+  EXPECT_FALSE(responder.WaitForCompletion());
+  EXPECT_EQ(responder.error_status(),
+            blink::mojom::ModelStreamingResponseStatus::kErrorInputTooLarge);
+}
+
+TEST_F(AILanguageModelTest, QuotaOverflowOnPromptInput) {
+  // Set the execute result so the long prompt is not echoed back as the
+  // response.
+  fake_broker_.settings().set_execute_result({"hi"});
+  // Initial prompt should be kept on overflow.
+  auto options = blink::mojom::AILanguageModelCreateOptions::New();
+  options->initial_prompts.push_back(MakePrompt(Role::kSystem, "init"));
+  auto session = CreateSession(std::move(options));
+  // Set a prompt that is close to max token length. This string should be
+  // stripped from the prompt history, while the initial prompts and
+  // `long_prompt` will be kept.
+  EXPECT_THAT(
+      Prompt(*session, MakeInput(std::string(kTestMaxTokens - 20, 'a'))),
+      ElementsAre("hi"));
+  EXPECT_THAT(Prompt(*session, MakeInput("foo")), ElementsAre("hi"));
+
+  // Clear execute result so we can verify the input by checking the response.
+  fake_broker_.settings().set_execute_result({});
+  std::string long_prompt(kTestMaxTokens / 3, 'a');
+  TestStreamingResponder responder;
+  session->Prompt(MakeInput(long_prompt), nullptr, responder.BindRemote());
+  responder.WaitForQuotaOverflow();
+  EXPECT_TRUE(responder.WaitForCompletion());
+  // Response should include input/output of previous prompt with the original
+  // long prompt not present.
+  EXPECT_THAT(responder.responses(),
+              ElementsAre("Context: SinitE\n", "Context: UfooEMhiE\n",
+                          "Context: U" + long_prompt + "EM\n"));
+}
+
+TEST_F(AILanguageModelTest, QuotaOverflowOnAppend) {
+  // Initial prompt should be kept on overflow.
+  auto options = blink::mojom::AILanguageModelCreateOptions::New();
+  options->initial_prompts.push_back(MakePrompt(Role::kSystem, "init"));
+  auto session = CreateSession(std::move(options));
+  // Set a prompt that is close to max token length.
+  Append(*session, MakeInput(std::string(kTestMaxTokens - 20, 'a')));
+
+  std::string long_prompt(kTestMaxTokens / 3, 'a');
+  TestStreamingResponder responder;
+  session->Append(MakeInput(long_prompt), responder.BindAppendRemote());
+  responder.WaitForQuotaOverflow();
+  EXPECT_TRUE(responder.WaitForCompletion());
+
+  EXPECT_THAT(
+      Prompt(*session, MakeInput("foo")),
+      ElementsAre("Context: SinitE\n", "Context: U" + long_prompt + "E\n",
+                  "Context: UfooEM\n"));
+}
+
+TEST_F(AILanguageModelTest, QuotaOverflowOnOutput) {
+  // Set the execute result so the long prompt is not echoed back as the
+  // response.
+  fake_broker_.settings().set_execute_result({"hi"});
+  auto session = CreateSession();
+  // Set a prompt that is close to max token length. This string should be
+  // stripped from the prompt history, while the next prompt's input and output
+  // will be kept.
+  EXPECT_THAT(
+      Prompt(*session, MakeInput(std::string(kTestMaxTokens - 20, 'a'))),
+      ElementsAre("hi"));
+
+  // Reset result to a long response that should cause overflow. `long_response`
+  // should be kept, but the previous prompt will be removed.
+  std::string long_response(kTestMaxTokens / 3, 'a');
+  fake_broker_.settings().set_execute_result({long_response});
+
+  TestStreamingResponder responder;
+  session->Prompt(MakeInput("foo"), nullptr, responder.BindRemote());
+  responder.WaitForQuotaOverflow();
+  EXPECT_TRUE(responder.WaitForCompletion());
+  EXPECT_THAT(responder.responses(), ElementsAre(long_response));
+
+  // Verify the original long response was removed. The response should contain:
+  // - "foo"+long_response from the previous prompt call
+  // - "bar" from the current prompt call
+  fake_broker_.settings().set_execute_result({});
+  EXPECT_THAT(Prompt(*session, MakeInput("bar")),
+              ElementsAre("Context: UfooEM" + long_response + "E\n",
+                          "Context: UbarEM\n"));
+}
+
+TEST_F(AILanguageModelTest, Destroy) {
+  auto session = CreateSession();
+  base::RunLoop run_loop;
+  session.set_disconnect_handler(run_loop.QuitClosure());
+  EXPECT_THAT(Prompt(*session, MakeInput("foo")),
+              ElementsAreArray(FormatResponses({"UfooEM"})));
+  session->Destroy();
+  run_loop.Run();
+}
+
+TEST_F(AILanguageModelTest, UnsupportedLanguage) {
+  base::test::TestFuture<blink::mojom::AIManagerCreateClientError> future;
+  AITestUtils::MockCreateLanguageModelClient language_model_client;
+  EXPECT_CALL(language_model_client, OnError(_)).WillOnce([&](auto error) {
+    future.SetValue(error);
+  });
+
+  auto expected_input = blink::mojom::AILanguageModelExpectedInput::New();
+  expected_input->languages.emplace();
+  expected_input->languages->push_back(blink::mojom::AILanguageCode::New("ja"));
+
+  auto options = blink::mojom::AILanguageModelCreateOptions::New();
+  options->expected_inputs.emplace();
+  options->expected_inputs->push_back(std::move(expected_input));
+  GetAIManagerRemote()->CreateLanguageModel(
+      language_model_client.BindNewPipeAndPassRemote(), std::move(options));
+  EXPECT_EQ(future.Take(),
+            blink::mojom::AIManagerCreateClientError::kUnsupportedLanguage);
+}
+
+TEST_F(AILanguageModelTest, UnsupportedCapability) {
+  ON_CALL(*mock_optimization_guide_keyed_service_, GetOnDeviceCapabilities())
+      .WillByDefault(Return(on_device_model::Capabilities()));
+
+  base::test::TestFuture<blink::mojom::AIManagerCreateClientError> future;
+  AITestUtils::MockCreateLanguageModelClient language_model_client;
+  EXPECT_CALL(language_model_client, OnError(_)).WillOnce([&](auto error) {
+    future.SetValue(error);
+  });
+
+  auto expected_input = blink::mojom::AILanguageModelExpectedInput::New();
+  expected_input->type = blink::mojom::AILanguageModelPromptType::kImage;
+
+  auto options = blink::mojom::AILanguageModelCreateOptions::New();
+  options->expected_inputs.emplace();
+  options->expected_inputs->push_back(std::move(expected_input));
+  GetAIManagerRemote()->CreateLanguageModel(
+      language_model_client.BindNewPipeAndPassRemote(), std::move(options));
+  EXPECT_EQ(future.Take(),
+            blink::mojom::AIManagerCreateClientError::kUnableToCreateSession);
+}
+
+TEST_F(AILanguageModelTest, MultimodalInputImageNotSpecified) {
+  auto audio_input = blink::mojom::AILanguageModelExpectedInput::New();
+  audio_input->type = blink::mojom::AILanguageModelPromptType::kAudio;
+  auto options = blink::mojom::AILanguageModelCreateOptions::New();
+  options->expected_inputs.emplace();
+  options->expected_inputs->push_back(std::move(audio_input));
+  auto session = CreateSession(std::move(options));
+
+  auto make_input = [] {
+    std::vector<blink::mojom::AILanguageModelPromptPtr> input =
+        MakeInput("foo");
+    input.push_back(blink::mojom::AILanguageModelPrompt::New(
+        Role::kUser, blink::mojom::AILanguageModelPromptContent::NewBitmap(
+                         CreateTestBitmap(10, 10))));
+    return input;
+  };
+  {
+    TestStreamingResponder responder;
+    session->Prompt(make_input(), nullptr, responder.BindRemote());
+    EXPECT_FALSE(responder.WaitForCompletion());
+    EXPECT_EQ(responder.error_status(),
+              blink::mojom::ModelStreamingResponseStatus::kErrorInvalidRequest);
+  }
+  {
+    TestStreamingResponder responder;
+    session->Append(make_input(), responder.BindAppendRemote());
+    EXPECT_FALSE(responder.WaitForCompletion());
+    EXPECT_EQ(responder.error_status(),
+              blink::mojom::ModelStreamingResponseStatus::kErrorInvalidRequest);
+  }
+  TestMeasureInputUsageClient client;
+  session->MeasureInputUsage(make_input(), client.BindRemote());
+  EXPECT_EQ(client.Wait(), 0u);
+}
+
+TEST_F(AILanguageModelTest, MultimodalInputAudioNotSpecified) {
+  auto image_input = blink::mojom::AILanguageModelExpectedInput::New();
+  image_input->type = blink::mojom::AILanguageModelPromptType::kImage;
+  auto options = blink::mojom::AILanguageModelCreateOptions::New();
+  options->expected_inputs.emplace();
+  options->expected_inputs->push_back(std::move(image_input));
+  auto session = CreateSession(std::move(options));
+
+  auto make_input = [] {
+    std::vector<blink::mojom::AILanguageModelPromptPtr> input =
+        MakeInput("foo");
+    input.push_back(blink::mojom::AILanguageModelPrompt::New(
+        Role::kUser, blink::mojom::AILanguageModelPromptContent::NewAudio(
+                         CreateTestAudio())));
+    return input;
+  };
+  {
+    TestStreamingResponder responder;
+    session->Prompt(make_input(), nullptr, responder.BindRemote());
+    EXPECT_FALSE(responder.WaitForCompletion());
+    EXPECT_EQ(responder.error_status(),
+              blink::mojom::ModelStreamingResponseStatus::kErrorInvalidRequest);
+  }
+  {
+    TestStreamingResponder responder;
+    session->Append(make_input(), responder.BindAppendRemote());
+    EXPECT_FALSE(responder.WaitForCompletion());
+    EXPECT_EQ(responder.error_status(),
+              blink::mojom::ModelStreamingResponseStatus::kErrorInvalidRequest);
+  }
+  TestMeasureInputUsageClient client;
+  session->MeasureInputUsage(make_input(), client.BindRemote());
+  EXPECT_EQ(client.Wait(), 0u);
+}
+
+TEST_F(AILanguageModelTest, MultimodalInput) {
+  auto audio_input = blink::mojom::AILanguageModelExpectedInput::New();
+  audio_input->type = blink::mojom::AILanguageModelPromptType::kAudio;
+  auto image_input = blink::mojom::AILanguageModelExpectedInput::New();
+  image_input->type = blink::mojom::AILanguageModelPromptType::kImage;
+
+  auto options = blink::mojom::AILanguageModelCreateOptions::New();
+  options->expected_inputs.emplace();
+  options->expected_inputs->push_back(std::move(audio_input));
+  options->expected_inputs->push_back(std::move(image_input));
+  auto session = CreateSession(std::move(options));
+
+  std::vector<blink::mojom::AILanguageModelPromptPtr> input = MakeInput("foo");
+  input.push_back(blink::mojom::AILanguageModelPrompt::New(
+      Role::kUser, blink::mojom::AILanguageModelPromptContent::NewBitmap(
+                       CreateTestBitmap(10, 10))));
+  input.push_back(blink::mojom::AILanguageModelPrompt::New(
+      Role::kUser,
+      blink::mojom::AILanguageModelPromptContent::NewAudio(CreateTestAudio())));
+  EXPECT_THAT(Prompt(*session, std::move(input)),
+              ElementsAreArray(FormatResponses({"UfooEU<image>EU<audio>EM"})));
+}
+
+TEST_F(AILanguageModelTest, ModelDownload) {
+  EXPECT_EQ(GetAIManagerDownloadProgressObserversSize(), 0u);
+  AITestUtils::FakeMonitor mock_monitor;
+
+  EXPECT_CALL(component_update_service_, GetComponentIDs()).Times(1);
+  GetAIManagerRemote()->AddModelDownloadProgressObserver(
+      mock_monitor.BindNewPipeAndPassRemote());
+
+  ASSERT_TRUE(base::test::RunUntil(
+      [this] { return GetAIManagerDownloadProgressObserversSize() == 1u; }));
+
+  // This is the component id of the on device model. The `AIManager` sends
+  // updates for it to the `CreateMonitor`s.
+  std::string model_component_id =
+      component_updater::OptimizationGuideOnDeviceModelInstallerPolicy::
+          GetOnDeviceModelExtensionId();
+  AITestUtils::FakeComponent model_component(model_component_id,
+                                             kTestModelDownloadSize);
+
+  component_update_service_.SendUpdate(model_component.CreateUpdateItem(
+      update_client::ComponentState::kDownloading, kTestModelDownloadSize));
+
+  mock_monitor.ExpectReceivedNormalizedUpdate(0, kTestModelDownloadSize);
+  mock_monitor.ExpectReceivedNormalizedUpdate(kTestModelDownloadSize,
+                                              kTestModelDownloadSize);
+}
+
+TEST_F(AILanguageModelTest, MeasureInputUsage) {
+  auto session = CreateSession();
+  TestMeasureInputUsageClient client;
+  session->MeasureInputUsage(MakeInput("foo"), client.BindRemote());
+  EXPECT_EQ(client.Wait(), std::string("UfooEM").size());
+}
+
+TEST_F(AILanguageModelTest, TextSafetyInput) {
+  auto config = CreateConfig();
+  config.set_can_skip_text_safety(false);
+  fake_broker_.UpdateModelAdaptation(
+      optimization_guide::FakeAdaptationAsset({.config = config}));
+  auto safety_config = CreateSafetyConfig();
+  auto* check = safety_config.add_request_check();
+  check->mutable_input_template()->Add(
+      FieldSubstitution("%s", StringValueField()));
+  optimization_guide::FakeSafetyModelAsset safety_asset(
+      std::move(safety_config));
+  fake_broker_.UpdateSafetyModel(safety_asset.model_info());
+
+  fake_broker_.settings().set_execute_result({"hi"});
+  auto session = CreateSession();
+  EXPECT_THAT(Prompt(*session, MakeInput("safe")), ElementsAre("hi"));
+
+  // Fake text safety checker looks for the string "unsafe".
+  TestStreamingResponder responder;
+  session->Prompt(MakeInput("unsafe"), nullptr, responder.BindRemote());
+  EXPECT_FALSE(responder.WaitForCompletion());
+  EXPECT_EQ(responder.error_status(),
+            blink::mojom::ModelStreamingResponseStatus::kErrorFiltered);
+}
+
+TEST_F(AILanguageModelTest, TextSafetyOutput) {
+  auto config = CreateConfig();
+  config.set_can_skip_text_safety(false);
+  fake_broker_.UpdateModelAdaptation(
+      optimization_guide::FakeAdaptationAsset({.config = config}));
+  auto safety_config = CreateSafetyConfig();
+  auto* check = safety_config.mutable_raw_output_check();
+  check->mutable_input_template()->Add(
+      FieldSubstitution("%s", StringValueField()));
+  safety_config.mutable_partial_output_checks()->set_minimum_tokens(1000);
+  optimization_guide::FakeSafetyModelAsset safety_asset(
+      std::move(safety_config));
+  fake_broker_.UpdateSafetyModel(safety_asset.model_info());
+
+  // Fake text safety checker looks for the string "unsafe".
+  fake_broker_.settings().set_execute_result(
+      {"a", "b", "c", "d", "e", "f", "g", "unsafe", "h"});
+  auto session = CreateSession();
+  TestStreamingResponder responder;
+  session->Prompt(MakeInput("foo"), nullptr, responder.BindRemote());
+  EXPECT_FALSE(responder.WaitForCompletion());
+  EXPECT_EQ(responder.error_status(),
+            blink::mojom::ModelStreamingResponseStatus::kErrorFiltered);
+  EXPECT_TRUE(responder.responses().empty());
+}
+
+TEST_F(AILanguageModelTest, TextSafetyOutputPartial) {
+  auto config = CreateConfig();
+  config.set_can_skip_text_safety(false);
+  fake_broker_.UpdateModelAdaptation(
+      optimization_guide::FakeAdaptationAsset({.config = config}));
+  auto safety_config = CreateSafetyConfig();
+  auto* check = safety_config.mutable_raw_output_check();
+  check->mutable_input_template()->Add(
+      FieldSubstitution("%s", StringValueField()));
+  safety_config.mutable_partial_output_checks()->set_minimum_tokens(3);
+  safety_config.mutable_partial_output_checks()->set_token_interval(2);
+  optimization_guide::FakeSafetyModelAsset safety_asset(
+      std::move(safety_config));
+  fake_broker_.UpdateSafetyModel(safety_asset.model_info());
+
+  // Fake text safety checker looks for the string "unsafe".
+  fake_broker_.settings().set_execute_result(
+      {"a", "b", "c", "d", "e", "f", "g", "unsafe", "h"});
+  auto session = CreateSession();
+  TestStreamingResponder responder;
+  session->Prompt(MakeInput("foo"), nullptr, responder.BindRemote());
+  EXPECT_FALSE(responder.WaitForCompletion());
+  EXPECT_EQ(responder.error_status(),
+            blink::mojom::ModelStreamingResponseStatus::kErrorFiltered);
+  // Partial checks should still allow some output to stream.
+  EXPECT_THAT(responder.responses(), ElementsAre("abc", "de", "fg"));
+}
+
+TEST_F(AILanguageModelTest, QueuesOperations) {
+  base::test::TestFuture<mojo::Remote<blink::mojom::AILanguageModel>>
+      fork_future;
+  AITestUtils::MockCreateLanguageModelClient fork_client;
+  EXPECT_CALL(fork_client, OnResult(_, _))
+      .WillOnce([&](auto language_model, auto info) {
+        fork_future.SetValue(mojo::Remote<blink::mojom::AILanguageModel>(
+            std::move(language_model)));
+      });
+
+  auto session = CreateSession();
+  TestStreamingResponder responder1;
+  TestStreamingResponder responder2;
+  TestStreamingResponder responder3;
+  // Add three prompts and a fork, all these operations should complete
+  // successfully and in order.
+  session->Prompt(MakeInput("foo"), nullptr, responder1.BindRemote());
+  session->Prompt(MakeInput("bar"), nullptr, responder2.BindRemote());
+  session->Fork(fork_client.BindNewPipeAndPassRemote());
+  session->Prompt(MakeInput("baz"), nullptr, responder3.BindRemote());
+
+  EXPECT_TRUE(responder1.WaitForCompletion());
+  EXPECT_THAT(responder1.responses(),
+              ElementsAreArray(FormatResponses({"UfooEM"})));
+
+  EXPECT_TRUE(responder2.WaitForCompletion());
+  EXPECT_THAT(responder2.responses(),
+              ElementsAreArray(FormatResponses({"UfooEM", "UbarEM"})));
+
+  EXPECT_TRUE(responder3.WaitForCompletion());
+  EXPECT_THAT(
+      responder3.responses(),
+      ElementsAreArray(FormatResponses({"UfooEM", "UbarEM", "UbazEM"})));
+
+  EXPECT_THAT(
+      Prompt(*fork_future.Take(), MakeInput("fork")),
+      ElementsAreArray(FormatResponses({"UfooEM", "UbarEM", "UforkEM"})));
+}
+
+TEST_F(AILanguageModelTest, Constraint) {
+  auto session = CreateSession();
+  EXPECT_THAT(
+      Prompt(*session, MakeInput("foo"),
+             on_device_model::mojom::ResponseConstraint::NewRegex("reg")),
+      ElementsAre("Constraint: regex reg\n", "Context: UfooEM\n"));
+}
+
+TEST_F(AILanguageModelTest, ServiceCrash) {
+  auto session = CreateSession();
+  TestStreamingResponder responder;
+  session->Prompt(MakeInput("bar"), nullptr, responder.BindRemote());
+  fake_broker_.CrashService();
+  EXPECT_FALSE(responder.WaitForCompletion());
+  EXPECT_EQ(responder.error_status(),
+            blink::mojom::ModelStreamingResponseStatus::kErrorGenericFailure);
+
+  // Recreating the session should be fine.
+  session = CreateSession();
+  EXPECT_THAT(Prompt(*session, MakeInput("foo")),
+              ElementsAreArray(FormatResponses({"UfooEM"})));
 }
 
 // TODO(crbug.com/414632884): This test is flaky on Linux TSAN.
@@ -935,11 +1004,10 @@
 #define MAYBE_CanCreate_WaitsForEligibility CanCreate_WaitsForEligibility
 #endif
 TEST_F(AILanguageModelTest, MAYBE_CanCreate_WaitsForEligibility) {
-  SetupMockOptimizationGuideKeyedService();
   EXPECT_CALL(*mock_optimization_guide_keyed_service_,
               GetOnDeviceModelEligibility(_))
-      .WillRepeatedly(testing::Return(
-          optimization_guide::OnDeviceModelEligibilityReason::kSuccess));
+      .WillRepeatedly(
+          Return(optimization_guide::OnDeviceModelEligibilityReason::kSuccess));
   base::test::TestFuture<base::OnceCallback<void(
       optimization_guide::OnDeviceModelEligibilityReason)>>
       eligibility_future;
@@ -962,11 +1030,10 @@
 }
 
 TEST_F(AILanguageModelTest, CanCreate_IsLanguagesSupported) {
-  SetupMockOptimizationGuideKeyedService();
   EXPECT_CALL(*mock_optimization_guide_keyed_service_,
               GetOnDeviceModelEligibility(_))
-      .WillRepeatedly(testing::Return(
-          optimization_guide::OnDeviceModelEligibilityReason::kSuccess));
+      .WillRepeatedly(
+          Return(optimization_guide::OnDeviceModelEligibilityReason::kSuccess));
 
   base::MockCallback<AIManager::CanCreateLanguageModelCallback> callback;
   auto options = blink::mojom::AILanguageModelCreateOptions::New();
@@ -983,11 +1050,10 @@
 }
 
 TEST_F(AILanguageModelTest, CanCreate_UnIsLanguagesSupported) {
-  SetupMockOptimizationGuideKeyedService();
   EXPECT_CALL(*mock_optimization_guide_keyed_service_,
               GetOnDeviceModelEligibility(_))
-      .WillRepeatedly(testing::Return(
-          optimization_guide::OnDeviceModelEligibilityReason::kSuccess));
+      .WillRepeatedly(
+          Return(optimization_guide::OnDeviceModelEligibilityReason::kSuccess));
 
   base::MockCallback<AIManager::CanCreateLanguageModelCallback> callback;
   EXPECT_CALL(callback, Run(blink::mojom::ModelAvailabilityCheckResult::
@@ -1003,317 +1069,86 @@
                                                   callback.Get());
 }
 
-// Test Prompt() with image and audio input.
-TEST_F(AILanguageModelTest, MultimodalInput) {
-  SetupMockOptimizationGuideKeyedService();
-  EXPECT_CALL(*mock_optimization_guide_keyed_service_, StartSession(_, _))
-      .WillOnce([&](optimization_guide::ModelBasedCapabilityKey feature,
-                    const std::optional<
-                        optimization_guide::SessionConfigParams>&
-                        config_params) {
-        auto session = std::make_unique<
-            testing::NiceMock<optimization_guide::MockSession>>();
-        SetUpMockSession(*session);
-        EXPECT_CALL(*session, GetCapabilities())
-            .WillRepeatedly(Return(on_device_model::Capabilities{
-                on_device_model::CapabilityFlags::kImageInput,
-                on_device_model::CapabilityFlags::kAudioInput}));
-        EXPECT_CALL(*session, SetInput(_, _))
-            .WillOnce([&](MultimodalMessage request_metadata,
-                          SetInputCallback callback) {
-              EXPECT_THAT(ToString(request_metadata),
-                          "U: Test prompt\n"
-                          "U: <image>\n"
-                          "U: <audio>\n"
-                          "M: ");
-            });
-        EXPECT_CALL(*session, ExecuteModelWithResponseConstraint(_, _, _))
-            .WillOnce(
-                [&](const google::protobuf::MessageLite& request_metadata,
-                    on_device_model::mojom::ResponseConstraintPtr constraint,
-                    optimization_guide::
-                        OptimizationGuideModelExecutionResultStreamingCallback
-                            callback) {
-                  EXPECT_THAT(request_metadata.ByteSizeLong(), 0);
-                  callback.Run(
-                      CreateExecutionResult("OK", /*is_complete=*/true,
-                                            /*input_token_count=*/1u,
-                                            /*output_token_count=*/1u));
-                });
-        return session;
-      });
-  mojo::Remote<blink::mojom::AILanguageModel> mock_session =
-      CreateMockSession();
-
-  AITestUtils::MockModelStreamingResponder mock_responder;
-  base::RunLoop run_loop;
-  EXPECT_CALL(mock_responder, OnStreaming("OK")).Times(1);
-  EXPECT_CALL(mock_responder, OnCompletion(_))
-      .WillOnce(testing::InvokeWithoutArgs(&run_loop, &base::RunLoop::Quit));
-
-  std::vector<blink::mojom::AILanguageModelPromptPtr> input =
-      MakeInput(kTestPrompt);
-  input.push_back(blink::mojom::AILanguageModelPrompt::New(
-      Role::kUser, blink::mojom::AILanguageModelPromptContent::NewBitmap(
-                       CreateTestBitmap(10, 10))));
-  input.push_back(blink::mojom::AILanguageModelPrompt::New(
-      Role::kUser,
-      blink::mojom::AILanguageModelPromptContent::NewAudio(CreateTestAudio())));
-  mock_session->Prompt(std::move(input), /*constraint=*/nullptr,
-                       mock_responder.BindNewPipeAndPassRemote());
-  run_loop.Run();
-}
-
-TEST_F(AILanguageModelTest, Append) {
-  SetupMockOptimizationGuideKeyedService();
-  EXPECT_CALL(*mock_optimization_guide_keyed_service_, StartSession(_, _))
-      .WillOnce([&](optimization_guide::ModelBasedCapabilityKey feature,
-                    const std::optional<
-                        optimization_guide::SessionConfigParams>&
-                        config_params) {
-        auto session = std::make_unique<
-            testing::NiceMock<optimization_guide::MockSession>>();
-
-        SetUpMockSession(*session);
-
-        ON_CALL(*session, GetContextSizeInTokens(_, _))
-            .WillByDefault(
-                [&](MultimodalMessageReadView request_metadata,
-                    optimization_guide::
-                        OptimizationGuideModelSizeInTokenCallback callback) {
-                  std::move(callback).Run(1);
-                });
-        ON_CALL(*session, SetInput(_, _))
-            .WillByDefault(
-                [&, is_append = true](MultimodalMessage request_metadata,
-                                      SetInputCallback callback) mutable {
-                  if (is_append) {
-                    is_append = false;
-                    EXPECT_THAT(ToString(request_metadata),
-                                kExpectedFormattedAppendPrompt);
-                    std::move(callback).Run(1ul);
-                  } else {
-                    EXPECT_THAT(ToString(request_metadata),
-                                base::StrCat({kExpectedFormattedAppendPrompt,
-                                              kExpectedFormattedTestPrompt}));
-                  }
-                });
-
-        EXPECT_CALL(*session, ExecuteModelWithResponseConstraint(_, _, _))
-            .WillOnce(
-                [&](const google::protobuf::MessageLite& request_metadata,
-                    on_device_model::mojom::ResponseConstraintPtr constraint,
-                    optimization_guide::
-                        OptimizationGuideModelExecutionResultStreamingCallback
-                            callback) {
-                  EXPECT_THAT(request_metadata.ByteSizeLong(), 0);
-                  callback.Run(
-                      CreateExecutionResult("OK", /*is_complete=*/true,
-                                            /*input_token_count=*/1u,
-                                            /*output_token_count=*/1u));
-                });
-        return session;
-      });
-
-  base::test::TestFuture<mojo::PendingRemote<blink::mojom::AILanguageModel>>
-      session_future;
-  AITestUtils::MockCreateLanguageModelClient mock_create_language_model_client;
-
-  EXPECT_CALL(mock_create_language_model_client, OnResult(_, _))
-      .WillOnce(
-          [&](mojo::PendingRemote<blink::mojom::AILanguageModel> language_model,
-              blink::mojom::AILanguageModelInstanceInfoPtr info) {
-            EXPECT_TRUE(language_model);
-            EXPECT_EQ(info->input_quota,
-                      AITestUtils::GetFakeTokenLimits().max_context_tokens);
-            EXPECT_EQ(info->input_usage, 0ul);
-            session_future.SetValue(
-                mojo::PendingRemote<blink::mojom::AILanguageModel>(
-                    std::move(language_model)));
-          });
-
-  mojo::Remote<blink::mojom::AIManager> mock_remote = GetAIManagerRemote();
-
-  mock_remote->CreateLanguageModel(
-      mock_create_language_model_client.BindNewPipeAndPassRemote(),
-      blink::mojom::AILanguageModelCreateOptions::New(
-          /*sampling_params=*/nullptr,
-          /*initial_prompts=*/
-          std::vector<blink::mojom::AILanguageModelPromptPtr>(),
-          /*expected_inputs=*/std::nullopt));
-
-  auto mock_responder = std::make_unique<
-      testing::NiceMock<AITestUtils::MockModelStreamingResponder>>();
-  base::RunLoop responder_run_loop;
-  EXPECT_CALL(*mock_responder, OnCompletion(_))
-      .WillOnce(testing::Invoke(
-          [&](blink::mojom::ModelExecutionContextInfoPtr context_info) {
-            responder_run_loop.Quit();
-          }));
-  auto mock_append_client = std::make_unique<
-      testing::NiceMock<AITestUtils::MockLanguageModelAppendClient>>();
-  base::RunLoop append_run_loop;
-  EXPECT_CALL(*mock_append_client, OnAppendComplete()).WillOnce([&]() {
-    append_run_loop.Quit();
-  });
-  mojo::Remote<blink::mojom::AILanguageModel> mock_session(
-      session_future.Take());
-  mock_session->Append(MakeInput(kTestAppendPrompt),
-                       mock_append_client->BindNewPipeAndPassRemote());
-  append_run_loop.Run();
-  mock_session->Prompt(MakeInput(kTestPrompt),
-                       /*constraint=*/nullptr,
-                       mock_responder->BindNewPipeAndPassRemote());
-  responder_run_loop.Run();
-}
-
-// Tests `AILanguageModel::Context` creation without initial prompts.
-TEST(AILanguageModelContextCreationTest, CreateContext_WithoutInitialPrompts) {
-  AILanguageModel::Context context(kTestMaxContextToken, {});
-  EXPECT_FALSE(context.HasContextItem());
-}
-
-// Tests `AILanguageModel::Context` creation with valid initial prompts.
-TEST(AILanguageModelContextCreationTest,
-     CreateContext_WithInitialPrompts_Normal) {
-  AILanguageModel::Context context(
-      kTestMaxContextToken,
-      SimpleContextItem("initial prompts\n", kTestInitialPromptsToken));
-  EXPECT_TRUE(context.HasContextItem());
-}
-
-// Tests `AILanguageModel::Context` creation with initial prompts that exceeds
-// the max token limit.
-TEST(AILanguageModelContextCreationTest,
-     CreateContext_WithInitialPrompts_Overflow) {
-  EXPECT_DEATH_IF_SUPPORTED(
-      AILanguageModel::Context context(
-          kTestMaxContextToken, SimpleContextItem("long initial prompts\n",
-                                                  kTestMaxContextToken + 1u)),
-      "");
-}
-
 // Tests the `AILanguageModel::Context` that's initialized with/without any
 // initial prompt.
-class AILanguageModelContextTest : public testing::Test,
-                                   public testing::WithParamInterface<
-                                       /*is_init_with_initial_prompts=*/bool> {
+class AILanguageModelContextTest : public testing::Test {
  public:
-  bool IsInitializedWithInitialPrompts() { return GetParam(); }
-
-  uint32_t GetMaxContextToken() {
-    return IsInitializedWithInitialPrompts()
-               ? kTestMaxContextToken - kTestInitialPromptsToken
-               : kTestMaxContextToken;
-  }
-
-  std::string GetInitialPromptsPrefix() {
-    return IsInitializedWithInitialPrompts() ? "S: initial prompts\n" : "";
-  }
-
-  AILanguageModel::Context context_{
-      kTestMaxContextToken,
-      IsInitializedWithInitialPrompts()
-          ? SimpleContextItem("initial prompts", kTestInitialPromptsToken)
-          : AILanguageModel::Context::ContextItem()};
+  AILanguageModel::Context context_{kTestMaxContextToken};
 };
 
-INSTANTIATE_TEST_SUITE_P(All,
-                         AILanguageModelContextTest,
-                         testing::Bool(),
-                         [](const testing::TestParamInfo<bool>& info) {
-                           return info.param ? "WithInitialPrompts"
-                                             : "WithoutInitialPrompts";
-                         });
-
-// Tests `GetContextString()` and `HasContextItem()` when the context is empty.
-TEST_P(AILanguageModelContextTest, TestContextOperation_Empty) {
-  EXPECT_EQ(GetContextString(context_), GetInitialPromptsPrefix());
-
-  if (IsInitializedWithInitialPrompts()) {
-    EXPECT_TRUE(context_.HasContextItem());
-  } else {
-    EXPECT_FALSE(context_.HasContextItem());
-  }
+// Tests `GetContextString()` when the context is empty.
+TEST_F(AILanguageModelContextTest, TestContextOperation_Empty) {
+  EXPECT_EQ(GetContextString(context_), "");
 }
 
-// Tests `GetContextString()` and `HasContextItem()` when some items are added
-// to the context.
-TEST_P(AILanguageModelContextTest, TestContextOperation_NonEmpty) {
+// Tests `GetContextString()` when some items are added to the context.
+TEST_F(AILanguageModelContextTest, TestContextOperation_NonEmpty) {
   EXPECT_EQ(context_.AddContextItem(SimpleContextItem("test", 1u)),
             AILanguageModel::Context::SpaceReservationResult::kSufficientSpace);
-  EXPECT_EQ(GetContextString(context_),
-            GetInitialPromptsPrefix() + "S: test\n");
-  EXPECT_TRUE(context_.HasContextItem());
+  EXPECT_EQ(GetContextString(context_), "S: test");
 
   context_.AddContextItem(SimpleContextItem(" test again", 2u));
-  EXPECT_EQ(GetContextString(context_),
-            GetInitialPromptsPrefix() + "S: test\nS:  test again\n");
-  EXPECT_TRUE(context_.HasContextItem());
+  EXPECT_EQ(GetContextString(context_), "S: testS:  test again");
 }
 
-// Tests `GetContextString()` and `HasContextItem()` when the items overflow.
-TEST_P(AILanguageModelContextTest, TestContextOperation_Overflow) {
+// Tests `GetContextString()` when the items overflow.
+TEST_F(AILanguageModelContextTest, TestContextOperation_Overflow) {
   EXPECT_EQ(context_.AddContextItem(SimpleContextItem("test", 1u)),
             AILanguageModel::Context::SpaceReservationResult::kSufficientSpace);
-  EXPECT_EQ(GetContextString(context_),
-            GetInitialPromptsPrefix() + "S: test\n");
-  EXPECT_TRUE(context_.HasContextItem());
+  EXPECT_EQ(GetContextString(context_), "S: test");
 
   // Since the total number of tokens will exceed `kTestMaxContextToken`, the
   // old item will be evicted.
   EXPECT_EQ(
       context_.AddContextItem(
-          SimpleContextItem("test long token", GetMaxContextToken())),
+          SimpleContextItem("long token", kTestMaxContextToken)),
       AILanguageModel::Context::SpaceReservationResult::kSpaceMadeAvailable);
-  EXPECT_EQ(GetContextString(context_),
-            GetInitialPromptsPrefix() + "S: test long token\n");
-  EXPECT_TRUE(context_.HasContextItem());
+  EXPECT_EQ(GetContextString(context_), "S: long token");
 }
 
-// Tests `GetContextString()` and `HasContextItem()` when the items overflow on
-// the first insertion.
-TEST_P(AILanguageModelContextTest, TestContextOperation_OverflowOnFirstItem) {
+TEST_F(AILanguageModelContextTest, TestContextOperation_PartialOverflow) {
+  EXPECT_EQ(context_.AddContextItem(SimpleContextItem("foo", 1u)),
+            AILanguageModel::Context::SpaceReservationResult::kSufficientSpace);
+  EXPECT_EQ(GetContextString(context_), "S: foo");
+
+  EXPECT_EQ(context_.AddContextItem(SimpleContextItem("bar", 1u)),
+            AILanguageModel::Context::SpaceReservationResult::kSufficientSpace);
+  EXPECT_EQ(GetContextString(context_), "S: fooS: bar");
+
+  // Add 1 token less than `kTestMaxContextToken` so one of the previous items
+  // will be kept.
   EXPECT_EQ(
       context_.AddContextItem(
-          SimpleContextItem("test very long token", GetMaxContextToken() + 1u)),
+          SimpleContextItem("long token", kTestMaxContextToken - 1)),
+      AILanguageModel::Context::SpaceReservationResult::kSpaceMadeAvailable);
+  EXPECT_EQ(GetContextString(context_), "S: barS: long token");
+}
+
+// Tests `GetContextString()` when the items overflow on the first insertion.
+TEST_F(AILanguageModelContextTest, TestContextOperation_OverflowOnFirstItem) {
+  EXPECT_EQ(
+      context_.AddContextItem(
+          SimpleContextItem("test very long token", kTestMaxContextToken + 1u)),
       AILanguageModel::Context::SpaceReservationResult::kInsufficientSpace);
-  EXPECT_EQ(GetContextString(context_), GetInitialPromptsPrefix());
-  if (IsInitializedWithInitialPrompts()) {
-    EXPECT_TRUE(context_.HasContextItem());
-  } else {
-    EXPECT_FALSE(context_.HasContextItem());
-  }
+  EXPECT_EQ(GetContextString(context_), "");
 }
 
 TEST_F(AILanguageModelTest, Priority) {
-  SetupMockOptimizationGuideKeyedService();
-  base::test::TestFuture<testing::NiceMock<optimization_guide::MockSession>*>
-      session_future;
-  EXPECT_CALL(*mock_optimization_guide_keyed_service_, StartSession(_, _))
-      .WillOnce(
-          [&](optimization_guide::ModelBasedCapabilityKey feature,
-              const std::optional<optimization_guide::SessionConfigParams>&
-                  config_params) {
-            auto session = std::make_unique<
-                testing::NiceMock<optimization_guide::MockSession>>();
-            SetUpMockSession(*session);
-            EXPECT_CALL(
-                *session,
-                SetPriority(on_device_model::mojom::Priority::kForeground));
-            session_future.SetValue(session.get());
-            return session;
-          });
-  auto session_remote = CreateMockSession();
-  auto* session = session_future.Get();
+  fake_broker_.settings().set_execute_result({"hi"});
+  auto session = CreateSession();
 
-  EXPECT_CALL(*session,
-              SetPriority(on_device_model::mojom::Priority::kBackground));
+  EXPECT_THAT(Prompt(*session, MakeInput("foo")), ElementsAre("hi"));
+
   main_rfh()->GetRenderWidgetHost()->GetView()->Hide();
+  EXPECT_THAT(Prompt(*session, MakeInput("bar")),
+              ElementsAre("Priority: background\n", "hi"));
 
-  EXPECT_CALL(*session,
-              SetPriority(on_device_model::mojom::Priority::kForeground));
+  auto fork = Fork(*session);
+  EXPECT_THAT(Prompt(*fork, MakeInput("bar")),
+              ElementsAre("Priority: background\n", "hi"));
+
   main_rfh()->GetRenderWidgetHost()->GetView()->Show();
+  EXPECT_THAT(Prompt(*session, MakeInput("baz")), ElementsAre("hi"));
 }
 
 }  // namespace
diff --git a/chrome/browser/ai/ai_manager.cc b/chrome/browser/ai/ai_manager.cc
index 35efcaa..f23e38f 100644
--- a/chrome/browser/ai/ai_manager.cc
+++ b/chrome/browser/ai/ai_manager.cc
@@ -65,6 +65,8 @@
 namespace {
 
 constexpr float kDefaultMaxTemperature = 2.0f;
+constexpr uint32_t kMinTopK = 1;
+constexpr float kMinTemperature = 0.0f;
 
 // Checks if the model path configured via command line is valid.
 bool IsModelPathValid(const std::string& model_path_str) {
@@ -316,6 +318,11 @@
   if (rfh && rfh->GetRenderWidgetHost()) {
     widget_observer_.Observe(rfh->GetRenderWidgetHost());
   }
+  auto* service = OptimizationGuideKeyedServiceFactory::GetForProfile(
+      Profile::FromBrowserContext(browser_context_));
+  if (service) {
+    model_broker_client_ = service->CreateModelBrokerClient();
+  }
 }
 
 AIManager::~AIManager() = default;
@@ -379,64 +386,6 @@
                    capabilities, std::move(callback));
 }
 
-std::unique_ptr<CreateLanguageModelOnDeviceSessionTask>
-AIManager::CreateLanguageModelInternal(
-    blink::mojom::AILanguageModelSamplingParamsPtr sampling_params,
-    on_device_model::Capabilities capabilities,
-    AIContextBoundObjectSet& context_bound_object_set,
-    base::OnceCallback<void(AILanguageModelOrCreationError)> callback,
-    const std::optional<const AILanguageModel::Context>& context) {
-  blink::mojom::AILanguageModelParamsPtr language_model_params =
-      GetLanguageModelParams();
-
-  optimization_guide::SamplingParams resolved_sampling_params;
-  if (sampling_params) {
-    resolved_sampling_params = optimization_guide::SamplingParams{
-        .top_k = std::min(sampling_params->top_k,
-                          language_model_params->max_sampling_params->top_k),
-        .temperature =
-            std::min(sampling_params->temperature,
-                     language_model_params->max_sampling_params->temperature)};
-  } else {
-    resolved_sampling_params = optimization_guide::SamplingParams{
-        .top_k = language_model_params->default_sampling_params->top_k,
-        .temperature =
-            language_model_params->default_sampling_params->temperature};
-  }
-
-  auto task = std::make_unique<CreateLanguageModelOnDeviceSessionTask>(
-      *this, context_bound_object_set, browser_context_,
-      std::move(resolved_sampling_params), capabilities,
-      base::BindOnce(
-          [](base::WeakPtr<content::BrowserContext> browser_context,
-             AIContextBoundObjectSet& context_bound_object_set,
-             const std::optional<const AILanguageModel::Context>& context,
-             AIManager& ai_manager,
-             base::OnceCallback<void(
-                 base::expected<std::unique_ptr<AILanguageModel>,
-                                blink::mojom::AIManagerCreateClientError>)>
-                 callback,
-             std::unique_ptr<
-                 optimization_guide::OptimizationGuideModelExecutor::Session>
-                 session) {
-            if (!session) {
-              std::move(callback).Run(
-                  base::unexpected(blink::mojom::AIManagerCreateClientError::
-                                       kUnableToCalculateTokenSize));
-              return;
-            }
-
-            mojo::PendingRemote<blink::mojom::AILanguageModel> pending_remote;
-            std::move(callback).Run(std::make_unique<AILanguageModel>(
-                std::move(session), browser_context, std::move(pending_remote),
-                context_bound_object_set, ai_manager, context));
-          },
-          browser_context_->GetWeakPtr(), std::ref(context_bound_object_set),
-          context, std::ref(*this), std::move(callback)));
-  task->Start();
-  return task;
-}
-
 void AIManager::CreateLanguageModel(
     mojo::PendingRemote<blink::mojom::AIManagerCreateLanguageModelClient>
         client,
@@ -451,85 +400,83 @@
       if (!base::FeatureList::IsEnabled(
               blink::features::kAIPromptAPIMultimodalInput) ||
           !service->GetOnDeviceCapabilities().HasAll(capabilities)) {
-        mojo::Remote<blink::mojom::AIManagerCreateLanguageModelClient>
-            client_remote(std::move(client));
-        client_remote->OnError(
-            blink::mojom::AIManagerCreateClientError::kUnableToCreateSession);
+        mojo::Remote<blink::mojom::AIManagerCreateLanguageModelClient>(
+            std::move(client))
+            ->OnError(blink::mojom::AIManagerCreateClientError::
+                          kUnableToCreateSession);
         return;
       }
     }
     for (const auto& expected_input : options->expected_inputs.value()) {
       if (expected_input->languages.has_value() &&
           !IsLanguagesSupported(expected_input->languages.value())) {
-        mojo::Remote<blink::mojom::AIManagerCreateLanguageModelClient>
-            client_remote(std::move(client));
-        client_remote->OnError(
-            blink::mojom::AIManagerCreateClientError::kUnsupportedLanguage);
+        mojo::Remote<blink::mojom::AIManagerCreateLanguageModelClient>(
+            std::move(client))
+            ->OnError(
+                blink::mojom::AIManagerCreateClientError::kUnsupportedLanguage);
         return;
       }
     }
   }
 
+  if (!model_broker_client_) {
+    mojo::Remote<blink::mojom::AIManagerCreateLanguageModelClient>(
+        std::move(client))
+        ->OnError(
+            blink::mojom::AIManagerCreateClientError::kUnableToCreateSession);
+    return;
+  }
+  model_broker_client_
+      ->GetSubscriber(
+          optimization_guide::mojom::ModelBasedCapabilityKey::kPromptApi)
+      .WaitForClient(base::BindOnce(&AIManager::CreateLanguageModelInternal,
+                                    weak_factory_.GetWeakPtr(),
+                                    std::move(client), std::move(options)));
+}
+
+void AIManager::CreateLanguageModelInternal(
+    mojo::PendingRemote<blink::mojom::AIManagerCreateLanguageModelClient>
+        client,
+    blink::mojom::AILanguageModelCreateOptionsPtr options,
+    base::WeakPtr<optimization_guide::ModelClient> model_client) {
+  if (!model_client) {
+    mojo::Remote<blink::mojom::AIManagerCreateLanguageModelClient>
+        client_remote(std::move(client));
+    client_remote->OnError(
+        blink::mojom::AIManagerCreateClientError::kUnableToCreateSession);
+    return;
+  }
+
+  blink::mojom::AILanguageModelParamsPtr language_model_params =
+      GetLanguageModelParams();
   blink::mojom::AILanguageModelSamplingParamsPtr sampling_params =
       std::move(options->sampling_params);
-
-  auto create_language_model_callback = base::BindOnce(
-      [](mojo::PendingRemote<blink::mojom::AIManagerCreateLanguageModelClient>
-             client,
-         AIContextBoundObjectSet& context_bound_object_set,
-         blink::mojom::AILanguageModelCreateOptionsPtr options,
-         AILanguageModelOrCreationError creation_result) {
-        mojo::Remote<blink::mojom::AIManagerCreateLanguageModelClient>
-            client_remote(std::move(client));
-        if (!creation_result.has_value()) {
-          client_remote->OnError(creation_result.error());
-          return;
-        }
-        std::unique_ptr<AILanguageModel> language_model =
-            std::move(creation_result.value());
-        CHECK(language_model);
-        if (!options->initial_prompts.empty()) {
-          // Set the initial prompts, checking if they fit within token limits.
-          language_model->SetInitialPrompts(
-              std::move(options->initial_prompts),
-              base::BindOnce(
-                  [](mojo::Remote<
-                         blink::mojom::AIManagerCreateLanguageModelClient>
-                         client_remote,
-                     base::expected<
-                         mojo::PendingRemote<blink::mojom::AILanguageModel>,
-                         blink::mojom::AIManagerCreateClientError> remote,
-                     blink::mojom::AILanguageModelInstanceInfoPtr info) {
-                    if (remote.has_value()) {
-                      client_remote->OnResult(std::move(remote.value()),
-                                              std::move(info));
-                    } else {
-                      client_remote->OnError(remote.error());
-                    }
-                  },
-                  std::move(client_remote)));
-        } else {
-          client_remote->OnResult(
-              language_model->TakePendingRemote(),
-              language_model->GetLanguageModelInstanceInfo());
-        }
-
-        context_bound_object_set.AddContextBoundObject(
-            std::move(language_model));
-      },
-      std::move(client), std::ref(context_bound_object_set_),
-      std::move(options));
-
-  // When creating a new language model, the `context` will not be set since it
-  // should start fresh.
-  auto task = CreateLanguageModelInternal(
-      std::move(sampling_params), capabilities, context_bound_object_set_,
-      std::move(create_language_model_callback));
-  if (task->IsPending()) {
-    // Put `task` to AIContextBoundObjectSet to continue observing the model
-    // availability.
-    context_bound_object_set_.AddContextBoundObject(std::move(task));
+  auto params = on_device_model::mojom::SessionParams::New();
+  if (sampling_params) {
+    params->top_k = std::min(std::max(kMinTopK, sampling_params->top_k),
+                             language_model_params->max_sampling_params->top_k);
+    params->temperature =
+        std::min(std::max(kMinTemperature, sampling_params->temperature),
+                 language_model_params->max_sampling_params->temperature);
+  } else {
+    params->top_k = language_model_params->default_sampling_params->top_k;
+    params->temperature =
+        language_model_params->default_sampling_params->temperature;
   }
+
+  if (options->expected_inputs) {
+    params->capabilities = GetExpectedCapabilities(*options->expected_inputs);
+  }
+  mojo::PendingRemote<on_device_model::mojom::Session> session;
+  model_client->solution().CreateSession(
+      session.InitWithNewPipeAndPassReceiver(), params.Clone());
+
+  auto model = std::make_unique<AILanguageModel>(
+      context_bound_object_set_, std::move(params), std::move(model_client),
+      std::move(session));
+  model->Initialize(std::move(options->initial_prompts), std::move(client));
+
+  context_bound_object_set_.AddContextBoundObject(std::move(model));
 }
 
 void AIManager::CanCreateSummarizer(
@@ -803,44 +750,6 @@
       blink::mojom::ModelAvailabilityCheckResult::kAvailable);
 }
 
-void AIManager::CreateLanguageModelForCloning(
-    base::PassKey<AILanguageModel> pass_key,
-    blink::mojom::AILanguageModelSamplingParamsPtr sampling_params,
-    on_device_model::Capabilities capabilities,
-    AIContextBoundObjectSet& context_bound_object_set,
-    const AILanguageModel::Context& context,
-    mojo::Remote<blink::mojom::AIManagerCreateLanguageModelClient>
-        client_remote) {
-  auto create_language_model_callback = base::BindOnce(
-      [](AIContextBoundObjectSet& context_bound_object_set,
-         mojo::Remote<blink::mojom::AIManagerCreateLanguageModelClient>
-             client_remote,
-         AILanguageModelOrCreationError creation_result) {
-        if (!creation_result.has_value()) {
-          client_remote->OnError(creation_result.error());
-          return;
-        }
-        std::unique_ptr<AILanguageModel> language_model =
-            std::move(creation_result.value());
-        CHECK(language_model);
-
-        client_remote->OnResult(language_model->TakePendingRemote(),
-                                language_model->GetLanguageModelInstanceInfo());
-        context_bound_object_set.AddContextBoundObject(
-            std::move(language_model));
-      },
-      std::ref(context_bound_object_set), std::move(client_remote));
-  // When cloning an existing language model, the `context` from the source of
-  // clone should be provided.
-  auto task = CreateLanguageModelInternal(
-      std::move(sampling_params), capabilities, context_bound_object_set,
-      std::move(create_language_model_callback), context);
-  // The on-device model must be available before the existing language model
-  // was created, so the `CreateLanguageModelOnDeviceSessionTask` should
-  // complete without waiting for the on-device model availability changes.
-  CHECK(!task->IsPending());
-}
-
 void AIManager::OnModelPathValidationComplete(const std::string& model_path,
                                               bool is_valid_path) {
   // TODO(crbug.com/346491542): Remove this when the error page is implemented.
diff --git a/chrome/browser/ai/ai_manager.h b/chrome/browser/ai/ai_manager.h
index 8c647aa..02c934a 100644
--- a/chrome/browser/ai/ai_manager.h
+++ b/chrome/browser/ai/ai_manager.h
@@ -56,14 +56,6 @@
   ~AIManager() override;
 
   void AddReceiver(mojo::PendingReceiver<blink::mojom::AIManager> receiver);
-  void CreateLanguageModelForCloning(
-      base::PassKey<AILanguageModel> pass_key,
-      blink::mojom::AILanguageModelSamplingParamsPtr sampling_params,
-      on_device_model::Capabilities capabilities,
-      AIContextBoundObjectSet& context_bound_object_set,
-      const AILanguageModel::Context& context,
-      mojo::Remote<blink::mojom::AIManagerCreateLanguageModelClient>
-          client_remote);
 
   size_t GetContextBoundObjectSetSizeForTesting() {
     return context_bound_object_set_.GetSizeForTesting();
@@ -133,21 +125,13 @@
   void OnModelPathValidationComplete(const std::string& model_path,
                                      bool is_valid_path);
 
-  // Creates an `AILanguageModel`, either as a new session, or as a clone of
-  // an existing session with its context copied. When this method is called
-  // during the session cloning, the optional `context` variable should be set
-  // to the existing `AILanguageModel`'s session.
-  // The `CreateLanguageModelOnDeviceSessionTask` will be returned and the
-  // caller is responsible for keeping it alive if the task is waiting for the
-  // model to be available.
-  std::unique_ptr<CreateLanguageModelOnDeviceSessionTask>
-  CreateLanguageModelInternal(
-      blink::mojom::AILanguageModelSamplingParamsPtr sampling_params,
-      on_device_model::Capabilities capabilities,
-      AIContextBoundObjectSet& context_bound_object_set,
-      base::OnceCallback<void(AILanguageModelOrCreationError)> callback,
-      const std::optional<const AILanguageModel::Context>& context =
-          std::nullopt);
+  // Creates an `AILanguageModel`, as a new session. Clones are created
+  // internally within the `AILanguageModel` object.
+  void CreateLanguageModelInternal(
+      mojo::PendingRemote<blink::mojom::AIManagerCreateLanguageModelClient>
+          client,
+      blink::mojom::AILanguageModelCreateOptionsPtr options,
+      base::WeakPtr<optimization_guide::ModelClient> model_client);
 
   // content::RenderWidgetHostObserver:
   void RenderWidgetHostVisibilityChanged(content::RenderWidgetHost* widget_host,
@@ -173,6 +157,8 @@
                           content::RenderWidgetHostObserver>
       widget_observer_{this};
 
+  std::unique_ptr<optimization_guide::ModelBrokerClient> model_broker_client_;
+
   base::WeakPtrFactory<AIManager> weak_factory_{this};
 };
 
diff --git a/chrome/browser/ai/ai_manager_unittest.cc b/chrome/browser/ai/ai_manager_unittest.cc
index 0697cc8..ced11973 100644
--- a/chrome/browser/ai/ai_manager_unittest.cc
+++ b/chrome/browser/ai/ai_manager_unittest.cc
@@ -15,6 +15,7 @@
 #include "chrome/browser/optimization_guide/mock_optimization_guide_keyed_service.h"
 #include "components/keyed_service/core/keyed_service.h"
 #include "components/optimization_guide/core/mock_optimization_guide_model_executor.h"
+#include "components/optimization_guide/core/model_execution/test/fake_model_broker.h"
 #include "components/optimization_guide/core/optimization_guide_model_executor.h"
 #include "components/optimization_guide/core/optimization_guide_switches.h"
 #include "components/policy/core/common/policy_pref_names.h"
@@ -39,8 +40,23 @@
 
 class AIManagerTest : public AITestUtils::AITestBase {
  protected:
+  AIManagerTest()
+      : fake_broker_(optimization_guide::FakeAdaptationAsset({
+            .config =
+                [] {
+                  optimization_guide::proto::OnDeviceModelExecutionFeatureConfig
+                      config;
+                  config.set_can_skip_text_safety(true);
+                  config.set_feature(
+                      optimization_guide::proto::ModelExecutionFeature::
+                          MODEL_EXECUTION_FEATURE_PROMPT_API);
+                  return config;
+                }(),
+        })) {}
+
   void SetUp() override {
     AITestUtils::AITestBase::SetUp();
+    SetupMockOptimizationGuideKeyedService();
     ai_manager_ =
         std::make_unique<AIManager>(main_rfh()->GetBrowserContext(),
                                     &component_update_service_, main_rfh());
@@ -75,6 +91,12 @@
             GetOnDeviceModelEligibility(_))
         .WillByDefault(testing::Return(
             optimization_guide::OnDeviceModelEligibilityReason::kSuccess));
+    ON_CALL(*mock_optimization_guide_keyed_service_, CreateModelBrokerClient())
+        .WillByDefault([&]() {
+          return std::make_unique<optimization_guide::ModelBrokerClient>(
+              fake_broker_.BindAndPassRemote(),
+              optimization_guide::CreateSessionArgs(nullptr, {}));
+        });
   }
 
   void SetBuildInAIAPIsEnterprisePolicy(bool value) {
@@ -82,18 +104,14 @@
         policy::policy_prefs::kBuiltInAIAPIsEnabled, value);
   }
 
- protected:
-  std::unique_ptr<AIManager> ai_manager_;
-
  private:
   testing::NiceMock<MockSession> session_;
+  optimization_guide::FakeModelBroker fake_broker_;
 };
 
 // Tests that involve invalid on-device model file paths should not crash when
 // the associated RFH is destroyed.
 TEST_F(AIManagerTest, NoUAFWithInvalidOnDeviceModelPath) {
-  SetupMockOptimizationGuideKeyedService();
-
   auto* command_line = base::CommandLine::ForCurrentProcess();
   command_line->AppendSwitchASCII(
       optimization_guide::switches::kOnDeviceModelExecutionOverride,
@@ -114,8 +132,6 @@
 // Tests the `AIUserDataSet`'s behavior of managing the lifetime of
 // `AILanguageModel`s.
 TEST_F(AIManagerTest, AIContextBoundObjectSet) {
-  SetupMockOptimizationGuideKeyedService();
-
   mojo::Remote<blink::mojom::AILanguageModel> mock_session;
   AITestUtils::MockCreateLanguageModelClient mock_create_language_model_client;
   base::RunLoop run_loop;
@@ -153,7 +169,6 @@
 }
 
 TEST_F(AIManagerTest, CanCreate) {
-  SetupMockOptimizationGuideKeyedService();
   base::MockCallback<
       base::OnceCallback<void(blink::mojom::ModelAvailabilityCheckResult)>>
       callback;
@@ -168,7 +183,6 @@
 }
 
 TEST_F(AIManagerTest, CanCreateNotEnabled) {
-  SetupMockOptimizationGuideKeyedService();
   EXPECT_CALL(*mock_optimization_guide_keyed_service_,
               GetOnDeviceModelEligibilityAsync(_, _))
       .Times(4)
@@ -191,7 +205,6 @@
 }
 
 TEST_F(AIManagerTest, CanCreateSessionWithTextInputCapabilities) {
-  SetupMockOptimizationGuideKeyedService();
   base::MockCallback<blink::mojom::AIManager::CanCreateLanguageModelCallback>
       callback;
   optimization_guide::ModelBasedCapabilityKey key =
@@ -214,7 +227,6 @@
 TEST_F(AIManagerTest, CanCreateSessionWithImageAndAudioInputCapabilities) {
   base::test::ScopedFeatureList scoped_feature_list(
       blink::features::kAIPromptAPIMultimodalInput);
-  SetupMockOptimizationGuideKeyedService();
   EXPECT_CALL(*mock_optimization_guide_keyed_service_,
               GetOnDeviceCapabilities())
       .Times(2)
@@ -237,7 +249,6 @@
 }
 
 TEST_F(AIManagerTest, CanCreateEnterprisePolicyDisabled) {
-  SetupMockOptimizationGuideKeyedService();
   SetBuildInAIAPIsEnterprisePolicy(false);
   base::MockCallback<
       base::OnceCallback<void(blink::mojom::ModelAvailabilityCheckResult)>>
diff --git a/chrome/browser/ai/ai_test_utils.h b/chrome/browser/ai/ai_test_utils.h
index 4f85cad..aa450d3 100644
--- a/chrome/browser/ai/ai_test_utils.h
+++ b/chrome/browser/ai/ai_test_utils.h
@@ -211,7 +211,6 @@
     testing::NiceMock<optimization_guide::MockSession> session_;
     AITestUtils::MockComponentUpdateService component_update_service_;
 
-   private:
     std::unique_ptr<AIManager> ai_manager_;
   };
 
diff --git a/chrome/browser/android/tab_android.cc b/chrome/browser/android/tab_android.cc
index 75d26839..c066ef5 100644
--- a/chrome/browser/android/tab_android.cc
+++ b/chrome/browser/android/tab_android.cc
@@ -678,6 +678,10 @@
   return tab_features_.get();
 }
 
+const tabs::TabFeatures* TabAndroid::GetTabFeatures() const {
+  return tab_features_.get();
+}
+
 bool TabAndroid::IsPinned() const {
   NOTIMPLEMENTED();
   return false;
diff --git a/chrome/browser/android/tab_android.h b/chrome/browser/android/tab_android.h
index 61bf905f..dae79a5 100644
--- a/chrome/browser/android/tab_android.h
+++ b/chrome/browser/android/tab_android.h
@@ -236,6 +236,7 @@
       TabInterfaceCallback callback) override;
   bool IsInNormalWindow() const override;
   tabs::TabFeatures* GetTabFeatures() override;
+  const tabs::TabFeatures* GetTabFeatures() const override;
   bool IsPinned() const override;
   bool IsSplit() const override;
   std::optional<tab_groups::TabGroupId> GetGroup() const override;
diff --git a/chrome/browser/ash/arc/session/arc_play_store_enabled_preference_handler_unittest.cc b/chrome/browser/ash/arc/session/arc_play_store_enabled_preference_handler_unittest.cc
index d028233e..87e3af1 100644
--- a/chrome/browser/ash/arc/session/arc_play_store_enabled_preference_handler_unittest.cc
+++ b/chrome/browser/ash/arc/session/arc_play_store_enabled_preference_handler_unittest.cc
@@ -22,6 +22,7 @@
 #include "chrome/browser/signin/identity_test_environment_profile_adaptor.h"
 #include "chrome/browser/ui/ash/login/fake_login_display_host.h"
 #include "chrome/grit/generated_resources.h"
+#include "chrome/test/base/scoped_testing_local_state.h"
 #include "chrome/test/base/testing_browser_process.h"
 #include "chrome/test/base/testing_profile.h"
 #include "chromeos/ash/components/dbus/concierge/concierge_client.h"
@@ -35,7 +36,6 @@
 #include "components/session_manager/core/session_manager.h"
 #include "components/signin/public/base/consent_level.h"
 #include "components/signin/public/identity_manager/identity_manager.h"
-#include "components/sync_preferences/testing_pref_service_syncable.h"
 #include "components/user_manager/known_user.h"
 #include "components/user_manager/scoped_user_manager.h"
 #include "content/public/test/browser_task_environment.h"
@@ -99,9 +99,6 @@
     identity_test_env_profile_adaptor_->identity_test_env()
         ->MakePrimaryAccountAvailable(kTestEmail,
                                       signin::ConsentLevel::kSignin);
-
-    TestingBrowserProcess::GetGlobal()->SetLocalState(&pref_service_);
-    user_manager::KnownUser::RegisterPrefs(pref_service_.registry());
   }
 
   void TearDown() override {
@@ -109,7 +106,6 @@
     arc_session_manager_.reset();
     identity_test_env_profile_adaptor_.reset();
     profile_.reset();
-    TestingBrowserProcess::GetGlobal()->SetLocalState(nullptr);
     ash::UpstartClient::Shutdown();
     ash::SessionManagerClient::Shutdown();
     ash::ConciergeClient::Shutdown();
@@ -144,6 +140,8 @@
 
  private:
   content::BrowserTaskEnvironment task_environment_;
+  ScopedTestingLocalState scoped_testing_local_state_{
+      TestingBrowserProcess::GetGlobal()};
   user_manager::TypedScopedUserManager<ash::FakeChromeUserManager>
       fake_user_manager_;
   session_manager::SessionManager session_manager_;
@@ -154,7 +152,6 @@
   std::unique_ptr<ArcSessionManager> arc_session_manager_;
   std::unique_ptr<ash::FakeLoginDisplayHost> fake_login_display_host_;
   std::unique_ptr<ArcPlayStoreEnabledPreferenceHandler> preference_handler_;
-  TestingPrefServiceSimple pref_service_;
 };
 
 TEST_F(ArcPlayStoreEnabledPreferenceHandlerTest, PrefChangeTriggersService) {
diff --git a/chrome/browser/ash/arc/session/arc_session_manager_unittest.cc b/chrome/browser/ash/arc/session/arc_session_manager_unittest.cc
index b4879f49..14a5a4b 100644
--- a/chrome/browser/ash/arc/session/arc_session_manager_unittest.cc
+++ b/chrome/browser/ash/arc/session/arc_session_manager_unittest.cc
@@ -51,6 +51,7 @@
 #include "chrome/browser/profiles/profile.h"
 #include "chrome/browser/ui/ash/login/fake_login_display_host.h"
 #include "chrome/browser/ui/ash/multi_user/multi_user_util.h"
+#include "chrome/test/base/scoped_testing_local_state.h"
 #include "chrome/test/base/testing_browser_process.h"
 #include "chrome/test/base/testing_profile.h"
 #include "chromeos/ash/components/browser_context_helper/annotated_account_id.h"
@@ -278,8 +279,6 @@
       : task_environment_(content::BrowserTaskEnvironment::IO_MAINLOOP,
                           base::test::TaskEnvironment::TimeSource::MOCK_TIME),
         fake_user_manager_(std::make_unique<ash::FakeChromeUserManager>()) {
-    TestingBrowserProcess::GetGlobal()->SetLocalState(&test_local_state_);
-    RegisterLocalState(test_local_state_.registry());
     auth_events_recorder_ = ash::AuthEventsRecorder::CreateForTesting();
   }
 
@@ -287,9 +286,7 @@
   ArcSessionManagerTestBase& operator=(const ArcSessionManagerTestBase&) =
       delete;
 
-  ~ArcSessionManagerTestBase() override {
-    TestingBrowserProcess::GetGlobal()->SetLocalState(nullptr);
-  }
+  ~ArcSessionManagerTestBase() override = default;
 
   void SetUp() override {
     ash::ArcVmDataMigratorClient::InitializeFake();
@@ -370,6 +367,8 @@
   }
 
   content::BrowserTaskEnvironment task_environment_;
+  ScopedTestingLocalState scoped_testing_local_state_{
+      TestingBrowserProcess::GetGlobal()};
   user_manager::TypedScopedUserManager<ash::FakeChromeUserManager>
       fake_user_manager_;
   session_manager::SessionManager session_manager_;
@@ -377,7 +376,6 @@
   std::unique_ptr<ArcServiceManager> arc_service_manager_;
   std::unique_ptr<ArcSessionManager> arc_session_manager_;
   base::ScopedTempDir temp_dir_;
-  TestingPrefServiceSimple test_local_state_;
   std::unique_ptr<ash::AuthEventsRecorder> auth_events_recorder_;
 };
 
@@ -1830,7 +1828,6 @@
   void TearDown() override {
     if (is_oobe_optin()) {
       fake_login_display_host_.reset();
-      TestingBrowserProcess::GetGlobal()->SetLocalState(nullptr);
     }
     ArcSessionManagerTestBase::TearDown();
   }
@@ -2067,7 +2064,6 @@
 
     ArcSessionManager::SetArcTermsOfServiceOobeNegotiatorEnabledForTesting(
         false);
-    TestingBrowserProcess::GetGlobal()->SetLocalState(nullptr);
 
     ArcSessionManagerTest::TearDown();
   }
diff --git a/chrome/browser/ash/arc/wallpaper/arc_wallpaper_service_unittest.cc b/chrome/browser/ash/arc/wallpaper/arc_wallpaper_service_unittest.cc
index d178e75..a3a12366 100644
--- a/chrome/browser/ash/arc/wallpaper/arc_wallpaper_service_unittest.cc
+++ b/chrome/browser/ash/arc/wallpaper/arc_wallpaper_service_unittest.cc
@@ -21,6 +21,7 @@
 #include "chrome/browser/ui/ash/wallpaper/test_wallpaper_controller.h"
 #include "chrome/browser/ui/ash/wallpaper/wallpaper_controller_client_impl.h"
 #include "chrome/common/pref_names.h"
+#include "chrome/test/base/scoped_testing_local_state.h"
 #include "chrome/test/base/testing_browser_process.h"
 #include "chrome/test/base/testing_profile.h"
 #include "chromeos/ash/components/cryptohome/system_salt_getter.h"
@@ -73,15 +74,6 @@
   ~ArcWallpaperServiceTest() override = default;
 
   void SetUp() override {
-    // Prefs
-    TestingBrowserProcess::GetGlobal()->SetLocalState(&pref_service_);
-    pref_service_.registry()->RegisterDictionaryPref(
-        ash::prefs::kUserWallpaperInfo);
-    pref_service_.registry()->RegisterDictionaryPref(
-        ash::prefs::kWallpaperColors);
-    pref_service_.registry()->RegisterStringPref(
-        prefs::kDeviceWallpaperImageFilePath, std::string());
-
     // User
     fake_user_manager_->AddUser(user_manager::StubAccountId());
     fake_user_manager_->LoginUser(user_manager::StubAccountId());
@@ -117,7 +109,6 @@
     wallpaper_instance_.reset();
 
     wallpaper_controller_client_.reset();
-    TestingBrowserProcess::GetGlobal()->SetLocalState(nullptr);
     ash::SystemSaltGetter::Shutdown();
   }
 
@@ -129,12 +120,13 @@
 
  private:
   std::unique_ptr<content::BrowserTaskEnvironment> task_environment_;
+  ScopedTestingLocalState scoped_testing_local_state_{
+      TestingBrowserProcess::GetGlobal()};
   user_manager::TypedScopedUserManager<ash::FakeChromeUserManager>
       fake_user_manager_;
   arc::ArcServiceManager arc_service_manager_;
-  TestingPrefServiceSimple pref_service_;
   // testing_profile_ needs to be deleted before arc_service_manager_ and
-  // pref_service_.
+  // scoped_testing_local_state_.
   TestingProfile testing_profile_;
 };
 
diff --git a/chrome/browser/ash/customization/customization_document_unittest.cc b/chrome/browser/ash/customization/customization_document_unittest.cc
index 9d4b654..8bfd395 100644
--- a/chrome/browser/ash/customization/customization_document_unittest.cc
+++ b/chrome/browser/ash/customization/customization_document_unittest.cc
@@ -16,6 +16,7 @@
 #include "chrome/browser/extensions/external_provider_impl.h"
 #include "chrome/browser/prefs/browser_prefs.h"
 #include "chrome/browser/profiles/profile_manager.h"
+#include "chrome/test/base/scoped_testing_local_state.h"
 #include "chrome/test/base/testing_browser_process.h"
 #include "chrome/test/base/testing_profile.h"
 #include "chromeos/ash/components/network/network_handler.h"
@@ -211,15 +212,11 @@
         default_network ? default_network->guid() : std::string();
     network_portal_detector_.SetDefaultNetworkForTesting(guid);
 
-    TestingBrowserProcess::GetGlobal()->SetLocalState(&local_state_);
-    RegisterLocalState(local_state_.registry());
-
     interceptor_ =
         std::make_unique<TestURLLoaderFactoryInterceptor>(&loader_factory_);
   }
 
   void TearDown() override {
-    TestingBrowserProcess::GetGlobal()->SetLocalState(nullptr);
     network_portal_detector::InitializeForTesting(nullptr);
     loader_factory_.ClearResponses();
     interceptor_.reset();
@@ -279,10 +276,11 @@
 
  private:
   content::BrowserTaskEnvironment task_environment_;
+  ScopedTestingLocalState scoped_testing_local_state_{
+      TestingBrowserProcess::GetGlobal()};
   NetworkHandlerTestHelper network_handler_test_helper_;
   system::ScopedFakeStatisticsProvider fake_statistics_provider_;
   ScopedCrosSettingsTestHelper scoped_cros_settings_test_helper_;
-  TestingPrefServiceSimple local_state_;
   network::TestURLLoaderFactory loader_factory_;
   std::unique_ptr<TestURLLoaderFactoryInterceptor> interceptor_;
   NetworkPortalDetectorTestImpl network_portal_detector_;
diff --git a/chrome/browser/ash/file_manager/file_tasks_browsertest.cc b/chrome/browser/ash/file_manager/file_tasks_browsertest.cc
index 7c1683d..a6f69f5 100644
--- a/chrome/browser/ash/file_manager/file_tasks_browsertest.cc
+++ b/chrome/browser/ash/file_manager/file_tasks_browsertest.cc
@@ -81,6 +81,7 @@
 #include "chrome/browser/web_applications/web_app_registry_update.h"
 #include "chrome/browser/web_applications/web_app_sync_bridge.h"
 #include "chrome/common/chrome_paths.h"
+#include "chrome/common/extensions/api/file_manager_private.h"
 #include "chrome/common/extensions/extension_constants.h"
 #include "chrome/common/pref_names.h"
 #include "chrome/common/webui_url_constants.h"
@@ -1295,6 +1296,7 @@
   }
 
  protected:
+  base::FilePath drive_mount_point_;
   const std::string alternate_url_ =
       "https://docs.google.com/document/d/smalldocxid?rtpof=true&usp=drive_fs";
   const TaskDescriptor web_drive_office_task_ = CreateWebDriveOfficeTask();
@@ -1304,7 +1306,6 @@
 
  private:
   base::ScopedTempDir temp_dir_;
-  base::FilePath drive_mount_point_;
   const std::string test_file_name_ = "text.docx";
   base::FilePath relative_test_file_path;
   base::test::ScopedFeatureList feature_list_;
@@ -1569,6 +1570,37 @@
       ash::cloud_upload::CloudProvider::kGoogleDrive, 1);
 }
 
+// Test that CloudOpenTask::Execute() will fail to open the file when source
+// volume cannot be found.
+IN_PROC_BROWSER_TEST_F(DriveTest, SourceVolumeNotFound) {
+  // Set up DriveFs.
+  SetUpTest(/*disable_set_up=*/false, /*launch_files_app=*/true);
+
+  // Create a test file outside of Drive.
+  FileSystemURL file_outside_drive = CreateOfficeFileSourceURL(profile());
+  std::vector<storage::FileSystemURL> file_urls{file_outside_drive};
+
+  // Remove source volume from the VolumeManager.
+  VolumeManager* volume_manager = VolumeManager::Get(profile());
+  base::WeakPtr<file_manager::Volume> source_volume =
+      volume_manager->FindVolumeFromPath(file_outside_drive.path());
+  volume_manager->RemoveVolumeForTesting(source_volume->volume_id());
+
+  // Ensure that the file cannot be opened.
+  ASSERT_FALSE(ash::cloud_upload::CloudOpenTask::Execute(
+      profile(), file_urls, CreateWebDriveOfficeTask(),
+      ash::cloud_upload::CloudProvider::kGoogleDrive,
+      std::move(cloud_open_metrics_)));
+
+  // The TaskResult should be kCannotGetSourceType and no TransferRequired
+  // metric should be logged.
+  histogram_.ExpectUniqueSample(
+      ash::cloud_upload::kGoogleDriveTaskResultMetricName,
+      ash::cloud_upload::OfficeTaskResult::kCannotGetSourceType, 1);
+  histogram_.ExpectTotalCount(ash::cloud_upload::kDriveTransferRequiredMetric,
+                              0);
+}
+
 // Fake app service web app publisher to test when an app is launched.
 class FakeWebAppPublisher : public apps::AppPublisher {
  public:
@@ -2042,6 +2074,7 @@
   // will fail as there is not an equivalent ODFS file path.
   auto task = base::WrapRefCounted(new ash::cloud_upload::CloudOpenTask(
       profile(), {android_onedrive_url}, open_in_office_task_,
+      ash::cloud_upload::SourceType::CLOUD,
       ash::cloud_upload::CloudProvider::kOneDrive,
       std::make_unique<ash::cloud_upload::CloudOpenMetrics>(
           ash::cloud_upload::CloudProvider::kOneDrive, 1)));
@@ -2478,6 +2511,7 @@
   // Triggers Move Confirmation dialog.
   auto task = base::WrapRefCounted(new ash::cloud_upload::CloudOpenTask(
       profile(), file_urls, CreateOpenInOfficeTask(),
+      ash::cloud_upload::SourceType::LOCAL,
       ash::cloud_upload::CloudProvider::kOneDrive,
       std::move(cloud_open_metrics_)));
   task->OpenOrMoveFiles();
@@ -2513,6 +2547,7 @@
   // Open file directly from ODFS.
   auto task = base::WrapRefCounted(new ash::cloud_upload::CloudOpenTask(
       profile(), file_urls_, open_in_office_task_,
+      ash::cloud_upload::SourceType::CLOUD,
       ash::cloud_upload::CloudProvider::kOneDrive,
       std::move(cloud_open_metrics_)));
   task->OpenOrMoveFiles();
@@ -2560,6 +2595,7 @@
   // Open file directly from ODFS.
   auto task = base::WrapRefCounted(new ash::cloud_upload::CloudOpenTask(
       profile(), file_urls_, open_in_office_task_,
+      ash::cloud_upload::SourceType::CLOUD,
       ash::cloud_upload::CloudProvider::kOneDrive,
       std::move(cloud_open_metrics_)));
   task->OpenOrMoveFiles();
@@ -2608,6 +2644,7 @@
   // Open the file indirectly from Android OneDrive (via ODFS).
   auto task = base::WrapRefCounted(new ash::cloud_upload::CloudOpenTask(
       profile(), {android_onedrive_url}, open_in_office_task_,
+      ash::cloud_upload::SourceType::CLOUD,
       ash::cloud_upload::CloudProvider::kOneDrive,
       std::move(cloud_open_metrics_)));
   task->OpenOrMoveFiles();
@@ -2652,6 +2689,7 @@
   // Open file directly from ODFS.
   auto task = base::WrapRefCounted(new ash::cloud_upload::CloudOpenTask(
       profile(), file_urls_, open_in_office_task_,
+      ash::cloud_upload::SourceType::CLOUD,
       ash::cloud_upload::CloudProvider::kOneDrive,
       std::move(cloud_open_metrics_)));
   task->OpenOrMoveFiles();
@@ -2741,6 +2779,7 @@
   // Open the file indirectly from Android OneDrive (via ODFS).
   auto task = base::WrapRefCounted(new ash::cloud_upload::CloudOpenTask(
       profile(), {android_onedrive_url}, open_in_office_task_,
+      ash::cloud_upload::SourceType::CLOUD,
       ash::cloud_upload::CloudProvider::kOneDrive,
       std::move(cloud_open_metrics_)));
   task->OpenOrMoveFiles();
@@ -2795,6 +2834,7 @@
   // will fail as the email accounts don't match.
   auto task = base::WrapRefCounted(new ash::cloud_upload::CloudOpenTask(
       profile(), {android_onedrive_url}, open_in_office_task_,
+      ash::cloud_upload::SourceType::CLOUD,
       ash::cloud_upload::CloudProvider::kOneDrive,
       std::move(cloud_open_metrics_)));
   task->OpenOrMoveFiles();
@@ -2838,6 +2878,7 @@
   // will fail as there is not an equivalent ODFS file path.
   auto task = base::WrapRefCounted(new ash::cloud_upload::CloudOpenTask(
       profile(), {android_onedrive_url}, open_in_office_task_,
+      ash::cloud_upload::SourceType::CLOUD,
       ash::cloud_upload::CloudProvider::kOneDrive,
       std::move(cloud_open_metrics_)));
   task->OpenOrMoveFiles();
diff --git a/chrome/browser/ash/file_manager/volume_manager.cc b/chrome/browser/ash/file_manager/volume_manager.cc
index ad129650..79226c18 100644
--- a/chrome/browser/ash/file_manager/volume_manager.cc
+++ b/chrome/browser/ash/file_manager/volume_manager.cc
@@ -1665,7 +1665,6 @@
     LOG(WARNING) << "Cannot find volume '" << volume_id << "' to unmount it";
     return;
   }
-
   DoUnmountEvent(std::move(it), error);
 }
 
diff --git a/chrome/browser/ash/guest_os/guest_id.h b/chrome/browser/ash/guest_os/guest_id.h
index 35e67e6..d603a362 100644
--- a/chrome/browser/ash/guest_os/guest_id.h
+++ b/chrome/browser/ash/guest_os/guest_id.h
@@ -36,9 +36,6 @@
 
 bool operator<(const GuestId& lhs, const GuestId& rhs) noexcept;
 bool operator==(const GuestId& lhs, const GuestId& rhs) noexcept;
-inline bool operator!=(const GuestId& lhs, const GuestId& rhs) noexcept {
-  return !(lhs == rhs);
-}
 
 std::ostream& operator<<(std::ostream& ostream, const GuestId& container_id);
 
diff --git a/chrome/browser/ash/login/enrollment/enrollment_screen_unittest.cc b/chrome/browser/ash/login/enrollment/enrollment_screen_unittest.cc
index 51300649..f37adfa3 100644
--- a/chrome/browser/ash/login/enrollment/enrollment_screen_unittest.cc
+++ b/chrome/browser/ash/login/enrollment/enrollment_screen_unittest.cc
@@ -37,6 +37,7 @@
 #include "chrome/browser/ui/ash/login/fake_login_display_host.h"
 #include "chrome/browser/ui/webui/ash/login/online_login_utils.h"
 #include "chrome/common/pref_names.h"
+#include "chrome/test/base/scoped_testing_local_state.h"
 #include "chrome/test/base/testing_browser_process.h"
 #include "chromeos/ash/components/install_attributes/stub_install_attributes.h"
 #include "chromeos/ash/components/network/network_handler_test_helper.h"
@@ -97,15 +98,11 @@
  protected:
   EnrollmentScreenBaseTest()
       : mock_error_screen_(mock_error_view_.AsWeakPtr()) {
-    RegisterLocalState(fake_local_state_.registry());
-    TestingBrowserProcess::GetGlobal()->SetLocalState(&fake_local_state_);
-
     policy::EnrollmentRequisitionManager::Initialize();
   }
 
   ~EnrollmentScreenBaseTest() override {
     TestingBrowserProcess::GetGlobal()->SetShuttingDown(true);
-    TestingBrowserProcess::GetGlobal()->SetLocalState(nullptr);
   }
 
   // Creates the EnrollmentScreen and sets required parameters.
@@ -329,7 +326,9 @@
     return CHECK_DEREF(fake_login_display_host_.GetWizardContext());
   }
 
-  TestingPrefServiceSimple& local_state() { return fake_local_state_; }
+  TestingPrefServiceSimple& local_state() {
+    return *scoped_testing_local_state_.Get();
+  }
 
   MockEnrollmentLauncher& mock_enrollment_launcher() {
     return mock_enrollment_launcher_;
@@ -379,7 +378,8 @@
   ScopedStubInstallAttributes test_install_attributes_;
 
   // Used by `EnrollmentRequisitionManager` and `StartupUtils`.
-  TestingPrefServiceSimple fake_local_state_;
+  ScopedTestingLocalState scoped_testing_local_state_{
+      TestingBrowserProcess::GetGlobal()};
 
   // Used by `EnrollmentRequisitionManager`.
   system::ScopedFakeStatisticsProvider fake_statistics_provider_;
diff --git a/chrome/browser/ash/login/session/session_length_limiter_unittest.cc b/chrome/browser/ash/login/session/session_length_limiter_unittest.cc
index b020cb5..79222f2c 100644
--- a/chrome/browser/ash/login/session/session_length_limiter_unittest.cc
+++ b/chrome/browser/ash/login/session/session_length_limiter_unittest.cc
@@ -19,6 +19,7 @@
 #include "base/values.h"
 #include "chrome/browser/ash/settings/scoped_cros_settings_test_helper.h"
 #include "chrome/common/pref_names.h"
+#include "chrome/test/base/scoped_testing_local_state.h"
 #include "chrome/test/base/testing_browser_process.h"
 #include "chromeos/ash/components/demo_mode/utils/demo_session_utils.h"
 #include "chromeos/ash/components/install_attributes/stub_install_attributes.h"
@@ -118,7 +119,13 @@
   base::Time session_stop_time_;
 
  private:
-  TestingPrefServiceSimple local_state_;
+  TestingPrefServiceSimple& local_state() {
+    return *scoped_testing_local_state_.Get();
+  }
+
+  ScopedTestingLocalState scoped_testing_local_state_{
+      TestingBrowserProcess::GetGlobal()};
+
   bool user_activity_seen_;
 
   raw_ptr<MockSessionLengthLimiterDelegate, DanglingUntriaged>
@@ -131,8 +138,6 @@
     : user_activity_seen_(false), delegate_(nullptr) {}
 
 void SessionLengthLimiterTest::SetUp() {
-  TestingBrowserProcess::GetGlobal()->SetLocalState(&local_state_);
-  SessionLengthLimiter::RegisterPrefs(local_state_.registry());
   runner_ = new base::TestMockTimeTaskRunner;
   wall_clock_forwarder_ = std::make_unique<WallClockForwarder>(runner_.get());
   runner_->FastForwardBy(base::TimeDelta::FromInternalValue(1000));
@@ -141,66 +146,65 @@
 void SessionLengthLimiterTest::TearDown() {
   wall_clock_forwarder_.reset();
   session_length_limiter_.reset();
-  TestingBrowserProcess::GetGlobal()->SetLocalState(nullptr);
 }
 
 void SessionLengthLimiterTest::SetSessionUserActivitySeenPref(
     bool user_activity_seen) {
-  local_state_.SetUserPref(prefs::kSessionUserActivitySeen,
-                           std::make_unique<base::Value>(user_activity_seen));
+  local_state().SetUserPref(prefs::kSessionUserActivitySeen,
+                            std::make_unique<base::Value>(user_activity_seen));
 }
 
 void SessionLengthLimiterTest::ClearSessionUserActivitySeenPref() {
-  local_state_.ClearPref(prefs::kSessionUserActivitySeen);
+  local_state().ClearPref(prefs::kSessionUserActivitySeen);
 }
 
 bool SessionLengthLimiterTest::IsSessionUserActivitySeenPrefSet() {
-  return local_state_.HasPrefPath(prefs::kSessionUserActivitySeen);
+  return local_state().HasPrefPath(prefs::kSessionUserActivitySeen);
 }
 
 bool SessionLengthLimiterTest::GetSessionUserActivitySeenPref() {
   EXPECT_TRUE(IsSessionUserActivitySeenPrefSet());
-  return local_state_.GetBoolean(prefs::kSessionUserActivitySeen);
+  return local_state().GetBoolean(prefs::kSessionUserActivitySeen);
 }
 
 void SessionLengthLimiterTest::SetSessionStartTimePref(
     const base::Time& session_start_time) {
-  local_state_.SetUserPref(prefs::kSessionStartTime,
-                           std::make_unique<base::Value>(base::NumberToString(
-                               session_start_time.ToInternalValue())));
+  local_state().SetUserPref(prefs::kSessionStartTime,
+                            std::make_unique<base::Value>(base::NumberToString(
+                                session_start_time.ToInternalValue())));
 }
 
 void SessionLengthLimiterTest::ClearSessionStartTimePref() {
-  local_state_.ClearPref(prefs::kSessionStartTime);
+  local_state().ClearPref(prefs::kSessionStartTime);
 }
 
 bool SessionLengthLimiterTest::IsSessionStartTimePrefSet() {
-  return local_state_.HasPrefPath(prefs::kSessionStartTime);
+  return local_state().HasPrefPath(prefs::kSessionStartTime);
 }
 
 base::Time SessionLengthLimiterTest::GetSessionStartTimePref() {
   EXPECT_TRUE(IsSessionStartTimePrefSet());
   return base::Time::FromInternalValue(
-      local_state_.GetInt64(prefs::kSessionStartTime));
+      local_state().GetInt64(prefs::kSessionStartTime));
 }
 
 void SessionLengthLimiterTest::SetSessionLengthLimitPref(
     const base::TimeDelta& session_length_limit) {
-  local_state_.SetUserPref(prefs::kSessionLengthLimit,
-                           std::make_unique<base::Value>(static_cast<int>(
-                               session_length_limit.InMilliseconds())));
+  local_state().SetUserPref(prefs::kSessionLengthLimit,
+                            std::make_unique<base::Value>(static_cast<int>(
+                                session_length_limit.InMilliseconds())));
   UpdateSessionStartTimeIfWaitingForUserActivity();
 }
 
 void SessionLengthLimiterTest::ClearSessionLengthLimitPref() {
-  local_state_.RemoveUserPref(prefs::kSessionLengthLimit);
+  local_state().RemoveUserPref(prefs::kSessionLengthLimit);
   UpdateSessionStartTimeIfWaitingForUserActivity();
 }
 
 void SessionLengthLimiterTest::SetWaitForInitialUserActivityPref(
     bool wait_for_initial_user_activity) {
   UpdateSessionStartTimeIfWaitingForUserActivity();
-  local_state_.SetUserPref(
+  local_state().SetUserPref(
       prefs::kSessionWaitForInitialUserActivity,
       std::make_unique<base::Value>(wait_for_initial_user_activity));
 }
@@ -215,7 +219,7 @@
 void SessionLengthLimiterTest::
     UpdateSessionStartTimeIfWaitingForUserActivity() {
   if (!user_activity_seen_ &&
-      local_state_.GetBoolean(prefs::kSessionWaitForInitialUserActivity)) {
+      local_state().GetBoolean(prefs::kSessionWaitForInitialUserActivity)) {
     session_start_time_ = runner_->Now();
   }
 }
diff --git a/chrome/browser/ash/login/smart_lock/smart_lock_service_unittest.cc b/chrome/browser/ash/login/smart_lock/smart_lock_service_unittest.cc
index e1d530b9..79e180bb 100644
--- a/chrome/browser/ash/login/smart_lock/smart_lock_service_unittest.cc
+++ b/chrome/browser/ash/login/smart_lock/smart_lock_service_unittest.cc
@@ -27,6 +27,7 @@
 #include "chrome/browser/signin/identity_test_environment_profile_adaptor.h"
 #include "chrome/browser/ui/webui/ash/multidevice_setup/multidevice_setup_dialog.h"
 #include "chrome/common/pref_names.h"
+#include "chrome/test/base/scoped_testing_local_state.h"
 #include "chrome/test/base/testing_browser_process.h"
 #include "chrome/test/base/testing_profile.h"
 #include "chromeos/ash/components/dbus/dbus_thread_manager.h"
@@ -195,8 +196,6 @@
     mock_adapter_ = new testing::NiceMock<MockBluetoothAdapter>();
     device::BluetoothAdapterFactory::SetAdapterForTesting(mock_adapter_);
 
-    TestingBrowserProcess::GetGlobal()->SetLocalState(&local_pref_service_);
-    RegisterLocalState(local_pref_service_.registry());
     fake_user_manager_.Reset(std::make_unique<ash::FakeChromeUserManager>());
 
     auto test_other_remote_device =
@@ -231,7 +230,6 @@
     SetScreenLockState(false /* is_locked */);
     smart_lock_service_->Shutdown();
     chromeos::PowerManagerClient::Shutdown();
-    TestingBrowserProcess::GetGlobal()->SetLocalState(nullptr);
     display::Screen::SetScreenInstance(nullptr);
   }
 
@@ -326,7 +324,8 @@
 
   // PrefService which contains the browser process' local storage. It should be
   // destructed after TestingProfile.
-  TestingPrefServiceSimple local_pref_service_;
+  ScopedTestingLocalState scoped_testing_local_state_{
+      TestingBrowserProcess::GetGlobal()};
 
   user_manager::TypedScopedUserManager<ash::FakeChromeUserManager>
       fake_user_manager_;
diff --git a/chrome/browser/ash/login/startup_utils_unittest.cc b/chrome/browser/ash/login/startup_utils_unittest.cc
index e7cbd1e..330daab 100644
--- a/chrome/browser/ash/login/startup_utils_unittest.cc
+++ b/chrome/browser/ash/login/startup_utils_unittest.cc
@@ -9,6 +9,7 @@
 #include "base/test/task_environment.h"
 #include "chrome/browser/ash/policy/enrollment/enrollment_test_helper.h"
 #include "chrome/browser/prefs/browser_prefs.h"
+#include "chrome/test/base/scoped_testing_local_state.h"
 #include "chrome/test/base/testing_browser_process.h"
 #include "chromeos/ash/components/system/fake_statistics_provider.h"
 #include "components/prefs/testing_pref_service.h"
@@ -18,20 +19,16 @@
 // other utility functions in StartupUtils.
 class StartupUtilsTest : public testing::Test {
  protected:
-  StartupUtilsTest() {
-    RegisterLocalState(fake_local_state_.registry());
-    TestingBrowserProcess::GetGlobal()->SetLocalState(&fake_local_state_);
-  }
-
   ~StartupUtilsTest() override {
     TestingBrowserProcess::GetGlobal()->SetShuttingDown(true);
-    TestingBrowserProcess::GetGlobal()->SetLocalState(nullptr);
   }
 
   base::test::TaskEnvironment task_environment_{
       base::test::TaskEnvironment::TimeSource::MOCK_TIME};
 
-  TestingPrefServiceSimple fake_local_state_;
+  ScopedTestingLocalState scoped_testing_local_state_{
+      TestingBrowserProcess::GetGlobal()};
+
   ash::system::ScopedFakeStatisticsProvider statistics_provider_;
   base::test::ScopedCommandLine command_line_;
   policy::test::EnrollmentTestHelper enrollment_test_helper_{
diff --git a/chrome/browser/ash/magic_boost/BUILD.gn b/chrome/browser/ash/magic_boost/BUILD.gn
index a8cde7c..bdf726e 100644
--- a/chrome/browser/ash/magic_boost/BUILD.gn
+++ b/chrome/browser/ash/magic_boost/BUILD.gn
@@ -23,6 +23,8 @@
     "//ash/constants",
     "//base",
     "//chrome/browser/ash/mahi:mahi_availability",
+    "//chrome/browser/profiles",
+    "//chromeos/ash/components/browser_context_helper",
     "//chromeos/ash/components/editor_menu/public/cpp",
     "//chromeos/components/magic_boost/public/cpp",
     "//chromeos/components/mahi/public/cpp",
@@ -76,6 +78,7 @@
     "//ash:test_support",
     "//ash/constants",
     "//base",
+    "//chrome/browser/profiles:profile",
     "//chromeos/components/magic_boost/public/cpp",
     "//components/sync_preferences:test_support",
     "//testing/gmock",
diff --git a/chrome/browser/ash/magic_boost/DEPS b/chrome/browser/ash/magic_boost/DEPS
index b2bda3f..4ece6713 100644
--- a/chrome/browser/ash/magic_boost/DEPS
+++ b/chrome/browser/ash/magic_boost/DEPS
@@ -25,6 +25,7 @@
   "+chrome/browser/ash/mahi",
 
   "+chrome/browser/profiles",
+  "+chrome/browser/signin",
 ]
 
 specific_include_rules = {
diff --git a/chrome/browser/ash/magic_boost/magic_boost_state_ash.cc b/chrome/browser/ash/magic_boost/magic_boost_state_ash.cc
index c1dd8cf2..4ad7597 100644
--- a/chrome/browser/ash/magic_boost/magic_boost_state_ash.cc
+++ b/chrome/browser/ash/magic_boost/magic_boost_state_ash.cc
@@ -5,24 +5,91 @@
 #include "chrome/browser/ash/magic_boost/magic_boost_state_ash.h"
 
 #include <cstdint>
+#include <memory>
+#include <optional>
 
 #include "ash/constants/ash_pref_names.h"
 #include "ash/session/session_controller_impl.h"
 #include "ash/shell.h"
 #include "ash/system/mahi/mahi_utils.h"
+#include "base/check_is_test.h"
 #include "base/functional/bind.h"
+#include "base/functional/callback_forward.h"
+#include "base/scoped_observation.h"
 #include "base/types/cxx23_to_underlying.h"
+#include "base/types/expected.h"
+#include "base/types/expected_macros.h"
 #include "chrome/browser/ash/input_method/editor_mediator_factory.h"
 #include "chrome/browser/ash/input_method/editor_panel_manager.h"
 #include "chrome/browser/ash/mahi/mahi_availability.h"
+#include "chrome/browser/profiles/profile.h"
 #include "chrome/browser/profiles/profile_manager.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "chromeos/ash/components/browser_context_helper/browser_context_helper.h"
 #include "chromeos/ash/components/editor_menu/public/cpp/editor_context.h"
 #include "chromeos/ash/components/editor_menu/public/cpp/editor_mode.h"
+#include "chromeos/components/magic_boost/public/cpp/magic_boost_state.h"
 #include "components/prefs/pref_service.h"
+#include "components/signin/public/identity_manager/identity_manager.h"
+#include "components/user_manager/user_manager.h"
 
 namespace ash {
+namespace {
 
-MagicBoostStateAsh::MagicBoostStateAsh() {
+base::expected<bool, chromeos::MagicBoostState::Error>
+IsMagicBoostAvailableExpected() {
+  std::optional<bool> availability = mahi_availability::IsMahiAvailable();
+  if (!availability.has_value()) {
+    return base::unexpected(chromeos::MagicBoostState::Error::kUninitialized);
+  }
+  return availability.value();
+}
+
+// Wait for refresh tokens load and run the provided callback. This object
+// immediately runs the callback if refresh tokens are already loaded.
+class RefreshTokensLoadedBarrier : public signin::IdentityManager::Observer {
+ public:
+  RefreshTokensLoadedBarrier(Profile* profile,
+                             signin::IdentityManager* identity_manager,
+                             base::OnceCallback<void()> callback)
+      : callback_(std::move(callback)) {
+    CHECK(profile);
+    CHECK(identity_manager);
+
+    if (identity_manager->AreRefreshTokensLoaded()) {
+      std::move(callback_).Run();
+      return;
+    }
+
+    identity_manager_observation_.Observe(identity_manager);
+  }
+
+  void OnRefreshTokensLoaded() override {
+    std::move(callback_).Run();
+    identity_manager_observation_.Reset();
+  }
+
+ private:
+  base::OnceCallback<void()> callback_;
+  base::ScopedObservation<signin::IdentityManager, RefreshTokensLoadedBarrier>
+      identity_manager_observation_{this};
+};
+
+}  // namespace
+
+MagicBoostStateAsh::MagicBoostStateAsh()
+    : MagicBoostStateAsh(
+          MagicBoostStateAsh::InjectActiveProfileForTestingCallback()) {}
+
+MagicBoostStateAsh::MagicBoostStateAsh(
+    MagicBoostStateAsh::InjectActiveProfileForTestingCallback
+        inject_active_profile_for_testing_callback)
+    : inject_active_profile_for_testing_callback_(
+          inject_active_profile_for_testing_callback) {
+  if (!inject_active_profile_for_testing_callback_.is_null()) {
+    CHECK_IS_TEST();
+  }
+
   shell_observation_.Observe(ash::Shell::Get());
 
   auto* session_controller = ash::Shell::Get()->session_controller();
@@ -42,15 +109,22 @@
   editor_manager_for_test_ = nullptr;
 }
 
+Profile* MagicBoostStateAsh::GetActiveUserProfile() {
+  if (!inject_active_profile_for_testing_callback_.is_null()) {
+    CHECK_IS_TEST();
+    return inject_active_profile_for_testing_callback_.Run();
+  }
+
+  return Profile::FromBrowserContext(
+      ash::BrowserContextHelper::Get()->GetBrowserContextByUser(
+          user_manager::UserManager::Get()->GetActiveUser()));
+}
+
 void MagicBoostStateAsh::OnActiveUserPrefServiceChanged(
     PrefService* pref_service) {
   RegisterPrefChanges(pref_service);
 }
 
-bool MagicBoostStateAsh::IsMagicBoostAvailable() {
-  return mahi_availability::IsMahiAvailable();
-}
-
 bool MagicBoostStateAsh::CanShowNoticeBannerForHMR() {
   PrefService* pref = pref_change_registrar_->prefs();
 
@@ -124,7 +198,7 @@
   }
 
   return input_method::EditorMediatorFactory::GetInstance()
-      ->GetForProfile(ProfileManager::GetActiveUserProfile())
+      ->GetForProfile(GetActiveUserProfile())
       ->panel_manager();
 }
 
@@ -170,6 +244,38 @@
   OnHMREnabledUpdated();
   OnHMRConsentStatusUpdated();
   OnHMRConsentWindowDismissCountUpdated();
+
+  Profile* profile = GetActiveUserProfile();
+  if (!profile) {
+    CHECK_IS_TEST();
+    // Test code can bypass availability check by a flag. Run check immediately
+    // for that case if `profile` is nullptr.
+    OnRefreshTokensReady();
+    return;
+  }
+
+  signin::IdentityManager* identity_manager =
+      IdentityManagerFactory::GetForProfile(profile);
+  if (!identity_manager) {
+    // `identity_manager` is not available under a certain condition, e.g.,
+    // guest session, test code. Run check immediately for those cases.
+    OnRefreshTokensReady();
+    return;
+  }
+
+  // Availability check contains an async operation where value is unavailable
+  // until refresh tokens are loaded. Run availability check after refresh token
+  // is loaded.
+  refresh_tokens_loaded_barrier_.reset(new RefreshTokensLoadedBarrier(
+      profile, identity_manager,
+      base::BindOnce(&MagicBoostStateAsh::OnRefreshTokensReady,
+                     base::Unretained(this))));
+}
+
+void MagicBoostStateAsh::OnRefreshTokensReady() {
+  ASSIGN_OR_RETURN(bool available, IsMagicBoostAvailableExpected(),
+                   [](auto) {});
+  UpdateMagicBoostAvailable(available);
 }
 
 void MagicBoostStateAsh::OnMagicBoostEnabledUpdated() {
diff --git a/chrome/browser/ash/magic_boost/magic_boost_state_ash.h b/chrome/browser/ash/magic_boost/magic_boost_state_ash.h
index f321fff..8276214 100644
--- a/chrome/browser/ash/magic_boost/magic_boost_state_ash.h
+++ b/chrome/browser/ash/magic_boost/magic_boost_state_ash.h
@@ -5,11 +5,17 @@
 #ifndef CHROME_BROWSER_ASH_MAGIC_BOOST_MAGIC_BOOST_STATE_ASH_H_
 #define CHROME_BROWSER_ASH_MAGIC_BOOST_MAGIC_BOOST_STATE_ASH_H_
 
+#include <memory>
+
 #include "ash/public/cpp/session/session_observer.h"
 #include "ash/shell_observer.h"
+#include "base/functional/callback_forward.h"
 #include "base/scoped_observation.h"
+#include "base/types/expected.h"
+#include "chrome/browser/profiles/profile.h"
 #include "chromeos/components/magic_boost/public/cpp/magic_boost_state.h"
 #include "components/prefs/pref_change_registrar.h"
+#include "components/signin/public/identity_manager/identity_manager.h"
 
 namespace ash {
 
@@ -25,7 +31,12 @@
                            public ash::SessionObserver,
                            public ash::ShellObserver {
  public:
+  using InjectActiveProfileForTestingCallback =
+      base::RepeatingCallback<Profile*()>;
+
   MagicBoostStateAsh();
+  explicit MagicBoostStateAsh(InjectActiveProfileForTestingCallback
+                                  inject_active_profile_for_testing_callback);
 
   MagicBoostStateAsh(const MagicBoostStateAsh&) = delete;
   MagicBoostStateAsh& operator=(const MagicBoostStateAsh&) = delete;
@@ -33,7 +44,6 @@
   ~MagicBoostStateAsh() override;
 
   // MagicBoostState:
-  bool IsMagicBoostAvailable() override;
   bool CanShowNoticeBannerForHMR() override;
   int32_t AsyncIncrementHMRConsentWindowDismissCount() override;
   void AsyncWriteConsentStatus(
@@ -58,6 +68,8 @@
  private:
   friend class MagicBoostStateAshTest;
 
+  Profile* GetActiveUserProfile();
+
   // ash::SessionObserver:
   void OnActiveUserPrefServiceChanged(PrefService* pref_service) override;
 
@@ -73,9 +85,16 @@
   void OnHMRConsentStatusUpdated();
   void OnHMRConsentWindowDismissCountUpdated();
 
+  void OnRefreshTokensReady();
+
   // Observes user profile prefs for magic_boost.
   std::unique_ptr<PrefChangeRegistrar> pref_change_registrar_;
 
+  // Use `std::unique_ptr` instead of `std::optional` as this variable holds an
+  // extended class of `signin::IdentityManager::Observer` class.
+  std::unique_ptr<signin::IdentityManager::Observer>
+      refresh_tokens_loaded_barrier_;
+
   base::ScopedObservation<ash::SessionController, ash::SessionObserver>
       session_observation_{this};
 
@@ -83,6 +102,9 @@
 
   base::ScopedObservation<ash::Shell, ash::ShellObserver> shell_observation_{
       this};
+
+  const InjectActiveProfileForTestingCallback
+      inject_active_profile_for_testing_callback_;
 };
 
 }  // namespace ash
diff --git a/chrome/browser/ash/magic_boost/magic_boost_state_ash_unittest.cc b/chrome/browser/ash/magic_boost/magic_boost_state_ash_unittest.cc
index a3b0a70..9d158ab8 100644
--- a/chrome/browser/ash/magic_boost/magic_boost_state_ash_unittest.cc
+++ b/chrome/browser/ash/magic_boost/magic_boost_state_ash_unittest.cc
@@ -4,14 +4,18 @@
 
 #include "chrome/browser/ash/magic_boost/magic_boost_state_ash.h"
 
+#include <memory>
+
 #include "ash/constants/ash_pref_names.h"
 #include "ash/session/session_controller_impl.h"
 #include "ash/shell.h"
 #include "ash/test/ash_test_base.h"
+#include "base/functional/bind.h"
 #include "base/test/scoped_feature_list.h"
 #include "base/types/cxx23_to_underlying.h"
 #include "base/values.h"
 #include "chrome/browser/ash/magic_boost/mock_editor_panel_manager.h"
+#include "chrome/browser/profiles/profile.h"
 #include "chromeos/components/magic_boost/public/cpp/magic_boost_state.h"
 #include "chromeos/constants/chromeos_features.h"
 #include "components/sync_preferences/testing_pref_service_syncable.h"
@@ -72,7 +76,8 @@
     prefs_ = static_cast<TestingPrefServiceSimple*>(
         ash::Shell::Get()->session_controller()->GetPrimaryUserPrefService());
 
-    magic_boost_state_ = std::make_unique<MagicBoostStateAsh>();
+    magic_boost_state_ = std::make_unique<MagicBoostStateAsh>(
+        base::BindRepeating([]() { return static_cast<Profile*>(nullptr); }));
     magic_boost_state_->set_editor_panel_manager_for_test(
         &mock_editor_manager_);
 
diff --git a/chrome/browser/ash/magic_boost/mock_magic_boost_state.cc b/chrome/browser/ash/magic_boost/mock_magic_boost_state.cc
index a04f694c..a119e39 100644
--- a/chrome/browser/ash/magic_boost/mock_magic_boost_state.cc
+++ b/chrome/browser/ash/magic_boost/mock_magic_boost_state.cc
@@ -4,9 +4,14 @@
 
 #include "chrome/browser/ash/magic_boost/mock_magic_boost_state.h"
 
+#include "base/functional/bind.h"
+#include "chrome/browser/ash/magic_boost/magic_boost_state_ash.h"
+
 namespace ash {
 
-MockMagicBoostState::MockMagicBoostState() = default;
+MockMagicBoostState::MockMagicBoostState()
+    : MagicBoostStateAsh(base::BindRepeating(
+          []() { return static_cast<Profile*>(nullptr); })) {}
 
 MockMagicBoostState::~MockMagicBoostState() = default;
 
diff --git a/chrome/browser/ash/mahi/BUILD.gn b/chrome/browser/ash/mahi/BUILD.gn
index a02ec14..cead4a71 100644
--- a/chrome/browser/ash/mahi/BUILD.gn
+++ b/chrome/browser/ash/mahi/BUILD.gn
@@ -77,6 +77,7 @@
     "//chrome/browser/ash/magic_boost:magic_boost",
     "//chrome/browser/ash/mahi/media_app:unit_tests",
     "//chrome/browser/ash/mahi/web_contents/test_support",
+    "//chrome/browser/profiles:profile",
     "//chromeos/components/magic_boost/public/cpp:cpp",
     "//chromeos/components/mahi/public/cpp",
     "//chromeos/constants",
diff --git a/chrome/browser/ash/mahi/mahi_availability.cc b/chrome/browser/ash/mahi/mahi_availability.cc
index b7010152..66e7286 100644
--- a/chrome/browser/ash/mahi/mahi_availability.cc
+++ b/chrome/browser/ash/mahi/mahi_availability.cc
@@ -21,7 +21,7 @@
 
 namespace ash::mahi_availability {
 
-bool CanUseMahiService() {
+std::optional<bool> CanUseMahiService() {
   if (!manta::features::IsMantaServiceEnabled()) {
     return false;
   }
@@ -56,9 +56,15 @@
     // MantaService might not be available in tests.
     if (manta::MantaService* service =
             manta::MantaServiceFactory::GetForProfile(profile);
-        service && service->CanAccessMantaFeaturesWithoutMinorRestrictions() !=
-                       manta::FeatureSupportStatus::kSupported) {
-      return false;
+        service) {
+      switch (service->CanAccessMantaFeaturesWithoutMinorRestrictions()) {
+        case manta::FeatureSupportStatus::kSupported:
+          break;
+        case manta::FeatureSupportStatus::kUnsupported:
+          return false;
+        case manta::FeatureSupportStatus::kUnknown:
+          return std::nullopt;
+      }
     }
   }
 
@@ -71,8 +77,12 @@
   return IsGenerativeAiAllowedForCountry(country_code);
 }
 
-bool IsMahiAvailable() {
-  return chromeos::features::IsMahiEnabled() && CanUseMahiService();
+std::optional<bool> IsMahiAvailable() {
+  if (!chromeos::features::IsMahiEnabled()) {
+    return false;
+  }
+
+  return CanUseMahiService();
 }
 
 bool IsPompanoAvailable() {
diff --git a/chrome/browser/ash/mahi/mahi_availability.h b/chrome/browser/ash/mahi/mahi_availability.h
index c2e9a7c..b3cb117e 100644
--- a/chrome/browser/ash/mahi/mahi_availability.h
+++ b/chrome/browser/ash/mahi/mahi_availability.h
@@ -5,18 +5,27 @@
 #ifndef CHROME_BROWSER_ASH_MAHI_MAHI_AVAILABILITY_H_
 #define CHROME_BROWSER_ASH_MAHI_MAHI_AVAILABILITY_H_
 
+#include <optional>
+
 namespace ash::mahi_availability {
 
 // Check whether Mahi is allowed. This function checks following restrictions:
 //   * age: if not demo mode, the account must not hit minor restrictions
 //   * country: the country code must be in the allow list.
 //   * If not in demo mode, guest session is not allowed.
-bool CanUseMahiService();
+//
+// This check reads a bit loaded as an async operation:
+// `CanAccessMantaFeaturesWithoutMinorRestrictions`. `std::nullopt` is returned
+// if the bit is not ready yet.
+std::optional<bool> CanUseMahiService();
 
 // Check if the mahi feature is available to use. It can be unavailable if the
 // mahi feature flag is disabled, or the age and country requirements are not
 // met.
-bool IsMahiAvailable();
+//
+// This check reads a bit loaded as an async operation via `CanUseMahiService`.
+// `std::nullopt` is returned if the bit is not ready yet.
+std::optional<bool> IsMahiAvailable();
 
 // Check if the Pompano feature is available to use.
 // Pompano is an add-on feature of mahi. Currently we make it available only
diff --git a/chrome/browser/ash/mahi/mahi_manager_impl_unittest.cc b/chrome/browser/ash/mahi/mahi_manager_impl_unittest.cc
index 5d43740..37e76218 100644
--- a/chrome/browser/ash/mahi/mahi_manager_impl_unittest.cc
+++ b/chrome/browser/ash/mahi/mahi_manager_impl_unittest.cc
@@ -17,6 +17,7 @@
 #include "ash/test/ash_test_base.h"
 #include "base/auto_reset.h"
 #include "base/command_line.h"
+#include "base/functional/bind.h"
 #include "base/functional/callback_helpers.h"
 #include "base/strings/utf_string_conversions.h"
 #include "base/test/gmock_callback_support.h"
@@ -26,6 +27,7 @@
 #include "chrome/browser/ash/magic_boost/magic_boost_state_ash.h"
 #include "chrome/browser/ash/mahi/mahi_cache_manager.h"
 #include "chrome/browser/ash/mahi/web_contents/test_support/fake_mahi_web_contents_manager.h"
+#include "chrome/browser/profiles/profile.h"
 #include "chromeos/components/magic_boost/public/cpp/magic_boost_state.h"
 #include "chromeos/components/mahi/public/cpp/mahi_media_app_content_manager.h"
 #include "chromeos/components/mahi/public/cpp/mahi_web_contents_manager.h"
@@ -145,7 +147,8 @@
     base::CommandLine::ForCurrentProcess()->AppendSwitch(
         chromeos::switches::kMahiRestrictionsOverride);
 
-    magic_boost_state_ = std::make_unique<MagicBoostStateAsh>();
+    magic_boost_state_ = std::make_unique<MagicBoostStateAsh>(
+        base::BindRepeating([]() { return static_cast<Profile*>(nullptr); }));
     mahi_manager_impl_ = std::make_unique<MahiManagerImpl>();
     mahi_manager_impl_->mahi_provider_ = CreateMahiProvider();
 
diff --git a/chrome/browser/ash/policy/core/device_cloud_policy_manager_ash_unittest.cc b/chrome/browser/ash/policy/core/device_cloud_policy_manager_ash_unittest.cc
index 54c50b2..f32a59c 100644
--- a/chrome/browser/ash/policy/core/device_cloud_policy_manager_ash_unittest.cc
+++ b/chrome/browser/ash/policy/core/device_cloud_policy_manager_ash_unittest.cc
@@ -40,6 +40,7 @@
 #include "chrome/browser/device_identity/device_oauth2_token_service_factory.h"
 #include "chrome/browser/policy/messaging_layer/public/report_client_test_util.h"
 #include "chrome/browser/prefs/browser_prefs.h"
+#include "chrome/test/base/scoped_testing_local_state.h"
 #include "chrome/test/base/testing_browser_process.h"
 #include "chrome/test/base/testing_profile.h"
 #include "chromeos/ash/components/attestation/fake_certificate.h"
@@ -225,24 +226,23 @@
         base::WrapUnique(store_.get()), std::move(external_data_manager),
         base::SingleThreadTaskRunner::GetCurrentDefault(), &state_keys_broker_);
 
-    RegisterLocalState(local_state_.registry());
     manager_->Init(&schema_registry_);
     manager_->SetSigninProfileSchemaRegistry(&schema_registry_);
 
-    user_manager_ =
-        std::make_unique<user_manager::FakeUserManager>(&local_state_);
+    user_manager_ = std::make_unique<user_manager::FakeUserManager>(
+        scoped_testing_local_state_.Get());
     manager_->OnUserManagerCreated(user_manager_.get());
 
     // SharedURLLoaderFactory and LocalState singletons have to be set since
     // they are accessed by EnrollmentHandler and StartupUtils.
     TestingBrowserProcess::GetGlobal()->SetSharedURLLoaderFactory(
         test_url_loader_factory_.GetSafeWeakWrapper());
-    TestingBrowserProcess::GetGlobal()->SetLocalState(&local_state_);
 
     // SystemSaltGetter is used in DeviceOAuth2TokenService.
     ash::SystemSaltGetter::Initialize();
     DeviceOAuth2TokenServiceFactory::Initialize(
-        test_url_loader_factory_.GetSafeWeakWrapper(), &local_state_);
+        test_url_loader_factory_.GetSafeWeakWrapper(),
+        scoped_testing_local_state_.Get());
 
     url_fetcher_response_code_ = net::HTTP_OK;
     url_fetcher_response_string_ =
@@ -275,7 +275,6 @@
     DeviceOAuth2TokenServiceFactory::Shutdown();
     ash::SystemSaltGetter::Shutdown();
     ash::InstallAttributesClient::Shutdown();
-    TestingBrowserProcess::GetGlobal()->SetLocalState(nullptr);
 
     reporting_test_enviroment_.reset();
 
@@ -315,7 +314,7 @@
   }
 
   void InitDeviceCloudPolicyInitializer() {
-    manager_->Initialize(&local_state_);
+    manager_->Initialize(scoped_testing_local_state_.Get());
     EnrollmentRequisitionManager::Initialize();
     initializer_ = std::make_unique<DeviceCloudPolicyInitializer>(
         &device_management_service_, install_attributes_.get(),
@@ -374,11 +373,13 @@
   std::unique_ptr<reporting::ReportingClient::TestEnvironment>
       reporting_test_enviroment_;
 
+  ScopedTestingLocalState scoped_testing_local_state_{
+      TestingBrowserProcess::GetGlobal()};
+
   std::unique_ptr<ash::InstallAttributes> install_attributes_;
 
   net::HttpStatusCode url_fetcher_response_code_;
   std::string url_fetcher_response_string_;
-  TestingPrefServiceSimple local_state_;
   std::unique_ptr<user_manager::FakeUserManager> user_manager_;
   StrictMock<MockJobCreationHandler> job_creation_handler_;
   FakeDeviceManagementService device_management_service_{
@@ -411,7 +412,7 @@
   FlushDeviceSettings();
   EXPECT_TRUE(manager_->IsInitializationComplete(POLICY_DOMAIN_CHROME));
 
-  manager_->Initialize(&local_state_);
+  manager_->Initialize(scoped_testing_local_state_.Get());
 
   PolicyBundle bundle;
   EXPECT_TRUE(manager_->policies().Equals(bundle));
diff --git a/chrome/browser/ash/policy/reporting/arc_app_install_encrypted_event_reporter_unittest.cc b/chrome/browser/ash/policy/reporting/arc_app_install_encrypted_event_reporter_unittest.cc
index 72d4d4bc..aeb4f7e 100644
--- a/chrome/browser/ash/policy/reporting/arc_app_install_encrypted_event_reporter_unittest.cc
+++ b/chrome/browser/ash/policy/reporting/arc_app_install_encrypted_event_reporter_unittest.cc
@@ -21,6 +21,7 @@
 #include "chrome/browser/prefs/browser_prefs.h"
 #include "chrome/browser/profiles/profile.h"
 #include "chrome/common/pref_names.h"
+#include "chrome/test/base/scoped_testing_local_state.h"
 #include "chrome/test/base/testing_browser_process.h"
 #include "chrome/test/base/testing_profile.h"
 #include "chromeos/ash/components/network/network_handler_test_helper.h"
@@ -52,15 +53,11 @@
 
   void SetUp() override {
     ash::system::StatisticsProvider::SetTestProvider(&statistics_provider_);
-
-    RegisterLocalState(pref_service_.registry());
-    TestingBrowserProcess::GetGlobal()->SetLocalState(&pref_service_);
     chromeos::PowerManagerClient::InitializeFake();
   }
 
   void TearDown() override {
     task_environment_.RunUntilIdle();
-    TestingBrowserProcess::GetGlobal()->SetLocalState(nullptr);
     chromeos::PowerManagerClient::Shutdown();
   }
 
@@ -72,7 +69,8 @@
   }
 
   content::BrowserTaskEnvironment task_environment_;
-  TestingPrefServiceSimple pref_service_;
+  ScopedTestingLocalState scoped_testing_local_state_{
+      TestingBrowserProcess::GetGlobal()};
   TestingProfile profile_;
   ash::system::FakeStatisticsProvider statistics_provider_;
 };
diff --git a/chrome/browser/ash/policy/reporting/arc_app_install_event_log_collector_unittest.cc b/chrome/browser/ash/policy/reporting/arc_app_install_event_log_collector_unittest.cc
index cc9c66e..042d1815 100644
--- a/chrome/browser/ash/policy/reporting/arc_app_install_event_log_collector_unittest.cc
+++ b/chrome/browser/ash/policy/reporting/arc_app_install_event_log_collector_unittest.cc
@@ -15,6 +15,7 @@
 #include "chrome/browser/browser_process.h"
 #include "chrome/browser/prefs/browser_prefs.h"
 #include "chrome/common/pref_names.h"
+#include "chrome/test/base/scoped_testing_local_state.h"
 #include "chrome/test/base/testing_browser_process.h"
 #include "chrome/test/base/testing_profile.h"
 #include "chromeos/ash/components/dbus/shill/shill_service_client.h"
@@ -141,9 +142,6 @@
   ~ArcAppInstallEventLogCollectorTest() override = default;
 
   void SetUp() override {
-    RegisterLocalState(pref_service_.registry());
-    TestingBrowserProcess::GetGlobal()->SetLocalState(&pref_service_);
-
     chromeos::PowerManagerClient::InitializeFake();
     profile_ = std::make_unique<TestingProfile>();
     arc_app_test_.SetUp(profile_.get());
@@ -165,7 +163,6 @@
 
     profile_.reset();
     chromeos::PowerManagerClient::Shutdown();
-    TestingBrowserProcess::GetGlobal()->SetLocalState(nullptr);
   }
 
   void SetNetworkState(
@@ -204,10 +201,11 @@
 
  private:
   content::BrowserTaskEnvironment task_environment_;
+  ScopedTestingLocalState scoped_testing_local_state_{
+      TestingBrowserProcess::GetGlobal()};
   std::unique_ptr<ash::NetworkHandlerTestHelper> network_handler_test_helper_;
   std::unique_ptr<TestingProfile> profile_;
   FakeAppInstallEventLogCollectorDelegate delegate_;
-  TestingPrefServiceSimple pref_service_;
   ArcAppTest arc_app_test_;
 };
 
diff --git a/chrome/browser/ash/policy/reporting/arc_app_install_event_logger_unittest.cc b/chrome/browser/ash/policy/reporting/arc_app_install_event_logger_unittest.cc
index 6054d74d..514bdc1 100644
--- a/chrome/browser/ash/policy/reporting/arc_app_install_event_logger_unittest.cc
+++ b/chrome/browser/ash/policy/reporting/arc_app_install_event_logger_unittest.cc
@@ -12,6 +12,7 @@
 #include "base/values.h"
 #include "chrome/browser/prefs/browser_prefs.h"
 #include "chrome/common/pref_names.h"
+#include "chrome/test/base/scoped_testing_local_state.h"
 #include "chrome/test/base/testing_browser_process.h"
 #include "chrome/test/base/testing_profile.h"
 #include "chromeos/ash/components/dbus/cros_disks/cros_disks_client.h"
@@ -185,9 +186,6 @@
       delete;
 
   void SetUp() override {
-    RegisterLocalState(pref_service_.registry());
-    TestingBrowserProcess::GetGlobal()->SetLocalState(&pref_service_);
-
     chromeos::PowerManagerClient::InitializeFake();
   }
 
@@ -195,7 +193,6 @@
     logger_.reset();
     task_environment_.RunUntilIdle();
     chromeos::PowerManagerClient::Shutdown();
-    TestingBrowserProcess::GetGlobal()->SetLocalState(nullptr);
   }
 
   // Runs |function|, verifies that the expected event is added to the logs for
@@ -282,8 +279,9 @@
   }
 
   content::BrowserTaskEnvironment task_environment_;
+  ScopedTestingLocalState scoped_testing_local_state_{
+      TestingBrowserProcess::GetGlobal()};
   ash::NetworkHandlerTestHelper network_handler_test_helper_;
-  TestingPrefServiceSimple pref_service_;
   TestingProfile profile_;
 
   MockAppInstallEventLoggerDelegate delegate_;
diff --git a/chrome/browser/ash/policy/status_collector/child_status_collector_browsertest.cc b/chrome/browser/ash/policy/status_collector/child_status_collector_browsertest.cc
index 470a522..218b6ba 100644
--- a/chrome/browser/ash/policy/status_collector/child_status_collector_browsertest.cc
+++ b/chrome/browser/ash/policy/status_collector/child_status_collector_browsertest.cc
@@ -45,6 +45,7 @@
 #include "chrome/common/chrome_paths.h"
 #include "chrome/common/pref_names.h"
 #include "chrome/test/base/chrome_unit_test_suite.h"
+#include "chrome/test/base/scoped_testing_local_state.h"
 #include "chrome/test/base/testing_browser_process.h"
 #include "chrome/test/base/testing_profile_manager.h"
 #include "chromeos/ash/components/dbus/cicerone/cicerone_client.h"
@@ -201,8 +202,6 @@
     std::unique_ptr<base::Environment> env(base::Environment::Create());
     env->SetVar("TZ", "UTC");
 
-    TestingBrowserProcess::GetGlobal()->SetLocalState(&local_state_);
-
     // Use FakeUpdateEngineClient.
     ash::UpdateEngineClient::InitializeFakeForTest();
     ash::CiceroneClient::InitializeFake();
@@ -223,7 +222,6 @@
     ash::ConciergeClient::Shutdown();
     ash::CiceroneClient::Shutdown();
     ash::UpdateEngineClient::Shutdown();
-    TestingBrowserProcess::GetGlobal()->SetLocalState(nullptr);
 
     // Finish pending tasks.
     content::RunAllTasksUntilIdle();
@@ -423,6 +421,10 @@
   content::BrowserTaskEnvironment task_environment_{
       base::test::TaskEnvironment::TimeSource::MOCK_TIME};
 
+  // scoped_testing_local_state_ should be destructed after TestingProfile.
+  ScopedTestingLocalState scoped_testing_local_state_{
+      TestingBrowserProcess::GetGlobal()};
+
   ChromeContentClient content_client_;
   ChromeContentBrowserClient browser_content_client_;
   ash::system::ScopedFakeStatisticsProvider fake_statistics_provider_;
@@ -430,8 +432,6 @@
   ash::ScopedTestingCrosSettings scoped_testing_cros_settings_;
   ash::FakeOwnerSettingsService owner_settings_service_{
       scoped_testing_cros_settings_.device_settings(), nullptr};
-  // local_state_ should be destructed after TestingProfile.
-  TestingPrefServiceSimple local_state_;
   std::unique_ptr<TestingProfile> testing_profile_;
   user_manager::ScopedUserManager user_manager_enabler_;
   em::ChildStatusReportRequest child_status_;
diff --git a/chrome/browser/ash/policy/uploading/system_log_uploader_unittest.cc b/chrome/browser/ash/policy/uploading/system_log_uploader_unittest.cc
index 643ff34..fb009a22 100644
--- a/chrome/browser/ash/policy/uploading/system_log_uploader_unittest.cc
+++ b/chrome/browser/ash/policy/uploading/system_log_uploader_unittest.cc
@@ -17,6 +17,7 @@
 #include "chrome/browser/ash/settings/scoped_cros_settings_test_helper.h"
 #include "chrome/browser/prefs/browser_prefs.h"
 #include "chrome/common/chrome_features.h"
+#include "chrome/test/base/scoped_testing_local_state.h"
 #include "chrome/test/base/testing_browser_process.h"
 #include "components/policy/core/common/remote_commands/remote_command_job.h"
 #include "components/prefs/testing_pref_service.h"
@@ -182,17 +183,13 @@
 
 class SystemLogUploaderTest : public testing::TestWithParam<bool> {
  public:
-  TestingPrefServiceSimple local_state_;
   SystemLogUploaderTest() : task_runner_(new base::TestSimpleTaskRunner()) {}
 
   void SetUp() override {
-    RegisterLocalState(local_state_.registry());
-    TestingBrowserProcess::GetGlobal()->SetLocalState(&local_state_);
     settings_helper_.ReplaceDeviceSettingsProviderWithStub();
   }
 
   void TearDown() override {
-    TestingBrowserProcess::GetGlobal()->SetLocalState(nullptr);
     settings_helper_.RestoreRealDeviceSettingsProvider();
     content::RunAllTasksUntilIdle();
   }
@@ -223,6 +220,8 @@
 
  protected:
   content::BrowserTaskEnvironment task_environment_;
+  ScopedTestingLocalState scoped_testing_local_state_{
+      TestingBrowserProcess::GetGlobal()};
   ash::ScopedCrosSettingsTestHelper settings_helper_;
   scoped_refptr<base::TestSimpleTaskRunner> task_runner_;
   base::test::ScopedFeatureList feature_list;
diff --git a/chrome/browser/ash/system/automatic_reboot_manager_unittest.cc b/chrome/browser/ash/system/automatic_reboot_manager_unittest.cc
index ccffb03..ff193fe 100644
--- a/chrome/browser/ash/system/automatic_reboot_manager_unittest.cc
+++ b/chrome/browser/ash/system/automatic_reboot_manager_unittest.cc
@@ -30,6 +30,7 @@
 #include "chrome/browser/ash/login/users/fake_chrome_user_manager.h"
 #include "chrome/browser/ash/system/automatic_reboot_manager_observer.h"
 #include "chrome/common/pref_names.h"
+#include "chrome/test/base/scoped_testing_local_state.h"
 #include "chrome/test/base/testing_browser_process.h"
 #include "chromeos/ash/components/dbus/update_engine/fake_update_engine_client.h"
 #include "chromeos/dbus/power/fake_power_manager_client.h"
@@ -224,7 +225,8 @@
   base::SingleThreadTaskRunner::CurrentHandleOverrideForTesting
       single_thread_task_runner_current_default_handle_override_;
 
-  TestingPrefServiceSimple local_state_;
+  ScopedTestingLocalState scoped_testing_local_state_{
+      TestingBrowserProcess::GetGlobal()};
   user_manager::TypedScopedUserManager<ash::FakeChromeUserManager>
       user_manager_;
   session_manager::SessionManager session_manager_;
@@ -343,9 +345,6 @@
                                        /*is_absolute=*/false,
                                        /*create=*/false);
 
-  TestingBrowserProcess::GetGlobal()->SetLocalState(&local_state_);
-  AutomaticRebootManager::RegisterPrefs(local_state_.registry());
-
   update_engine_client_ = UpdateEngineClient::InitializeFakeForTest();
   chromeos::PowerManagerClient::InitializeFake();
 }
@@ -365,7 +364,6 @@
 
   chromeos::PowerManagerClient::Shutdown();
   UpdateEngineClient::Shutdown();
-  TestingBrowserProcess::GetGlobal()->SetLocalState(nullptr);
 }
 
 void AutomaticRebootManagerBasicTest::SetUpdateRebootNeededUptime(
@@ -378,7 +376,7 @@
     bool reboot_after_update,
     bool expect_reboot) {
   reboot_after_update_ = reboot_after_update;
-  local_state_.SetManagedPref(
+  scoped_testing_local_state_.Get()->SetManagedPref(
       prefs::kRebootAfterUpdate,
       std::make_unique<base::Value>(reboot_after_update));
   task_runner_->RunUntilIdle();
@@ -392,9 +390,9 @@
     bool expect_reboot) {
   uptime_limit_ = limit;
   if (limit.is_zero()) {
-    local_state_.RemoveManagedPref(prefs::kUptimeLimit);
+    scoped_testing_local_state_.Get()->RemoveManagedPref(prefs::kUptimeLimit);
   } else {
-    local_state_.SetManagedPref(
+    scoped_testing_local_state_.Get()->SetManagedPref(
         prefs::kUptimeLimit,
         std::make_unique<base::Value>(static_cast<int>(limit.InSeconds())));
   }
diff --git a/chrome/browser/ash/system/device_disabling_manager_unittest.cc b/chrome/browser/ash/system/device_disabling_manager_unittest.cc
index 035f2c30..7a1e1d8 100644
--- a/chrome/browser/ash/system/device_disabling_manager_unittest.cc
+++ b/chrome/browser/ash/system/device_disabling_manager_unittest.cc
@@ -20,6 +20,7 @@
 #include "chrome/browser/ash/settings/scoped_cros_settings_test_helper.h"
 #include "chrome/browser/browser_process_platform_part.h"
 #include "chrome/common/pref_names.h"
+#include "chrome/test/base/scoped_testing_local_state.h"
 #include "chrome/test/base/testing_browser_process.h"
 #include "chromeos/ash/components/dbus/session_manager/fake_session_manager_client.h"
 #include "chromeos/ash/components/system/fake_statistics_provider.h"
@@ -146,7 +147,8 @@
  private:
   void OnDeviceDisabledChecked(bool device_disabled);
 
-  TestingPrefServiceSimple local_state_;
+  ScopedTestingLocalState scoped_testing_local_state_{
+      TestingBrowserProcess::GetGlobal()};
   FakeStatisticsProvider statistics_provider_;
 
   base::RunLoop run_loop_;
@@ -159,15 +161,12 @@
 }
 
 void DeviceDisablingManagerOOBETest::SetUp() {
-  TestingBrowserProcess::GetGlobal()->SetLocalState(&local_state_);
-  policy::DeviceCloudPolicyManagerAsh::RegisterPrefs(local_state_.registry());
   CreateDeviceDisablingManager();
   StatisticsProvider::SetTestProvider(&statistics_provider_);
 }
 
 void DeviceDisablingManagerOOBETest::TearDown() {
   DeviceDisablingManagerTestBase::TearDown();
-  TestingBrowserProcess::GetGlobal()->SetLocalState(nullptr);
 }
 
 void DeviceDisablingManagerOOBETest::CheckWhetherDeviceDisabledDuringOOBE() {
@@ -178,7 +177,8 @@
 }
 
 void DeviceDisablingManagerOOBETest::SetDeviceDisabled(bool disabled) {
-  ScopedDictPrefUpdate dict(&local_state_, prefs::kServerBackedDeviceState);
+  ScopedDictPrefUpdate dict(scoped_testing_local_state_.Get(),
+                            prefs::kServerBackedDeviceState);
   if (disabled) {
     dict->Set(policy::kDeviceStateMode, policy::kDeviceStateModeDisabled);
   } else {
diff --git a/chrome/browser/ash/tether/tether_service_unittest.cc b/chrome/browser/ash/tether/tether_service_unittest.cc
index 02f8efc0..a2565eca 100644
--- a/chrome/browser/ash/tether/tether_service_unittest.cc
+++ b/chrome/browser/ash/tether/tether_service_unittest.cc
@@ -18,6 +18,7 @@
 #include "chrome/browser/ash/login/users/fake_chrome_user_manager.h"
 #include "chrome/browser/prefs/browser_prefs.h"
 #include "chrome/browser/ui/ash/network/tether_notification_presenter.h"
+#include "chrome/test/base/scoped_testing_local_state.h"
 #include "chrome/test/base/testing_browser_process.h"
 #include "chrome/test/base/testing_profile.h"
 #include "chromeos/ash/components/dbus/shill/shill_device_client.h"
@@ -344,14 +345,9 @@
         base::WrapUnique(new FakeTetherHostFetcherFactory(test_device_));
     TetherHostFetcherImpl::Factory::SetFactoryForTesting(
         fake_tether_host_fetcher_factory_.get());
-
-    TestingBrowserProcess::GetGlobal()->SetLocalState(&local_pref_service_);
-    RegisterLocalState(local_pref_service_.registry());
   }
 
   void TearDown() override {
-    TestingBrowserProcess::GetGlobal()->SetLocalState(nullptr);
-
     device_sync::DeviceSyncClientImpl::Factory::SetFactoryForTesting(nullptr);
     secure_channel::SecureChannelClientImpl::Factory::SetFactoryForTesting(
         nullptr);
@@ -547,8 +543,8 @@
   bool is_adapter_powered_;
   bool shutdown_reason_verified_;
 
-  // PrefService which contains the browser process' local storage.
-  TestingPrefServiceSimple local_pref_service_;
+  ScopedTestingLocalState scoped_testing_local_state_{
+      TestingBrowserProcess::GetGlobal()};
 
   std::unique_ptr<TestTetherService> tether_service_;
   std::unique_ptr<TestingProfile> profile_;
diff --git a/chrome/browser/background/glic/glic_launcher_configuration_unittest.cc b/chrome/browser/background/glic/glic_launcher_configuration_unittest.cc
index 9a507907..86186cb1 100644
--- a/chrome/browser/background/glic/glic_launcher_configuration_unittest.cc
+++ b/chrome/browser/background/glic/glic_launcher_configuration_unittest.cc
@@ -7,6 +7,7 @@
 #include "base/test/task_environment.h"
 #include "chrome/browser/glic/glic_pref_names.h"
 #include "chrome/common/chrome_features.h"
+#include "chrome/test/base/scoped_testing_local_state.h"
 #include "chrome/test/base/testing_browser_process.h"
 #include "components/prefs/testing_pref_service.h"
 #include "content/public/test/browser_task_environment.h"
@@ -33,21 +34,13 @@
   GlicLauncherConfigurationTest() = default;
   ~GlicLauncherConfigurationTest() override = default;
 
-  void SetUp() override {
-    prefs::RegisterLocalStatePrefs(local_state_.registry());
-    TestingBrowserProcess::GetGlobal()->SetLocalState(local_state());
-  }
-
-  void TearDown() override {
-    TestingBrowserProcess::GetGlobal()->SetLocalState(nullptr);
-  }
-
-  PrefService* local_state() { return &local_state_; }
+  PrefService* local_state() { return scoped_testing_local_state_.Get(); }
 
  private:
   content::BrowserTaskEnvironment task_environment_{
       base::test::TaskEnvironment::MainThreadType::UI};
-  TestingPrefServiceSimple local_state_;
+  ScopedTestingLocalState scoped_testing_local_state_{
+      TestingBrowserProcess::GetGlobal()};
 };
 
 TEST_F(GlicLauncherConfigurationTest, IsEnabled) {
diff --git a/chrome/browser/background/glic/glic_status_icon_unittest.cc b/chrome/browser/background/glic/glic_status_icon_unittest.cc
index 5153b188..3f2f0c3 100644
--- a/chrome/browser/background/glic/glic_status_icon_unittest.cc
+++ b/chrome/browser/background/glic/glic_status_icon_unittest.cc
@@ -22,6 +22,7 @@
 #include "chrome/browser/ui/webui/whats_new/whats_new_ui.h"
 #include "chrome/common/chrome_features.h"
 #include "chrome/test/base/fake_profile_manager.h"
+#include "chrome/test/base/scoped_testing_local_state.h"
 #include "chrome/test/base/testing_browser_process.h"
 #include "components/prefs/testing_pref_service.h"
 #include "content/public/test/browser_task_environment.h"
@@ -80,14 +81,6 @@
   ~GlicStatusIconTest() override = default;
 
   void SetUp() override {
-    // Pref registrations needed for GlobalFeatures or GlicStatusIcon
-    prefs::RegisterLocalStatePrefs(local_state_.registry());
-    WhatsNewUI::RegisterLocalStatePrefs(local_state_.registry());
-    profiles::RegisterPrefs(local_state_.registry());
-    ProfileAttributesStorage::RegisterPrefs(local_state_.registry());
-
-    TestingBrowserProcess::GetGlobal()->SetLocalState(&local_state_);
-
     auto profile_manager_unique = std::make_unique<FakeProfileManager>(
         base::CreateUniqueTempDirectoryScopedToTest());
     TestingBrowserProcess::GetGlobal()->SetProfileManager(
@@ -103,7 +96,6 @@
     glic_status_icon_.reset();
 
     TestingBrowserProcess::GetGlobal()->GetFeatures()->Shutdown();
-    TestingBrowserProcess::GetGlobal()->SetLocalState(nullptr);
     TestingBrowserProcess::GetGlobal()->SetProfileManager(nullptr);
   }
 
@@ -117,7 +109,8 @@
  private:
   content::BrowserTaskEnvironment task_environment_{
       base::test::TaskEnvironment::MainThreadType::UI};
-  TestingPrefServiceSimple local_state_;
+  ScopedTestingLocalState scoped_testing_local_state_{
+      TestingBrowserProcess::GetGlobal()};
   std::unique_ptr<GlicStatusIcon> glic_status_icon_;
   MockStatusTray status_tray_;
   MockGlicController glic_controller_;
diff --git a/chrome/browser/browser_features.cc b/chrome/browser/browser_features.cc
index fdf45d1..709c7364d 100644
--- a/chrome/browser/browser_features.cc
+++ b/chrome/browser/browser_features.cc
@@ -326,6 +326,11 @@
     kNoPreReadMainDllStartup_StartupDuration{&kNoPreReadMainDllStartup,
                                              "no-preread-dll-startup-time",
                                              base::Minutes(2)};
+
+// When enabled, the browser process will re-launch itself when launched with
+// an elevated linked token. The re-launched browser will use the token from
+// the Windows Shell (explorer.exe), which is typically non-elevated.
+BASE_FEATURE(kAutoDeElevate, "AutoDeElevate", base::FEATURE_ENABLED_BY_DEFAULT);
 #endif  // BUILDFLAG(IS_WIN)
 
 #if !BUILDFLAG(IS_ANDROID)
diff --git a/chrome/browser/browser_features.h b/chrome/browser/browser_features.h
index f59e9cd..4d0a5b70 100644
--- a/chrome/browser/browser_features.h
+++ b/chrome/browser/browser_features.h
@@ -121,6 +121,7 @@
 BASE_DECLARE_FEATURE(kNoPreReadMainDllStartup);
 extern const base::FeatureParam<base::TimeDelta>
     kNoPreReadMainDllStartup_StartupDuration;
+BASE_DECLARE_FEATURE(kAutoDeElevate);
 #endif
 
 BASE_DECLARE_FEATURE(kReportPakFileIntegrity);
diff --git a/chrome/browser/chrome_browser_interface_binders_webui.cc b/chrome/browser/chrome_browser_interface_binders_webui.cc
index 8621130..b8cc6d1 100644
--- a/chrome/browser/chrome_browser_interface_binders_webui.cc
+++ b/chrome/browser/chrome_browser_interface_binders_webui.cc
@@ -357,6 +357,7 @@
 #endif
 
 #if BUILDFLAG(ENABLE_WEBUI_TAB_STRIP)
+#include "chrome/browser/ui/tabs/tab_strip_api/tab_strip_api.mojom.h"
 #include "chrome/browser/ui/webui/tab_strip/tab_strip.mojom.h"
 #include "chrome/browser/ui/webui/tab_strip/tab_strip_ui.h"
 #endif
@@ -756,6 +757,8 @@
 #if BUILDFLAG(ENABLE_WEBUI_TAB_STRIP)
   RegisterWebUIControllerInterfaceBinder<tab_strip::mojom::PageHandlerFactory,
                                          TabStripUI>(map);
+  RegisterWebUIControllerInterfaceBinder<tabs_api::mojom::TabStripService,
+                                         TabStripUI>(map);
 #endif
 
 #if BUILDFLAG(IS_CHROMEOS)
diff --git a/chrome/browser/chrome_browser_main.cc b/chrome/browser/chrome_browser_main.cc
index c001bf4c..b20dfdb 100644
--- a/chrome/browser/chrome_browser_main.cc
+++ b/chrome/browser/chrome_browser_main.cc
@@ -762,6 +762,11 @@
     // result in browser startup bailing.
     return CHROME_RESULT_CODE_NORMAL_EXIT_UPGRADE_RELAUNCHED;
   }
+
+  // Requires FeatureList and may restart the browser.
+  if (auto result = ChromeBrowserMainPartsWin::MaybeAutoDeElevate()) {
+    return *result;
+  }
 #endif
 
   return load_local_state_result;
diff --git a/chrome/browser/chrome_browser_main_win.cc b/chrome/browser/chrome_browser_main_win.cc
index 2424460..58e18c4 100644
--- a/chrome/browser/chrome_browser_main_win.cc
+++ b/chrome/browser/chrome_browser_main_win.cc
@@ -45,6 +45,7 @@
 #include "base/trace_event/base_tracing.h"
 #include "base/types/expected.h"
 #include "base/version.h"
+#include "base/win/elevation_util.h"
 #include "base/win/pe_image.h"
 #include "base/win/win_util.h"
 #include "base/win/wrapped_window_proc.h"
@@ -950,3 +951,36 @@
   *module_watcher = ModuleWatcher::Create(base::BindRepeating(
       &ChromeBrowserMainPartsWin::OnModuleEvent, base::Unretained(this)));
 }
+
+std::optional<int> ChromeBrowserMainPartsWin::MaybeAutoDeElevate() {
+  // Check if the browser process is launching elevated, and attempt to
+  // automatically de-elevate. Do not interfere with automation scenarios. Don't
+  // bother trying when UAC is disabled because it won't work anyway.
+  if (base::FeatureList::IsEnabled(features::kAutoDeElevate) &&
+      base::win::UserAccountIsUnnecessarilyElevated() &&
+      !base::CommandLine::ForCurrentProcess()->HasSwitch(
+          switches::kEnableAutomation) &&
+      !base::CommandLine::ForCurrentProcess()->HasSwitch(
+          switches::kDoNotDeElevateOnLaunch)) {
+    base::CommandLine new_command_line(*base::CommandLine::ForCurrentProcess());
+    // Give a fully qualified .exe name
+    base::FilePath full_exe_name;
+    if (base::PathService::Get(base::FILE_EXE, &full_exe_name)) {
+      new_command_line.SetProgram(full_exe_name);
+    }
+    new_command_line.AppendSwitch(switches::kDoNotDeElevateOnLaunch);
+
+    base::FilePath current_dir;
+    CHECK(base::PathService::Get(base::DIR_CURRENT, &current_dir));
+
+    HRESULT hr = base::win::RunDeElevatedNoWait(
+        new_command_line.GetProgram().value(),
+        new_command_line.GetArgumentsString(), current_dir.value());
+    base::UmaHistogramSparse("Windows.AutoDeElevateResult", hr);
+    // If it fails, it doesn't matter why, just proceed with the normal launch.
+    if (SUCCEEDED(hr)) {
+      return CHROME_RESULT_CODE_NORMAL_EXIT_AUTO_DE_ELEVATED;
+    }
+  }
+  return std::nullopt;
+}
diff --git a/chrome/browser/chrome_browser_main_win.h b/chrome/browser/chrome_browser_main_win.h
index 8e7d61b3..e273a4b 100644
--- a/chrome/browser/chrome_browser_main_win.h
+++ b/chrome/browser/chrome_browser_main_win.h
@@ -80,6 +80,11 @@
   static base::CommandLine GetRestartCommandLine(
       const base::CommandLine& command_line);
 
+  // Check if running elevated, and attempt to automatically de-elevate. Returns
+  // an exit code if browser should exit due to a restart, or std::nullopt if
+  // startup should continue.
+  static std::optional<int> MaybeAutoDeElevate();
+
  private:
   void OnModuleEvent(const ModuleWatcher::ModuleEvent& event);
   void SetupModuleDatabase(std::unique_ptr<ModuleWatcher>* module_watcher);
diff --git a/chrome/browser/companion/text_finder/text_finder_manager_base_test.h b/chrome/browser/companion/text_finder/text_finder_manager_base_test.h
index e3a52b8..525ef859 100644
--- a/chrome/browser/companion/text_finder/text_finder_manager_base_test.h
+++ b/chrome/browser/companion/text_finder/text_finder_manager_base_test.h
@@ -38,6 +38,10 @@
                void(blink::mojom::AnnotationType,
                     CreateAgentFromSelectionCallback));
 
+  void RemoveAgentsOfType(blink::mojom::AnnotationType) override {
+    NOTREACHED();
+  }
+
   void MockCreateAgent(
       mojo::PendingRemote<blink::mojom::AnnotationAgentHost> host_remote,
       mojo::PendingReceiver<blink::mojom::AnnotationAgent> agent_receiver,
diff --git a/chrome/browser/data_sharing/android/java/src/org/chromium/chrome/browser/data_sharing/InstantMessageDelegateImpl.java b/chrome/browser/data_sharing/android/java/src/org/chromium/chrome/browser/data_sharing/InstantMessageDelegateImpl.java
index 98a04a4..8adbd2f 100644
--- a/chrome/browser/data_sharing/android/java/src/org/chromium/chrome/browser/data_sharing/InstantMessageDelegateImpl.java
+++ b/chrome/browser/data_sharing/android/java/src/org/chromium/chrome/browser/data_sharing/InstantMessageDelegateImpl.java
@@ -56,6 +56,7 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
+import java.util.Set;
 import java.util.UUID;
 import java.util.concurrent.atomic.AtomicReference;
 
@@ -223,6 +224,11 @@
         }
     }
 
+    @Override
+    public void hideInstantaneousMessage(Set<String> messageIds) {
+        // TODO(crbug.com/416264627): Implement this.
+    }
+
     private @Nullable AttachedWindowInfo getAttachedWindowInfo(
             InstantMessage message, boolean fallbackToLastFocusedWindow) {
         if (mAttachList.size() == 0) {
diff --git a/chrome/browser/enterprise/connectors/analysis/content_analysis_delegate.cc b/chrome/browser/enterprise/connectors/analysis/content_analysis_delegate.cc
index 43f09064..77df1b61 100644
--- a/chrome/browser/enterprise/connectors/analysis/content_analysis_delegate.cc
+++ b/chrome/browser/enterprise/connectors/analysis/content_analysis_delegate.cc
@@ -404,7 +404,7 @@
         show_fail_closed_ui ? FinalContentAnalysisResult::FAIL_CLOSED
                             : FinalContentAnalysisResult::SUCCESS;
 
-#if BUILDFLAG(IS_WIN)
+#if BUILDFLAG(ENABLE_GLIC) && BUILDFLAG(IS_WIN)
     content::WebContents* top_web_contents =
         guest_view::GuestViewBase::GetTopLevelWebContents(
             web_contents->GetResponsibleWebContents());
diff --git a/chrome/browser/enterprise/connectors/analysis/content_analysis_dialog.cc b/chrome/browser/enterprise/connectors/analysis/content_analysis_dialog.cc
index ab04e8a..e221f36 100644
--- a/chrome/browser/enterprise/connectors/analysis/content_analysis_dialog.cc
+++ b/chrome/browser/enterprise/connectors/analysis/content_analysis_dialog.cc
@@ -310,7 +310,7 @@
 // Glic port enabled for Mac only at the moment until fixed on Windows.
 // TODO(416748209): Follow up with full port of ContentAnalysisDialog to use
 // non web modals on both Mac and Windows for all sources.
-#if BUILDFLAG(IS_MAC)
+#if BUILDFLAG(ENABLE_GLIC) && BUILDFLAG(IS_MAC)
   if (glic::IsGlicWebUI(top_level_contents_.get())) {
     // make sure only one dialog is displayed at a time. If a dialog exists we
     // just update the view.
diff --git a/chrome/browser/extensions/api/cookies/cookies_api.cc b/chrome/browser/extensions/api/cookies/cookies_api.cc
index e284ce2..d4d7cfd 100644
--- a/chrome/browser/extensions/api/cookies/cookies_api.cc
+++ b/chrome/browser/extensions/api/cookies/cookies_api.cc
@@ -217,13 +217,19 @@
   // When an off-the-record spinoff of |profile_| is created, start listening
   // for cookie changes there. The OTR receiver should never be bound, since
   // there wasn't previously an OTR profile.
-  if (off_the_record->IsPrimaryOTRProfile()) {
-    DCHECK(!otr_receiver_.is_bound());
-#if !BUILDFLAG(IS_ANDROID)
-    otr_profile_observation_.Observe(off_the_record);
-#endif
-    BindToCookieManager(&otr_receiver_, off_the_record);
+  // TODO(crbug.com/417228685): Clank allows for multiple OTR profiles, unlike
+  // desktop Chrome. Extensions APIs may have built-in assumptions that there
+  // will only be one OTR profile. We need to determine how this will be handled
+  // in Desktop Android.
+  if (!off_the_record->IsPrimaryOTRProfile()) {
+    return;
   }
+
+  DCHECK(!otr_receiver_.is_bound());
+#if !BUILDFLAG(IS_ANDROID)
+  otr_profile_observation_.Observe(off_the_record);
+#endif
+  BindToCookieManager(&otr_receiver_, off_the_record);
 }
 
 void CookiesEventRouter::OnProfileWillBeDestroyed(Profile* profile) {
diff --git a/chrome/browser/extensions/api/developer_private/developer_private_functions_android.cc b/chrome/browser/extensions/api/developer_private/developer_private_functions_android.cc
index 648f0fe..292e5f54 100644
--- a/chrome/browser/extensions/api/developer_private/developer_private_functions_android.cc
+++ b/chrome/browser/extensions/api/developer_private/developer_private_functions_android.cc
@@ -30,9 +30,6 @@
     DeveloperPrivateSetShortcutHandlingSuspendedFunction,
     "developerPrivate.setShortcutHandlingSuspended")
 DEFINE_UNIMPLEMENTED_EXTENSION_FUNCTION(
-    DeveloperPrivateUpdateExtensionCommandFunction,
-    "developerPrivate.updateExtensionCommand")
-DEFINE_UNIMPLEMENTED_EXTENSION_FUNCTION(
     DeveloperPrivateRemoveMultipleExtensionsFunction,
     "developerPrivate.removeMultipleExtensions")
 DEFINE_UNIMPLEMENTED_EXTENSION_FUNCTION(
diff --git a/chrome/browser/extensions/api/developer_private/developer_private_functions_android.h b/chrome/browser/extensions/api/developer_private/developer_private_functions_android.h
index 07ebcdb..0460fbc 100644
--- a/chrome/browser/extensions/api/developer_private/developer_private_functions_android.h
+++ b/chrome/browser/extensions/api/developer_private/developer_private_functions_android.h
@@ -38,9 +38,6 @@
 DECLARE_UNIMPLEMENTED_EXTENSION_FUNCTION(DeveloperPrivateSetShortcutHandlingSuspendedFunction,
                    "developerPrivate.setShortcutHandlingSuspended",
                    DEVELOPERPRIVATE_SETSHORTCUTHANDLINGSUSPENDED);
-DECLARE_UNIMPLEMENTED_EXTENSION_FUNCTION(DeveloperPrivateUpdateExtensionCommandFunction,
-                   "developerPrivate.updateExtensionCommand",
-                   DEVELOPERPRIVATE_UPDATEEXTENSIONCOMMAND);
 DECLARE_UNIMPLEMENTED_EXTENSION_FUNCTION(DeveloperPrivateRemoveMultipleExtensionsFunction,
                    "developerPrivate.removeMultipleExtensions",
                    DEVELOPERPRIVATE_REMOVEMULTIPLEEXTENSIONS);
diff --git a/chrome/browser/extensions/api/developer_private/developer_private_functions_desktop.cc b/chrome/browser/extensions/api/developer_private/developer_private_functions_desktop.cc
index 750d22f..c31cdde 100644
--- a/chrome/browser/extensions/api/developer_private/developer_private_functions_desktop.cc
+++ b/chrome/browser/extensions/api/developer_private/developer_private_functions_desktop.cc
@@ -835,31 +835,6 @@
   return RespondNow(NoArguments());
 }
 
-DeveloperPrivateUpdateExtensionCommandFunction::
-    ~DeveloperPrivateUpdateExtensionCommandFunction() = default;
-
-ExtensionFunction::ResponseAction
-DeveloperPrivateUpdateExtensionCommandFunction::Run() {
-  std::optional<developer::UpdateExtensionCommand::Params> params =
-      developer::UpdateExtensionCommand::Params::Create(args());
-  EXTENSION_FUNCTION_VALIDATE(params);
-  const developer::ExtensionCommandUpdate& update = params->update;
-
-  CommandService* command_service = CommandService::Get(browser_context());
-
-  if (update.scope != developer::CommandScope::kNone) {
-    command_service->SetScope(update.extension_id, update.command_name,
-                              update.scope == developer::CommandScope::kGlobal);
-  }
-
-  if (update.keybinding) {
-    command_service->UpdateKeybindingPrefs(
-        update.extension_id, update.command_name, *update.keybinding);
-  }
-
-  return RespondNow(NoArguments());
-}
-
 DeveloperPrivateRemoveMultipleExtensionsFunction::
     DeveloperPrivateRemoveMultipleExtensionsFunction() = default;
 DeveloperPrivateRemoveMultipleExtensionsFunction::
diff --git a/chrome/browser/extensions/api/developer_private/developer_private_functions_desktop.h b/chrome/browser/extensions/api/developer_private/developer_private_functions_desktop.h
index 3d45a255..55241e3 100644
--- a/chrome/browser/extensions/api/developer_private/developer_private_functions_desktop.h
+++ b/chrome/browser/extensions/api/developer_private/developer_private_functions_desktop.h
@@ -266,17 +266,6 @@
   ResponseAction Run() override;
 };
 
-class DeveloperPrivateUpdateExtensionCommandFunction
-    : public DeveloperPrivateAPIFunction {
- public:
-  DECLARE_EXTENSION_FUNCTION("developerPrivate.updateExtensionCommand",
-                             DEVELOPERPRIVATE_UPDATEEXTENSIONCOMMAND)
-
- protected:
-  ~DeveloperPrivateUpdateExtensionCommandFunction() override;
-  ResponseAction Run() override;
-};
-
 class DeveloperPrivateRemoveMultipleExtensionsFunction
     : public DeveloperPrivateAPIFunction {
  public:
diff --git a/chrome/browser/extensions/api/developer_private/developer_private_functions_shared.cc b/chrome/browser/extensions/api/developer_private/developer_private_functions_shared.cc
index bcfd7ae..b686722 100644
--- a/chrome/browser/extensions/api/developer_private/developer_private_functions_shared.cc
+++ b/chrome/browser/extensions/api/developer_private/developer_private_functions_shared.cc
@@ -686,6 +686,31 @@
   return RespondNow(NoArguments());
 }
 
+DeveloperPrivateUpdateExtensionCommandFunction::
+    ~DeveloperPrivateUpdateExtensionCommandFunction() = default;
+
+ExtensionFunction::ResponseAction
+DeveloperPrivateUpdateExtensionCommandFunction::Run() {
+  std::optional<developer::UpdateExtensionCommand::Params> params =
+      developer::UpdateExtensionCommand::Params::Create(args());
+  EXTENSION_FUNCTION_VALIDATE(params);
+  const developer::ExtensionCommandUpdate& update = params->update;
+
+  CommandService* command_service = CommandService::Get(browser_context());
+
+  if (update.scope != developer::CommandScope::kNone) {
+    command_service->SetScope(update.extension_id, update.command_name,
+                              update.scope == developer::CommandScope::kGlobal);
+  }
+
+  if (update.keybinding) {
+    command_service->UpdateKeybindingPrefs(
+        update.extension_id, update.command_name, *update.keybinding);
+  }
+
+  return RespondNow(NoArguments());
+}
+
 DeveloperPrivateAddHostPermissionFunction::
     DeveloperPrivateAddHostPermissionFunction() = default;
 DeveloperPrivateAddHostPermissionFunction::
diff --git a/chrome/browser/extensions/api/developer_private/developer_private_functions_shared.h b/chrome/browser/extensions/api/developer_private/developer_private_functions_shared.h
index 14b1358..0464925 100644
--- a/chrome/browser/extensions/api/developer_private/developer_private_functions_shared.h
+++ b/chrome/browser/extensions/api/developer_private/developer_private_functions_shared.h
@@ -251,6 +251,17 @@
   ResponseAction Run() override;
 };
 
+class DeveloperPrivateUpdateExtensionCommandFunction
+    : public DeveloperPrivateAPIFunction {
+ public:
+  DECLARE_EXTENSION_FUNCTION("developerPrivate.updateExtensionCommand",
+                             DEVELOPERPRIVATE_UPDATEEXTENSIONCOMMAND)
+
+ protected:
+  ~DeveloperPrivateUpdateExtensionCommandFunction() override;
+  ResponseAction Run() override;
+};
+
 class DeveloperPrivateAddHostPermissionFunction
     : public DeveloperPrivateAPIFunction {
  public:
diff --git a/chrome/browser/extensions/api/file_system/consent_provider_unittest.cc b/chrome/browser/extensions/api/file_system/consent_provider_unittest.cc
index 9961bbe..33eb73e 100644
--- a/chrome/browser/extensions/api/file_system/consent_provider_unittest.cc
+++ b/chrome/browser/extensions/api/file_system/consent_provider_unittest.cc
@@ -13,6 +13,7 @@
 #include "base/run_loop.h"
 #include "chrome/browser/ash/login/users/fake_chrome_user_manager.h"
 #include "chrome/browser/extensions/api/file_system/consent_provider_impl.h"
+#include "chrome/test/base/scoped_testing_local_state.h"
 #include "chrome/test/base/testing_browser_process.h"
 #include "components/prefs/testing_pref_service.h"
 #include "components/user_manager/scoped_user_manager.h"
@@ -111,9 +112,6 @@
   FileSystemApiConsentProviderTest() = default;
 
   void SetUp() override {
-    testing_pref_service_ = std::make_unique<TestingPrefServiceSimple>();
-    TestingBrowserProcess::GetGlobal()->SetLocalState(
-        testing_pref_service_.get());
     user_manager_ = new ash::FakeChromeUserManager;
     scoped_user_manager_enabler_ =
         std::make_unique<user_manager::ScopedUserManager>(
@@ -123,12 +121,11 @@
   void TearDown() override {
     scoped_user_manager_enabler_.reset();
     user_manager_ = nullptr;
-    TestingBrowserProcess::GetGlobal()->SetLocalState(nullptr);
-    testing_pref_service_.reset();
   }
 
  protected:
-  std::unique_ptr<TestingPrefServiceSimple> testing_pref_service_;
+  ScopedTestingLocalState scoped_testing_local_state_{
+      TestingBrowserProcess::GetGlobal()};
   raw_ptr<ash::FakeChromeUserManager, DanglingUntriaged>
       user_manager_;  // Owned by the scope enabler.
   std::unique_ptr<user_manager::ScopedUserManager> scoped_user_manager_enabler_;
diff --git a/chrome/browser/extensions/api/metrics_private/OWNERS b/chrome/browser/extensions/api/metrics_private/OWNERS
index feb8271..b9e8da9 100644
--- a/chrome/browser/extensions/api/metrics_private/OWNERS
+++ b/chrome/browser/extensions/api/metrics_private/OWNERS
@@ -1,2 +1 @@
 asvitkine@chromium.org
-isherman@chromium.org
diff --git a/chrome/browser/extensions/service_worker_tracking_browsertest.cc b/chrome/browser/extensions/service_worker_tracking_browsertest.cc
index a03c0c6..58fe36c 100644
--- a/chrome/browser/extensions/service_worker_tracking_browsertest.cc
+++ b/chrome/browser/extensions/service_worker_tracking_browsertest.cc
@@ -521,7 +521,7 @@
   // Confirm the worker state does still exist, and that the browser stop
   // notification reset it to no longer ready.
   EXPECT_EQ(worker_state->browser_state(),
-            ServiceWorkerTaskQueue::BrowserState::kInitial);
+            ServiceWorkerTaskQueue::BrowserState::kNotStarted);
   EXPECT_EQ(worker_state->renderer_state(),
             ServiceWorkerTaskQueue::RendererState::kNotActive);
 
@@ -535,7 +535,7 @@
 
   // Confirm the worker state still exists and state remains the same.
   EXPECT_EQ(worker_state->browser_state(),
-            ServiceWorkerTaskQueue::BrowserState::kInitial);
+            ServiceWorkerTaskQueue::BrowserState::kNotStarted);
   EXPECT_EQ(worker_state->renderer_state(),
             ServiceWorkerTaskQueue::RendererState::kNotActive);
 }
@@ -582,7 +582,7 @@
   // Confirm the worker state still exists and browser and renderer state are
   // not ready.
   EXPECT_EQ(worker_state->browser_state(),
-            ServiceWorkerTaskQueue::BrowserState::kInitial);
+            ServiceWorkerTaskQueue::BrowserState::kNotStarted);
   EXPECT_EQ(worker_state->renderer_state(),
             ServiceWorkerTaskQueue::RendererState::kNotActive);
 
@@ -593,7 +593,7 @@
   // Confirm the worker state still exists, and browser and renderer state
   // remain not ready.
   EXPECT_EQ(worker_state->browser_state(),
-            ServiceWorkerTaskQueue::BrowserState::kInitial);
+            ServiceWorkerTaskQueue::BrowserState::kNotStarted);
   EXPECT_EQ(worker_state->renderer_state(),
             ServiceWorkerTaskQueue::RendererState::kNotActive);
 }
@@ -692,7 +692,7 @@
   // Confirm the worker state still exists and browser and renderer states have
   // been set to inactive by `ServiceWorkerHost::RenderProcessForWorkerExited`.
   EXPECT_EQ(worker_state->browser_state(),
-            ServiceWorkerTaskQueue::BrowserState::kInitial);
+            ServiceWorkerTaskQueue::BrowserState::kNotStarted);
   EXPECT_EQ(worker_state->renderer_state(),
             ServiceWorkerTaskQueue::RendererState::kNotActive);
 }
diff --git a/chrome/browser/external_protocol/external_protocol_handler_unittest.cc b/chrome/browser/external_protocol/external_protocol_handler_unittest.cc
index e9ce996..d8334a5 100644
--- a/chrome/browser/external_protocol/external_protocol_handler_unittest.cc
+++ b/chrome/browser/external_protocol/external_protocol_handler_unittest.cc
@@ -190,7 +190,6 @@
   void TearDown() override {
     // Ensure that g_accept_requests gets set back to true after test execution.
     ExternalProtocolHandler::PermitLaunchUrl();
-    TestingBrowserProcess::GetGlobal()->SetLocalState(nullptr);
   }
 
   enum class Action { PROMPT, LAUNCH, BLOCK, NONE };
diff --git a/chrome/browser/feed/android/java/res/layout/new_tab_page_feed_header.xml b/chrome/browser/feed/android/java/res/layout/new_tab_page_feed_header.xml
index bea9e01..5a3747d 100644
--- a/chrome/browser/feed/android/java/res/layout/new_tab_page_feed_header.xml
+++ b/chrome/browser/feed/android/java/res/layout/new_tab_page_feed_header.xml
@@ -20,6 +20,8 @@
     -->
     <TextView
         android:id="@+id/header_title"
+        android:focusable="true"
+        android:accessibilityHeading="true"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:textAppearance="@style/TextAppearance.HeaderTitle"
diff --git a/chrome/browser/flag-metadata.json b/chrome/browser/flag-metadata.json
index 874b188..656472f 100644
--- a/chrome/browser/flag-metadata.json
+++ b/chrome/browser/flag-metadata.json
@@ -6925,21 +6925,11 @@
     "expiry_milestone": 125
   },
   {
-    "name": "omnibox-contextual-search-actions-at-top",
-    "owners": [ "mahmadi@chromium.org", "orinj@chromium.org", "chrome-desktop-search@google.com"],
-    "expiry_milestone": 150
-  },
-  {
     "name": "omnibox-contextual-search-on-focus-suggestions",
     "owners": [ "mahmadi@chromium.org", "orinj@chromium.org", "chrome-desktop-search@google.com"],
     "expiry_milestone": 150
   },
   {
-    "name": "omnibox-contextual-search-single-lens-action",
-    "owners": [ "mahmadi@chromium.org", "orinj@chromium.org", "chrome-desktop-search@google.com"],
-    "expiry_milestone": 150
-  },
-  {
     "name": "omnibox-contextual-suggestions",
     "owners": [ "mahmadi@chromium.org", "orinj@chromium.org", "chrome-desktop-search@google.com"],
     "expiry_milestone": 150
@@ -7251,7 +7241,7 @@
   },
   {
     "name": "optimization-guide-enable-dogfood-logging",
-    "owners": [ "isherman@chromium.org", "curranmax@chromium.org", "chrome-intelligence-core@google.com" ],
+    "owners": [ "spelchat@chromium.org", "curranmax@chromium.org", "chrome-intelligence-core@google.com" ],
     // This flag is used in regular manual QA and should not be removed.
     "expiry_milestone": -1
   },
diff --git a/chrome/browser/flag_descriptions.cc b/chrome/browser/flag_descriptions.cc
index d0b7c54..5ca6938 100644
--- a/chrome/browser/flag_descriptions.cc
+++ b/chrome/browser/flag_descriptions.cc
@@ -2953,22 +2953,11 @@
     "Enables the contextual search box to use the ContextualSearchProvider "
     "instead of the ZeroSuggestProvider as the source for suggestions.";
 
-const char kOmniboxContextualSearchActionsAtTopName[] =
-    "Omnibox contextual search actions at top";
-const char kOmniboxContextualSearchActionsAtTopDescription[] =
-    "Enables overriding the placement of contextual search actions in the "
-    "omnibox popup.";
-
 const char kOmniboxContextualSearchOnFocusSuggestionsName[] =
     "Omnibox contextual search on focus suggestions";
 const char kOmniboxContextualSearchOnFocusSuggestionsDescription[] =
     "Enables omnibox contextual search suggestions in zero prefix suggest.";
 
-const char kOmniboxContextualSearchSingleLensActionName[] =
-    "Omnibox contextual search single lens action";
-const char kOmniboxContextualSearchSingleLensActionDescription[] =
-    "Enables single action UX for contextual search in the omnibox popup";
-
 const char kOmniboxContextualSuggestionsName[] =
     "Omnibox contextual suggestions";
 const char kOmniboxContextualSuggestionsDescription[] =
diff --git a/chrome/browser/flag_descriptions.h b/chrome/browser/flag_descriptions.h
index e0f4e85..958be59 100644
--- a/chrome/browser/flag_descriptions.h
+++ b/chrome/browser/flag_descriptions.h
@@ -1694,15 +1694,9 @@
 extern const char kContextualSearchBoxUsesContextualSearchProviderName[];
 extern const char kContextualSearchBoxUsesContextualSearchProviderDescription[];
 
-extern const char kOmniboxContextualSearchActionsAtTopName[];
-extern const char kOmniboxContextualSearchActionsAtTopDescription[];
-
 extern const char kOmniboxContextualSearchOnFocusSuggestionsName[];
 extern const char kOmniboxContextualSearchOnFocusSuggestionsDescription[];
 
-extern const char kOmniboxContextualSearchSingleLensActionName[];
-extern const char kOmniboxContextualSearchSingleLensActionDescription[];
-
 extern const char kOmniboxContextualSuggestionsName[];
 extern const char kOmniboxContextualSuggestionsDescription[];
 
diff --git a/chrome/browser/flags/android/chrome_feature_list.cc b/chrome/browser/flags/android/chrome_feature_list.cc
index fe80cf96..996fd3dc 100644
--- a/chrome/browser/flags/android/chrome_feature_list.cc
+++ b/chrome/browser/flags/android/chrome_feature_list.cc
@@ -1205,7 +1205,7 @@
 
 BASE_FEATURE(kTabStripGroupDragDropAndroid,
              "TabStripGroupDragDropAndroid",
-             base::FEATURE_DISABLED_BY_DEFAULT);
+             base::FEATURE_ENABLED_BY_DEFAULT);
 
 BASE_FEATURE(kTabStripGroupReorderAndroid,
              "TabStripGroupReorderAndroid",
diff --git a/chrome/browser/glic/BUILD.gn b/chrome/browser/glic/BUILD.gn
index 7b2d691..69423f3 100644
--- a/chrome/browser/glic/BUILD.gn
+++ b/chrome/browser/glic/BUILD.gn
@@ -115,6 +115,8 @@
     "glic_keyed_service_factory.cc",
     "glic_metrics.cc",
     "glic_metrics.h",
+    "glic_metrics_provider.cc",
+    "glic_metrics_provider.h",
     "glic_pref_names.cc",
     "glic_profile_manager.cc",
     "glic_settings_util.cc",
@@ -190,6 +192,7 @@
     "//components/live_caption:constants",
     "//components/live_caption:live_caption",
     "//components/live_caption:utils",
+    "//components/metrics",
     "//components/metrics_services_manager:metrics_services_manager",
     "//components/optimization_guide/core",
     "//components/optimization_guide/proto:optimization_guide_proto",
@@ -200,6 +203,7 @@
     "//components/sessions",
     "//components/variations/service",
     "//extensions/browser:browser",
+    "//third_party/metrics_proto",
     "//ui/webui",
     "//url",
   ]
diff --git a/chrome/browser/glic/browser_ui/glic_border_view_interactive_uitest.cc b/chrome/browser/glic/browser_ui/glic_border_view_interactive_uitest.cc
index a6b06263..9c17dcf 100644
--- a/chrome/browser/glic/browser_ui/glic_border_view_interactive_uitest.cc
+++ b/chrome/browser/glic/browser_ui/glic_border_view_interactive_uitest.cc
@@ -218,6 +218,12 @@
                                 kCloseWindowButton, kClickFn));
   }
 
+  void ShutdownGlicWindow() {
+    const DeepQuery kShutdownWindowButton{{"#shutdownbn"}};
+    RunTestSequence(ExecuteJsAt(test::kGlicContentsElementId,
+                                kShutdownWindowButton, kClickFn));
+  }
+
   void ClickGlicButtonInBrowser(Browser* browser) {
     RunTestSequence(InContext(browser->window()->GetElementContext(),
                               PressButton(kGlicButtonElementId)),
@@ -391,6 +397,36 @@
   EXPECT_FALSE(border->GetVisible());
 }
 
+// Ensures that the border animation state is reset after canceling the
+// animation via closePanelAndShutdown.
+IN_PROC_BROWSER_TEST_F(GlicBorderViewUiTest, AnimationStateResetOnShutdown) {
+  auto* border = browser()->window()->AsBrowserView()->glic_border();
+  ASSERT_TRUE(border);
+
+  TesterImpl* tester = static_cast<TesterImpl*>(border->tester());
+  StartBorderAnimation();
+  tester->WaitForAnimationStart();
+  EXPECT_TRUE(border->IsShowing());
+  // Initializes some timestamps.
+  tester->AdvanceTimeAndTickAnimation(base::TimeDelta());
+
+  tester->AdvanceTimeAndTickAnimation(base::Seconds(0.3));
+  // We should be showing something on the screen at 0.3s.
+  EXPECT_GT(border->opacity_for_testing(), 0.f);
+
+  ShutdownGlicWindow();
+  tester->WaitForRampDownStarted();
+  tester->FinishRampDown();
+
+  EXPECT_FALSE(border->IsShowing());
+  EXPECT_FALSE(border->opacity_for_testing());
+  EXPECT_FALSE(border->emphasis_for_testing());
+  EXPECT_FALSE(border->GetVisible());
+
+  // Also check that the web client is gone.
+  EXPECT_FALSE(glic_service()->window_controller().IsWarmed());
+}
+
 // Ensures that the emphasis animation is restarted when tab focus changes.
 IN_PROC_BROWSER_TEST_F(GlicBorderViewUiTest, FocusedTabChange) {
   auto* border = browser()->window()->AsBrowserView()->glic_border();
diff --git a/chrome/browser/glic/e2e_test/glic_e2e_test.cc b/chrome/browser/glic/e2e_test/glic_e2e_test.cc
index 1498e355..ff3cdc9 100644
--- a/chrome/browser/glic/e2e_test/glic_e2e_test.cc
+++ b/chrome/browser/glic/e2e_test/glic_e2e_test.cc
@@ -4,6 +4,8 @@
 
 #include "chrome/browser/glic/e2e_test/glic_e2e_test.h"
 
+#include <optional>
+
 #include "base/files/file_path.h"
 #include "base/files/file_util.h"
 #include "base/path_service.h"
@@ -40,15 +42,13 @@
 #include "chrome/browser/glic/e2e_test/internal_test_placeholder_constants.h"  // nogncheck
 #endif
 
-#include <optional>
-
 namespace glic::test {
 
 namespace {
 
 using glic::test::internal::kGlicWindowControllerState;
 
-base::FilePath::StringViewType kRecordingDirectoryPath =
+constexpr base::FilePath::StringViewType kRecordingDirectoryPath =
     FILE_PATH_LITERAL("chrome/browser/glic/e2e_test/internal/wpr_recordings");
 
 const char kGlicE2ETestModeSwitch[] = "glic-e2e-test-mode";
@@ -215,7 +215,7 @@
   base::FilePath recording_dir_path =
       base::MakeAbsoluteFilePath(root_path.Append(kRecordingDirectoryPath));
   base::FilePath recording_path = recording_dir_path.Append(
-      base::FilePath::FromUTF8Unsafe((recording_filename)));
+      base::FilePath::FromUTF8Unsafe(recording_filename));
   if (test_mode_ == kReplay) {
     CHECK(base::PathExists(recording_path))
         << recording_filename << " does not exist.";
diff --git a/chrome/browser/glic/fre/fre_util.cc b/chrome/browser/glic/fre/fre_util.cc
index 231cbb3..1f38171 100644
--- a/chrome/browser/glic/fre/fre_util.cc
+++ b/chrome/browser/glic/fre/fre_util.cc
@@ -35,7 +35,13 @@
   }
 
   // Add the hotkey configuration to the URL as a query parameter.
-  std::string hotkey_param_value = GetHotkeyString();
+  std::string hotkey_param_value;
+#if !BUILDFLAG(IS_MAC)
+  hotkey_param_value = GetHotkeyString();
+#else
+  hotkey_param_value = GetLongFormMacHotkeyString();
+#endif
+
   if (!hotkey_param_value.empty()) {
     base_url = net::AppendOrReplaceQueryParameter(base_url, "hotkey",
                                                   hotkey_param_value);
diff --git a/chrome/browser/glic/fre/glic_fre_controller.cc b/chrome/browser/glic/fre/glic_fre_controller.cc
index 1f4fcc8..aa83a816e 100644
--- a/chrome/browser/glic/fre/glic_fre_controller.cc
+++ b/chrome/browser/glic/fre/glic_fre_controller.cc
@@ -134,13 +134,14 @@
   if (!browser) {
     return;
   }
-  source_browser_ = browser.get();
 
   // Close any existing FRE dialog before showing.
   if (IsShowingDialog()) {
     DismissFre();
   }
 
+  source_browser_ = browser.get();
+
   CreateView();
 
   tab_showing_modal_ = browser->GetActiveTabInterface();
diff --git a/chrome/browser/glic/glic_enabling.cc b/chrome/browser/glic/glic_enabling.cc
index 005d7f14..287f682f 100644
--- a/chrome/browser/glic/glic_enabling.cc
+++ b/chrome/browser/glic/glic_enabling.cc
@@ -41,7 +41,7 @@
   auto* command_line = base::CommandLine::ForCurrentProcess();
   if (!command_line->HasSwitch(::switches::kGlicDev)) {
     if (!base::FeatureList::IsEnabled(features::kGlicRollout) &&
-        !profile->GetPrefs()->GetBoolean(prefs::kGlicRolloutEligibility)) {
+        !IsEligibleForGlicTieredRollout(profile)) {
       result.not_rolled_out = true;
     }
 
diff --git a/chrome/browser/glic/glic_enabling_browsertest.cc b/chrome/browser/glic/glic_enabling_browsertest.cc
index 8597282..fff1ede3 100644
--- a/chrome/browser/glic/glic_enabling_browsertest.cc
+++ b/chrome/browser/glic/glic_enabling_browsertest.cc
@@ -4,8 +4,11 @@
 
 #include "chrome/browser/glic/glic_enabling.h"
 
+#include "base/test/metrics/histogram_tester.h"
 #include "base/test/scoped_feature_list.h"
+#include "base/time/time.h"
 #include "chrome/browser/browser_process.h"
+#include "chrome/browser/glic/glic_metrics_provider.h"
 #include "chrome/browser/glic/glic_pref_names.h"
 #include "chrome/browser/glic/test_support/glic_test_util.h"
 #include "chrome/browser/global_features.h"
@@ -13,11 +16,15 @@
 #include "chrome/browser/profiles/profile_attributes_entry.h"
 #include "chrome/browser/profiles/profile_attributes_storage.h"
 #include "chrome/browser/profiles/profile_manager.h"
+#include "chrome/browser/profiles/profile_test_util.h"
 #include "chrome/browser/ui/views/tabs/tab_strip.h"
 #include "chrome/common/chrome_features.h"
 #include "chrome/test/base/in_process_browser_test.h"
+#include "components/metrics/metrics_service.h"
 #include "content/public/test/browser_test.h"
 #include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/metrics_proto/chrome_user_metrics_extension.pb.h"
+#include "third_party/metrics_proto/system_profile.pb.h"
 
 using base::test::FeatureRef;
 
@@ -93,6 +100,22 @@
     profile()->GetPrefs()->SetBoolean(prefs::kGlicRolloutEligibility,
                                       is_eligible);
   }
+
+  // Explicitly calls ProvideCurrentSessionData() for all metrics providers.
+  void ProvideCurrentSessionData() {
+    // The purpose of the below call is to avoid a DCHECK failure in an
+    // unrelated metrics provider, in
+    // |FieldTrialsProvider::ProvideCurrentSessionData()|.
+    metrics::SystemProfileProto system_profile_proto;
+    g_browser_process->metrics_service()
+        ->GetDelegatingProviderForTesting()
+        ->ProvideSystemProfileMetricsWithLogCreationTime(base::TimeTicks::Now(),
+                                                         &system_profile_proto);
+    metrics::ChromeUserMetricsExtension uma_proto;
+    g_browser_process->metrics_service()
+        ->GetDelegatingProviderForTesting()
+        ->ProvideCurrentSessionData(&uma_proto);
+  }
 };
 
 IN_PROC_BROWSER_TEST_F(GlicEnablingTieredRolloutTest, EnabledForProfileTest) {
@@ -115,6 +138,12 @@
   // Should be enabled as profile.
   SetTieredRolloutEligibilityForProfile(/*is_eligible=*/true);
   EXPECT_FALSE(GlicEnabling::IsEnabledForProfile(profile()));
+
+  // No profiles eligible so this trial should not be emitted.
+  base::HistogramTester histogram_tester;
+  ProvideCurrentSessionData();
+
+  histogram_tester.ExpectTotalCount("Glic.TieredRolloutEnablementStatus", 0);
 }
 
 class GlicEnablingSimultaneousRolloutTest
@@ -137,10 +166,45 @@
   SetTieredRolloutEligibilityForProfile(/*is_eligible=*/true);
   ASSERT_TRUE(GlicEnabling::IsEnabledForProfile(profile()));
 
-  // No longer eligible for tiered rollout. Should not have effect on overall
-  // enablement.
+  {
+    base::HistogramTester histogram_tester;
+    ProvideCurrentSessionData();
+    histogram_tester.ExpectUniqueSample(
+        "Glic.TieredRolloutEnablementStatus",
+        GlicTieredRolloutEnablementStatus::kAllProfilesEnabled, 1);
+  }
+
+  // Add another profile and have it signed in. The default value for
+  // tiered rollout is false but this profile is enabled via the general
+  // GlicRollout flag and canUseModelExecutionFeatures check.
+  ProfileManager* profile_manager = g_browser_process->profile_manager();
+  base::FilePath new_path = profile_manager->GenerateNextProfileDirectoryPath();
+  Profile* second_profile =
+      &profiles::testing::CreateProfileSync(profile_manager, new_path);
+  ForceSigninAndModelExecutionCapability(second_profile);
+  ASSERT_TRUE(GlicEnabling::IsEnabledForProfile(second_profile));
+
+  {
+    base::HistogramTester histogram_tester;
+    ProvideCurrentSessionData();
+    histogram_tester.ExpectUniqueSample(
+        "Glic.TieredRolloutEnablementStatus",
+        GlicTieredRolloutEnablementStatus::kSomeProfilesEnabled, 1);
+  }
+
+  // Primary profile no longer eligible for tiered rollout. Should not have
+  // effect on overall enablement, but will have an effect on the histogram
+  // emitted.
   SetTieredRolloutEligibilityForProfile(/*is_eligible=*/false);
   ASSERT_TRUE(GlicEnabling::IsEnabledForProfile(profile()));
+
+  {
+    base::HistogramTester histogram_tester;
+    ProvideCurrentSessionData();
+    histogram_tester.ExpectUniqueSample(
+        "Glic.TieredRolloutEnablementStatus",
+        GlicTieredRolloutEnablementStatus::kNoProfilesEnabled, 1);
+  }
 }
 
 }  // namespace
diff --git a/chrome/browser/glic/glic_hotkey.cc b/chrome/browser/glic/glic_hotkey.cc
index cd5bf25..a9a5b1c1 100644
--- a/chrome/browser/glic/glic_hotkey.cc
+++ b/chrome/browser/glic/glic_hotkey.cc
@@ -4,6 +4,8 @@
 
 #include "chrome/browser/glic/glic_hotkey.h"
 
+#include "base/functional/bind.h"
+#include "base/functional/callback_helpers.h"
 #include "base/strings/string_split.h"
 #include "base/strings/string_util.h"
 #include "base/strings/utf_string_conversions.h"
@@ -11,7 +13,11 @@
 #include "ui/base/accelerators/command.h"
 
 namespace glic {
-std::string GetHotkeyString() {
+
+namespace {
+
+std::string GetHotkeyStringWithMapping(
+    base::RepeatingCallback<void(std::u16string&)> token_mapping) {
   std::vector<std::u16string> hotkey_tokens =
       glic::GlicLauncherConfiguration::GetGlobalHotkey()
           .GetShortcutVectorRepresentation();
@@ -25,6 +31,7 @@
   // will be demarked with the '<' and '>' characters, and all components will
   // then be joined with the '-' character.
   for (std::u16string& token : hotkey_tokens) {
+    token_mapping.Run(token);
     token = u"<" + token + u">";
   }
 
@@ -33,4 +40,32 @@
   return base::UTF16ToUTF8(base::JoinString(hotkey_tokens, u"-"));
 }
 
+}  // namespace
+
+std::string GetHotkeyString() {
+  // No mapping used for base implementation.
+  return GetHotkeyStringWithMapping(base::DoNothing());
+}
+
+#if BUILDFLAG(IS_MAC)
+std::string GetLongFormMacHotkeyString() {
+  return GetHotkeyStringWithMapping(
+      base::BindRepeating([](std::u16string& token) {
+        // Accelerator code returns hotkeys on Mac represented by their
+        // respective symbols (i.e. ⌘) rather than their spelled forms (i.e.
+        // Cmd). Map the former to the latter, as that is what is preferred by
+        // the glic UI.
+        if (token == u"⌃") {
+          token = u"Ctrl";
+        } else if (token == u"⌥") {
+          token = u"Option";
+        } else if (token == u"⇧") {
+          token = u"Shift";
+        } else if (token == u"⌘") {
+          token = u"Cmd";
+        }
+      }));
+}
+#endif
+
 }  // namespace glic
diff --git a/chrome/browser/glic/glic_hotkey.h b/chrome/browser/glic/glic_hotkey.h
index 58a04f6..99c0f2b 100644
--- a/chrome/browser/glic/glic_hotkey.h
+++ b/chrome/browser/glic/glic_hotkey.h
@@ -7,12 +7,20 @@
 
 #include <string>
 
+#include "build/build_config.h"
+
 namespace glic {
 
 // Util function shared by both the FRE and the Host for communicating the OS
 // Hotkey to the web implementations.
 std::string GetHotkeyString();
 
+#if BUILDFLAG(IS_MAC)
+// Used for the glic FRE on Mac, where the long form spelling of modifiers is
+// used instead of the Mac-localized symbols.
+std::string GetLongFormMacHotkeyString();
+#endif
+
 }  // namespace glic
 
 #endif  // CHROME_BROWSER_GLIC_GLIC_HOTKEY_H_
diff --git a/chrome/browser/glic/glic_metrics.cc b/chrome/browser/glic/glic_metrics.cc
index b8359b8..4c8af91 100644
--- a/chrome/browser/glic/glic_metrics.cc
+++ b/chrome/browser/glic/glic_metrics.cc
@@ -12,9 +12,7 @@
 #include "chrome/browser/glic/glic_enabling.h"
 #include "chrome/browser/glic/glic_pref_names.h"
 #include "chrome/browser/glic/host/context/glic_focused_tab_manager.h"
-#include "chrome/browser/glic/host/glic_synthetic_trial_manager.h"
 #include "chrome/browser/glic/widget/glic_window_controller.h"
-#include "chrome/browser/global_features.h"
 #include "components/prefs/pref_service.h"
 #include "content/public/browser/render_frame_host.h"
 #include "content/public/browser/web_contents.h"
@@ -225,15 +223,6 @@
   base::UmaHistogramBoolean("Glic.Session.Open.Attached", attached);
   base::UmaHistogramEnumeration("Glic.Session.Open.InvocationSource", source);
 
-  auto* synthetic_trial_manager =
-      g_browser_process->GetFeatures()->glic_synthetic_trial_manager();
-  if (synthetic_trial_manager) {
-    synthetic_trial_manager->SetSyntheticExperimentState(
-        "SyntheticGlicTieredRollout",
-        GlicEnabling::IsEligibleForGlicTieredRollout(profile_) ? "Enabled"
-                                                               : "Disabled");
-  }
-
   ukm::builders::Glic_WindowOpen(source_id_)
       .SetAttached(attached)
       .SetInvocationSource(static_cast<int64_t>(source))
diff --git a/chrome/browser/glic/glic_metrics_provider.cc b/chrome/browser/glic/glic_metrics_provider.cc
new file mode 100644
index 0000000..14d3f29
--- /dev/null
+++ b/chrome/browser/glic/glic_metrics_provider.cc
@@ -0,0 +1,59 @@
+// Copyright 2025 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/glic/glic_metrics_provider.h"
+
+#include <vector>
+
+#include "base/metrics/histogram_functions.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/glic/glic_enabling.h"
+#include "chrome/browser/glic/glic_enums.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/profiles/profile_manager.h"
+#include "third_party/metrics_proto/chrome_user_metrics_extension.pb.h"
+
+namespace glic {
+
+GlicMetricsProvider::GlicMetricsProvider() = default;
+GlicMetricsProvider::~GlicMetricsProvider() = default;
+
+void GlicMetricsProvider::ProvideCurrentSessionData(
+    metrics::ChromeUserMetricsExtension* uma_proto) {
+  ProfileManager* profile_manager = g_browser_process->profile_manager();
+  if (!profile_manager) {
+    return;
+  }
+
+  std::vector<Profile*> profile_list = profile_manager->GetLoadedProfiles();
+  int num_enabled_profiles_enabled_for_tiered_rollout = 0;
+  int num_enabled_profiles = 0;
+  for (auto* profile : profile_list) {
+    if (GlicEnabling::IsEnabledForProfile(profile)) {
+      num_enabled_profiles++;
+      if (GlicEnabling::IsEligibleForGlicTieredRollout(profile)) {
+        num_enabled_profiles_enabled_for_tiered_rollout++;
+      }
+    }
+  }
+
+  // No profiles enabled.
+  if (num_enabled_profiles == 0) {
+    return;
+  }
+
+  GlicTieredRolloutEnablementStatus enablement_status;
+  if (num_enabled_profiles == num_enabled_profiles_enabled_for_tiered_rollout) {
+    enablement_status = GlicTieredRolloutEnablementStatus::kAllProfilesEnabled;
+  } else if (num_enabled_profiles_enabled_for_tiered_rollout == 0) {
+    enablement_status = GlicTieredRolloutEnablementStatus::kNoProfilesEnabled;
+  } else {
+    enablement_status = GlicTieredRolloutEnablementStatus::kSomeProfilesEnabled;
+  }
+
+  base::UmaHistogramEnumeration("Glic.TieredRolloutEnablementStatus",
+                                enablement_status);
+}
+
+}  // namespace glic
diff --git a/chrome/browser/glic/glic_metrics_provider.h b/chrome/browser/glic/glic_metrics_provider.h
new file mode 100644
index 0000000..8577c55
--- /dev/null
+++ b/chrome/browser/glic/glic_metrics_provider.h
@@ -0,0 +1,36 @@
+// Copyright 2025 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_GLIC_GLIC_METRICS_PROVIDER_H_
+#define CHROME_BROWSER_GLIC_GLIC_METRICS_PROVIDER_H_
+
+#include "components/metrics/metrics_provider.h"
+
+namespace glic {
+
+// These values are persisted to logs. Entries should not be renumbered and
+// numeric values should never be reused.
+// LINT.IfChange(TieredRolloutEnablementStatus)
+enum class GlicTieredRolloutEnablementStatus {
+  kAllProfilesEnabled = 0,
+  kSomeProfilesEnabled = 1,
+  kNoProfilesEnabled = 2,
+
+  kMaxValue = kNoProfilesEnabled,
+};
+// LINT.ThenChange(//tools/metrics/histograms/metadata/glic/enums.xml:TieredRolloutEnablementStatus)
+
+class GlicMetricsProvider : public metrics::MetricsProvider {
+ public:
+  GlicMetricsProvider();
+  ~GlicMetricsProvider() override;
+
+  // metrics::MetricsProvider:
+  void ProvideCurrentSessionData(
+      metrics::ChromeUserMetricsExtension* uma_proto) override;
+};
+
+}  // namespace glic
+
+#endif  // CHROME_BROWSER_GLIC_GLIC_METRICS_PROVIDER_H_
diff --git a/chrome/browser/glic/host/glic.mojom b/chrome/browser/glic/host/glic.mojom
index e92cb4c..f61c880 100644
--- a/chrome/browser/glic/host/glic.mojom
+++ b/chrome/browser/glic/host/glic.mojom
@@ -362,6 +362,9 @@
   // Closes the Glic panel.
   ClosePanel();
 
+  // Shuts down the Glic panel.
+  ClosePanelAndShutdown();
+
   // Requests that the web client's panel be attached to a browser
   // window.
   AttachPanel();
diff --git a/chrome/browser/glic/host/glic_page_handler.cc b/chrome/browser/glic/host/glic_page_handler.cc
index 5621007a..18a4439 100644
--- a/chrome/browser/glic/host/glic_page_handler.cc
+++ b/chrome/browser/glic/host/glic_page_handler.cc
@@ -405,6 +405,12 @@
 
   void ClosePanel() override { glic_service_->ClosePanel(); }
 
+  void ClosePanelAndShutdown() override {
+    // Despite the name, CloseUI here tears down the web client in addition to
+    // closing the window.
+    glic_service_->CloseUI();
+  }
+
   void AttachPanel() override { glic_service_->AttachPanel(); }
 
   void DetachPanel() override { glic_service_->DetachPanel(); }
diff --git a/chrome/browser/glic/host/glic_ui_interactive_uitest.cc b/chrome/browser/glic/host/glic_ui_interactive_uitest.cc
index 6ab1ca6..7e8378c 100644
--- a/chrome/browser/glic/host/glic_ui_interactive_uitest.cc
+++ b/chrome/browser/glic/host/glic_ui_interactive_uitest.cc
@@ -15,7 +15,6 @@
 #include "chrome/browser/glic/widget/glic_window_controller.h"
 #include "chrome/browser/ui/browser_element_identifiers.h"
 #include "chrome/common/chrome_features.h"
-#include "components/variations/active_field_trials.h"
 #include "content/public/test/browser_test.h"
 #include "content/public/test/no_renderer_crashes_assertion.h"
 #include "ui/base/accelerators/accelerator.h"
@@ -250,11 +249,6 @@
       OpenGlicWindow(GlicWindowMode::kAttached, GlicInstrumentMode::kHostOnly),
       WaitForState(kGlicUiStateHistory, IsNotCurrently(WebUiState::kOffline)),
       CheckElementVisible(kOfflinePanel, false));
-
-  // Window opened - make sure tier state finch group logged.
-  // Tiered rollout not enabled for this test.
-  EXPECT_TRUE(variations::IsInSyntheticTrialGroup("SyntheticGlicTieredRollout",
-                                                  "Disabled"));
 }
 
 IN_PROC_BROWSER_TEST_P(GlicUiConnectedUiTest,
diff --git a/chrome/browser/hub/internal/android/java/src/org/chromium/chrome/browser/hub/HubColors.java b/chrome/browser/hub/internal/android/java/src/org/chromium/chrome/browser/hub/HubColors.java
index 3c2c2c0..d57f927 100644
--- a/chrome/browser/hub/internal/android/java/src/org/chromium/chrome/browser/hub/HubColors.java
+++ b/chrome/browser/hub/internal/android/java/src/org/chromium/chrome/browser/hub/HubColors.java
@@ -31,6 +31,8 @@
             new int[][] {new int[] {android.R.attr.state_selected}, new int[] {}};
     private static final int[][] DISABLED_AND_NORMAL_STATES =
             new int[][] {new int[] {-android.R.attr.state_enabled}, new int[] {}};
+    private static final int[][] HOVERED_STATE =
+            new int[][] {new int[] {android.R.attr.state_hovered}};
 
     private HubColors() {}
 
@@ -202,17 +204,51 @@
         }
     }
 
+    /** Returns the hub pane switcher tab item hover color as per the given color scheme. */
+    public static @ColorInt int getPaneSwitcherTabItemHoverColor(
+            Context context, @HubColorScheme int colorScheme) {
+        switch (colorScheme) {
+            case HubColorScheme.DEFAULT -> {
+                return SemanticColorUtils.getColorOnSurface(context);
+            }
+            case HubColorScheme.INCOGNITO -> {
+                return ContextCompat.getColor(
+                        context, R.color.pane_switcher_tab_item_hover_incognito);
+            }
+            default -> {
+                assert false;
+                return Color.TRANSPARENT;
+            }
+        }
+    }
+
     public static ColorStateList getActionButtonColor(Context context, @ColorInt int color) {
         @DimenRes int disabledAlpha = R.dimen.default_disabled_alpha;
         return generateDisabledAndNormalStatesColorStateList(context, color, disabledAlpha);
     }
 
+    /**
+     * Generates a {@link ColorStateList} with a specific color applied when the view is in a
+     * hovered state.
+     */
+    public static ColorStateList generateHoveredStateColorStateList(Context context, int color) {
+        @DimenRes int hoveredAlpha = R.dimen.hub_pane_switcher_tab_item_hover_alpha;
+        int hoveredColor = getColorWithAlphaApplied(context, color, hoveredAlpha);
+        return new ColorStateList(HOVERED_STATE, new int[] {hoveredColor});
+    }
+
     private static ColorStateList generateDisabledAndNormalStatesColorStateList(
             Context context, int color, int disabledAlpha) {
-        Resources resources = context.getResources();
-        float alpha = ValueUtils.getFloat(resources, disabledAlpha);
-        int alphaScaled = Math.round(alpha * 255);
-        int[] colors = new int[] {ColorUtils.setAlphaComponent(color, alphaScaled), color};
+        int[] colors = new int[] {getColorWithAlphaApplied(context, color, disabledAlpha), color};
         return new ColorStateList(DISABLED_AND_NORMAL_STATES, colors);
     }
+
+    private static @ColorInt int getColorWithAlphaApplied(
+            Context context, int color, @DimenRes int alphaRes) {
+        Resources resources = context.getResources();
+        float alpha = ValueUtils.getFloat(resources, alphaRes);
+        int alphaScaled = Math.round(alpha * 255);
+
+        return ColorUtils.setAlphaComponent(color, alphaScaled);
+    }
 }
diff --git a/chrome/browser/hub/internal/android/java/src/org/chromium/chrome/browser/hub/HubToolbarView.java b/chrome/browser/hub/internal/android/java/src/org/chromium/chrome/browser/hub/HubToolbarView.java
index 7e0228f..f6b92be 100644
--- a/chrome/browser/hub/internal/android/java/src/org/chromium/chrome/browser/hub/HubToolbarView.java
+++ b/chrome/browser/hub/internal/android/java/src/org/chromium/chrome/browser/hub/HubToolbarView.java
@@ -178,6 +178,7 @@
                     tab.view.setLayoutParams(tabLayoutParams);
                     tab.view.setPadding(
                             tabItemPadding, tabItemPadding, tabItemPadding, tabItemPadding);
+                    tab.view.setBackground(buildBackgroundDrawableForTab());
                 }
             }
             mPaneSwitcher.setVisibility(View.VISIBLE);
@@ -336,6 +337,14 @@
                                         new PorterDuffColorFilter(color, PorterDuff.Mode.SRC);
                                 mPaneSwitcherCard.getBackground().setColorFilter(filter);
                             }));
+
+            mixer.registerBlend(
+                    new SingleHubViewColorBlend(
+                            PANE_COLOR_BLEND_ANIMATION_DURATION_MS,
+                            colorScheme ->
+                                    HubColors.getPaneSwitcherTabItemHoverColor(
+                                            context, colorScheme),
+                            color -> updateTabItemBackgroundColor(context, color)));
         }
     }
 
@@ -384,6 +393,18 @@
         mSearchLoupeView.setImageTintList(colorStateList);
     }
 
+    private void updateTabItemBackgroundColor(Context context, @ColorInt int color) {
+        ColorStateList colorStateList =
+                HubColors.generateHoveredStateColorStateList(context, color);
+        for (int i = 0; i < mPaneSwitcher.getTabCount(); i++) {
+            View tabView = getButtonView(i);
+            if (tabView != null) {
+                GradientDrawable background = (GradientDrawable) tabView.getBackground();
+                background.setColor(colorStateList);
+            }
+        }
+    }
+
     void setButtonLookupConsumer(Callback<PaneButtonLookup> lookupConsumer) {
         lookupConsumer.onResult(this::getButtonView);
     }
@@ -516,4 +537,13 @@
 
         return new TouchDelegate(rect, mActionButton);
     }
+
+    private GradientDrawable buildBackgroundDrawableForTab() {
+        int radius = getResources().getDimensionPixelSize(R.dimen.hub_pane_switcher_tab_radius);
+        GradientDrawable hoverDrawable = new GradientDrawable();
+
+        hoverDrawable.setShape(GradientDrawable.RECTANGLE);
+        hoverDrawable.setCornerRadius(radius);
+        return hoverDrawable;
+    }
 }
diff --git a/chrome/browser/hub/internal/android/res/drawable/hub_pane_switcher_item_selector.xml b/chrome/browser/hub/internal/android/res/drawable/hub_pane_switcher_item_selector.xml
index bca63615..bf0b525 100644
--- a/chrome/browser/hub/internal/android/res/drawable/hub_pane_switcher_item_selector.xml
+++ b/chrome/browser/hub/internal/android/res/drawable/hub_pane_switcher_item_selector.xml
@@ -7,6 +7,6 @@
 <shape
   xmlns:android="http://schemas.android.com/apk/res/android"
   android:shape="rectangle">
-  <corners android:radius="12dp" />
+  <corners android:radius="@dimen/hub_pane_switcher_tab_radius" />
   <size android:height="@dimen/hub_pane_switcher_tab_size" />
 </shape>
\ No newline at end of file
diff --git a/chrome/browser/hub/internal/android/res/values/colors.xml b/chrome/browser/hub/internal/android/res/values/colors.xml
index ba7576f..8f04f6f 100644
--- a/chrome/browser/hub/internal/android/res/values/colors.xml
+++ b/chrome/browser/hub/internal/android/res/values/colors.xml
@@ -7,4 +7,5 @@
 <resources>
     <color name="pane_switcher_selected_tab_incognito">@color/gm3_baseline_surface_bright_dark</color>
     <color name="pane_switcher_background_incognito">@color/gm3_baseline_surface_container_dark</color>
+    <color name="pane_switcher_tab_item_hover_incognito">@color/baseline_neutral_90</color>
 </resources>
\ No newline at end of file
diff --git a/chrome/browser/hub/internal/android/res/values/dimens.xml b/chrome/browser/hub/internal/android/res/values/dimens.xml
index 1436cd7e..0a0040c 100644
--- a/chrome/browser/hub/internal/android/res/values/dimens.xml
+++ b/chrome/browser/hub/internal/android/res/values/dimens.xml
@@ -16,6 +16,10 @@
     <dimen name="hub_pane_switcher_vertical_padding">4dp</dimen>
     <dimen name="hub_pane_switcher_card_height">48dp</dimen>
     <dimen name="hub_pane_switcher_tab_size">40dp</dimen>
+    <dimen name="hub_pane_switcher_tab_radius">12dp</dimen>
     <dimen name="hub_pane_switcher_tab_item_padding">8dp</dimen>
     <dimen name="hub_pane_switcher_tab_item_horizontal_margin">2dp</dimen>
+
+    <!-- Pane switcher tab item hover color alpha values -->
+    <item name="hub_pane_switcher_tab_item_hover_alpha" format="float" type="dimen">0.08</item>
 </resources>
diff --git a/chrome/browser/metrics/chrome_metrics_service_client.cc b/chrome/browser/metrics/chrome_metrics_service_client.cc
index 60932d11..9136ad142 100644
--- a/chrome/browser/metrics/chrome_metrics_service_client.cc
+++ b/chrome/browser/metrics/chrome_metrics_service_client.cc
@@ -149,6 +149,10 @@
 #include "extensions/common/extension.h"
 #endif
 
+#if BUILDFLAG(ENABLE_GLIC)
+#include "chrome/browser/glic/glic_metrics_provider.h"
+#endif
+
 #if BUILDFLAG(IS_CHROMEOS)
 #include "ash/constants/ash_features.h"
 #include "base/feature_list.h"
@@ -165,6 +169,7 @@
 #include "chrome/browser/metrics/chromeos_family_link_user_metrics_provider.h"
 #include "chrome/browser/metrics/chromeos_metrics_provider.h"
 #include "chrome/browser/metrics/chromeos_system_profile_provider.h"
+#include "chrome/browser/metrics/class_management_enabled_metrics_provider.h"
 #include "chrome/browser/metrics/cros_healthd_metrics_provider.h"
 #include "chrome/browser/metrics/cros_pre_consent_metrics_manager.h"
 #include "chrome/browser/metrics/family_user_metrics_provider.h"
@@ -937,6 +942,11 @@
     metrics_service_->RegisterMetricsProvider(
         std::make_unique<K12AgeClassificationMetricsProvider>());
   }
+  if (base::FeatureList::IsEnabled(
+          ::features::kClassManagementEnabledMetricsProvider)) {
+    metrics_service_->RegisterMetricsProvider(
+        std::make_unique<ClassManagementEnabledMetricsProvider>());
+  }
 
 #endif  // BUILDFLAG(IS_CHROMEOS)
 
@@ -978,6 +988,11 @@
   metrics_service_->RegisterMetricsProvider(
       metrics::CreateDesktopSessionMetricsProvider());
 #endif  // BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || (BUILDFLAG(IS_LINUX)
+
+#if BUILDFLAG(ENABLE_GLIC)
+  metrics_service_->RegisterMetricsProvider(
+      std::make_unique<glic::GlicMetricsProvider>());
+#endif
 }
 
 void ChromeMetricsServiceClient::RegisterUKMProviders() {
diff --git a/chrome/browser/metrics/chrome_metrics_service_client_unittest.cc b/chrome/browser/metrics/chrome_metrics_service_client_unittest.cc
index 71a33a3cb..63fa4951 100644
--- a/chrome/browser/metrics/chrome_metrics_service_client_unittest.cc
+++ b/chrome/browser/metrics/chrome_metrics_service_client_unittest.cc
@@ -97,6 +97,7 @@
     scoped_feature_list_.InitWithFeatures(
         {features::kUmaStorageDimensions,
          features::kK12AgeClassificationMetricsProvider,
+         features::kClassManagementEnabledMetricsProvider,
          metrics::dwa::kDwaFeature},
         {});
 
@@ -239,13 +240,14 @@
   // AmbientModeMetricsProvider, AssistantServiceMetricsProvider,
   // CrosHealthdMetricsProvider, ChromeOSMetricsProvider,
   // ChromeOSHistogramMetricsProvider, ChromeShelfMetricsProvider,
+  // ClassManagementEnabledMetricsProvider,
   // K12AgeClassificationMetricsProvider, KeyboardBacklightColorMetricsProvider,
   // PersonalizationAppThemeMetricsProvider, PrinterMetricsProvider,
   // FamilyUserMetricsProvider, FamilyLinkUserMetricsProvider,
   // UpdateEngineMetricsProvider, OsSettingsMetricsProvider,
   // UserTypeByDeviceTypeMetricsProvider, WallpaperMetricsProvider,
   // and VmmMetricsProvider.
-  expected_providers += 17;
+  expected_providers += 18;
 #endif  // BUILDFLAG(IS_CHROMEOS)
 
 #if !BUILDFLAG(IS_CHROMEOS)
@@ -273,6 +275,11 @@
   expected_providers += 1;
 #endif
 
+#if BUILDFLAG(ENABLE_GLIC)
+  // GlicMetricsProvider
+  expected_providers += 1;
+#endif
+
   std::unique_ptr<TestChromeMetricsServiceClient>
       chrome_metrics_service_client = TestChromeMetricsServiceClient::Create(
           metrics_state_manager_.get(), synthetic_trial_registry_.get());
diff --git a/chrome/browser/metrics/chrome_metrics_services_manager_client_unittest.cc b/chrome/browser/metrics/chrome_metrics_services_manager_client_unittest.cc
index 06fd208..fe400d9 100644
--- a/chrome/browser/metrics/chrome_metrics_services_manager_client_unittest.cc
+++ b/chrome/browser/metrics/chrome_metrics_services_manager_client_unittest.cc
@@ -10,6 +10,7 @@
 #include "build/build_config.h"
 #include "chrome/browser/metrics/chrome_metrics_service_accessor.h"
 #include "chrome/browser/metrics/chrome_metrics_service_client.h"
+#include "chrome/test/base/scoped_testing_local_state.h"
 #include "chrome/test/base/testing_browser_process.h"
 #include "components/metrics/enabled_state_provider.h"
 #include "components/metrics/metrics_pref_names.h"
@@ -32,28 +33,18 @@
 
   ~ChromeMetricsServicesManagerClientTest() override = default;
 
-  void SetUp() override {
-    // Set up Local State prefs.
-    TestingBrowserProcess::GetGlobal()->SetLocalState(&local_state_);
-    ChromeMetricsServiceClient::RegisterPrefs(local_state()->registry());
-  }
-
-  void TearDown() override {
-    TestingBrowserProcess::GetGlobal()->SetLocalState(nullptr);
-  }
-
-  TestingPrefServiceSimple* local_state() { return &local_state_; }
+  PrefService* local_state() { return scoped_testing_local_state_.Get(); }
 
  private:
-  TestingPrefServiceSimple local_state_;
+  ScopedTestingLocalState scoped_testing_local_state_{
+      TestingBrowserProcess::GetGlobal()};
 };
 
 using IsClientInSampleTest = ChromeMetricsServicesManagerClientTest;
 
 TEST_F(ChromeMetricsServicesManagerClientTest, ForceTrialsDisablesReporting) {
   // First, test with UMA reporting setting defaulting to off.
-  local_state()->registry()->RegisterBooleanPref(
-      metrics::prefs::kMetricsReportingEnabled, false);
+  local_state()->SetBoolean(metrics::prefs::kMetricsReportingEnabled, false);
   // Force the pref to be used, even in unofficial builds.
   ChromeMetricsServiceAccessor::SetForceIsMetricsReportingEnabledPrefLookup(
       true);
@@ -85,8 +76,7 @@
 
 TEST_F(ChromeMetricsServicesManagerClientTest, PopulateStartupVisibility) {
   // Register the kMetricsReportingEnabled pref.
-  local_state()->registry()->RegisterBooleanPref(
-      metrics::prefs::kMetricsReportingEnabled, false);
+  local_state()->SetBoolean(metrics::prefs::kMetricsReportingEnabled, false);
 
   ChromeMetricsServicesManagerClient client(local_state());
   metrics::MetricsStateManager* metrics_state_manager =
diff --git a/chrome/browser/metrics/class_management_enabled_metrics_provider.cc b/chrome/browser/metrics/class_management_enabled_metrics_provider.cc
new file mode 100644
index 0000000..a59873b
--- /dev/null
+++ b/chrome/browser/metrics/class_management_enabled_metrics_provider.cc
@@ -0,0 +1,113 @@
+// Copyright 2025 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/metrics/class_management_enabled_metrics_provider.h"
+
+#include "base/check_is_test.h"
+#include "base/metrics/histogram_functions.h"
+#include "chrome/browser/ash/login/demo_mode/demo_session.h"
+#include "chrome/browser/ash/policy/core/browser_policy_connector_ash.h"
+#include "chrome/browser/ash/policy/core/user_cloud_policy_manager_ash.h"
+#include "chrome/browser/ash/profiles/profile_helper.h"
+#include "chrome/browser/policy/profile_policy_connector.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/profiles/profiles_state.h"
+#include "chromeos/components/kiosk/kiosk_utils.h"
+#include "chromeos/components/mgs/managed_guest_session_utils.h"
+#include "components/session_manager/core/session_manager.h"
+#include "components/user_manager/user.h"
+#include "components/user_manager/user_manager.h"
+
+namespace {
+
+constexpr std::string_view kClassManagementStudent = "student";
+constexpr std::string_view kClassManagementTeacher = "teacher";
+constexpr std::string_view kHistogramName = "ChromeOS.ClassManagementEnabled";
+constexpr std::string_view kClassManagementEnabledName =
+    "ClassManagementEnabled";
+
+ClassManagementEnabledMetricsProvider::ClassManagementEnabled
+GetClassManagementEnabled(Profile* profile) {
+  const policy::UserCloudPolicyManagerAsh* const user_cloud_policy_manager =
+      profile->GetUserCloudPolicyManagerAsh();
+  if (!user_cloud_policy_manager) {
+    return ClassManagementEnabledMetricsProvider::ClassManagementEnabled::
+        kDisabled;
+  }
+
+  const base::Value* const policy =
+      user_cloud_policy_manager->core()->store()->policy_map().GetValue(
+          std::string(kClassManagementEnabledName), base::Value::Type::STRING);
+
+  if (!policy) {
+    return ClassManagementEnabledMetricsProvider::ClassManagementEnabled::
+        kDisabled;
+  }
+  const std::string& policy_str = policy->GetString();
+  if (policy_str == kClassManagementStudent) {
+    return ClassManagementEnabledMetricsProvider::ClassManagementEnabled::
+        kStudent;
+  }
+  if (policy_str == kClassManagementTeacher) {
+    return ClassManagementEnabledMetricsProvider::ClassManagementEnabled::
+        kTeacher;
+  }
+  return ClassManagementEnabledMetricsProvider::ClassManagementEnabled::
+      kDisabled;
+}
+}  // namespace
+
+ClassManagementEnabledMetricsProvider::ClassManagementEnabledMetricsProvider() {
+  auto* const session_manager = session_manager::SessionManager::Get();
+  // The `session_manager` is nullptr only for unit tests.
+  if (session_manager) {
+    session_manager->AddObserver(this);
+  } else {
+    CHECK_IS_TEST();
+  }
+}
+
+ClassManagementEnabledMetricsProvider::
+    ~ClassManagementEnabledMetricsProvider() {
+  auto* const session_manager = session_manager::SessionManager::Get();
+  // The `session_manager` is nullptr only for unit tests.
+  if (session_manager) {
+    session_manager->RemoveObserver(this);
+  } else {
+    CHECK_IS_TEST();
+  }
+}
+
+bool ClassManagementEnabledMetricsProvider::ProvideHistograms() {
+  if (!segment_.has_value()) {
+    return false;
+  }
+
+  base::UmaHistogramEnumeration(kHistogramName, segment_.value());
+  return true;
+}
+
+void ClassManagementEnabledMetricsProvider::OnUserSessionStarted(
+    bool is_primary_user) {
+  // Skip non-primary, demo, managed guest and kiosk users.
+  if (!is_primary_user || profiles::IsDemoSession() ||
+      chromeos::IsManagedGuestSession() || chromeos::IsKioskSession()) {
+    return;
+  }
+
+  const user_manager::User* const primary_user =
+      user_manager::UserManager::Get()->GetPrimaryUser();
+  CHECK(primary_user);
+  CHECK(primary_user->is_profile_created());
+  Profile* const profile =
+      ash::ProfileHelper::Get()->GetProfileByUser(primary_user);
+  CHECK(profile);
+
+  // Skip unmanaged and unaffiliated users.
+  if (profile->IsOffTheRecord() || primary_user->IsAffiliated()) {
+    return;
+  }
+
+  segment_ = GetClassManagementEnabled(profile);
+}
diff --git a/chrome/browser/metrics/class_management_enabled_metrics_provider.h b/chrome/browser/metrics/class_management_enabled_metrics_provider.h
new file mode 100644
index 0000000..6b94fe18
--- /dev/null
+++ b/chrome/browser/metrics/class_management_enabled_metrics_provider.h
@@ -0,0 +1,44 @@
+// Copyright 2025 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_METRICS_CLASS_MANAGEMENT_ENABLED_METRICS_PROVIDER_H_
+#define CHROME_BROWSER_METRICS_CLASS_MANAGEMENT_ENABLED_METRICS_PROVIDER_H_
+
+#include <optional>
+
+#include "components/metrics/metrics_provider.h"
+#include "components/session_manager/core/session_manager_observer.h"
+
+class ClassManagementEnabledMetricsProvider
+    : public metrics::MetricsProvider,
+      public session_manager::SessionManagerObserver {
+ public:
+  enum class ClassManagementEnabled {
+    // Class management disabled for the current user.
+    kDisabled = 0,
+    // Class management enabled for the current student user.
+    kStudent = 1,
+    // Class management enabled for the current teacher user.
+    kTeacher = 2,
+    kMaxValue = kTeacher,
+  };
+
+  ClassManagementEnabledMetricsProvider();
+  ClassManagementEnabledMetricsProvider(
+      const ClassManagementEnabledMetricsProvider&) = delete;
+  ClassManagementEnabledMetricsProvider& operator=(
+      const ClassManagementEnabledMetricsProvider&) = delete;
+  ~ClassManagementEnabledMetricsProvider() override;
+
+  // MetricsProvider:
+  bool ProvideHistograms() override;
+
+  // session_manager::SessionManagerObserver:
+  void OnUserSessionStarted(bool is_primary_user) override;
+
+ private:
+  std::optional<ClassManagementEnabled> segment_;
+};
+
+#endif  // CHROME_BROWSER_METRICS_CLASS_MANAGEMENT_ENABLED_METRICS_PROVIDER_H_
diff --git a/chrome/browser/metrics/class_management_enabled_metrics_provider_browsertest.cc b/chrome/browser/metrics/class_management_enabled_metrics_provider_browsertest.cc
new file mode 100644
index 0000000..5121fa6
--- /dev/null
+++ b/chrome/browser/metrics/class_management_enabled_metrics_provider_browsertest.cc
@@ -0,0 +1,153 @@
+// Copyright 2025 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/metrics/class_management_enabled_metrics_provider.h"
+
+#include <optional>
+
+#include "ash/constants/ash_switches.h"
+#include "base/test/metrics/histogram_tester.h"
+#include "base/test/scoped_feature_list.h"
+#include "chrome/browser/ash/login/existing_user_controller.h"
+#include "chrome/browser/ash/login/test/logged_in_user_mixin.h"
+#include "chrome/browser/ash/login/users/fake_chrome_user_manager.h"
+#include "chrome/browser/ash/login/wizard_controller.h"
+#include "chrome/browser/ash/policy/core/device_policy_cros_browser_test.h"
+#include "chrome/browser/ash/policy/test_support/embedded_policy_test_server_mixin.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/common/chrome_features.h"
+#include "chrome/test/base/fake_gaia_mixin.h"
+#include "chromeos/ash/components/dbus/session_manager/fake_session_manager_client.h"
+#include "components/account_id/account_id.h"
+#include "components/metrics/metrics_service.h"
+#include "components/policy/core/common/cloud/cloud_policy_constants.h"
+#include "components/policy/core/common/cloud/mock_cloud_policy_store.h"
+#include "components/policy/core/common/cloud/test/policy_builder.h"
+#include "components/policy/proto/device_management_backend.pb.h"
+#include "content/public/test/browser_test.h"
+
+namespace {
+
+using ClassManagementEnabled =
+    ClassManagementEnabledMetricsProvider::ClassManagementEnabled;
+using testing::InvokeWithoutArgs;
+
+constexpr std::string_view kClassManagementDisabled = "disabled";
+constexpr std::string_view kClassManagementStudent = "student";
+constexpr std::string_view kClassManagementTeacher = "teacher";
+constexpr std::string_view kHistogramName = "ChromeOS.ClassManagementEnabled";
+
+void ProvideHistograms() {
+  // The purpose of the below call is to avoid a DCHECK failure in an unrelated
+  // metrics provider, in |FieldTrialsProvider::ProvideCurrentSessionData()|.
+  metrics::SystemProfileProto system_profile_proto;
+  // Downstream functions do not use system_profile_proto so there is no risk of
+  // UAF.
+  g_browser_process->metrics_service()
+      ->GetDelegatingProviderForTesting()
+      ->ProvideSystemProfileMetricsWithLogCreationTime(base::TimeTicks::Now(),
+                                                       &system_profile_proto);
+  g_browser_process->metrics_service()
+      ->GetDelegatingProviderForTesting()
+      ->OnDidCreateMetricsLog();
+}
+
+class TestCase {
+ public:
+  explicit TestCase(std::string_view policy_value,
+                    ClassManagementEnabled segment)
+      : policy_value_(policy_value), segment_(segment) {}
+
+  ClassManagementEnabled GetSegment() const { return segment_; }
+  std::string_view GetPolicyValue() const { return policy_value_; }
+
+ private:
+  const std::string_view policy_value_;
+  const ClassManagementEnabled segment_;
+};
+
+class ClassManagementEnabledMetricsProviderTest
+    : public policy::DevicePolicyCrosBrowserTest,
+      public testing::WithParamInterface<TestCase> {
+ protected:
+  ClassManagementEnabledMetricsProviderTest() {
+    scoped_feature_list_.InitAndEnableFeature(
+        features::kClassManagementEnabledMetricsProvider);
+  }
+  void SetUpInProcessBrowserTestFixture() override {
+    policy::DevicePolicyCrosBrowserTest::SetUpInProcessBrowserTestFixture();
+    InitializePolicy();
+  }
+
+  void SetUpCommandLine(base::CommandLine* command_line) override {
+    command_line->AppendSwitch(ash::switches::kOobeSkipPostLogin);
+    DevicePolicyCrosBrowserTest::SetUpCommandLine(command_line);
+  }
+
+  void InitializePolicy() {
+    device_policy()->policy_data().set_public_key_version(1);
+  }
+
+  void SetDevicePolicy() {
+    device_local_account_policy_.SetDefaultSigningKey();
+    device_local_account_policy_.Build();
+    logged_in_user_mixin_.GetEmbeddedPolicyTestServerMixin()
+        ->UpdateExternalPolicy(
+            policy::dm_protocol::kChromePublicAccountPolicyType,
+            FakeGaiaMixin::kEnterpriseUser1,
+            device_local_account_policy_.payload().SerializeAsString());
+    session_manager_client()->set_device_local_account_policy(
+        FakeGaiaMixin::kEnterpriseUser1,
+        device_local_account_policy_.GetBlob());
+  }
+
+  void LogInUser() {
+    std::unique_ptr<ash::ScopedUserPolicyUpdate> policy =
+        logged_in_user_mixin_.GetUserPolicyMixin()->RequestPolicyUpdate();
+    policy->policy_payload()
+        ->mutable_subproto1()
+        ->mutable_classmanagementenabled()
+        ->set_value(GetParam().GetPolicyValue());
+    logged_in_user_mixin_.LogInUser();
+  }
+
+  int GetExpectedUmaValue() {
+    return static_cast<int>(GetParam().GetSegment());
+  }
+
+ private:
+  base::test::ScopedFeatureList scoped_feature_list_;
+  ash::LoggedInUserMixin logged_in_user_mixin_{
+      &mixin_host_, /*test_base=*/this, embedded_test_server(),
+      ash::LoggedInUserMixin::LogInType::kManaged};
+  policy::UserPolicyBuilder device_local_account_policy_;
+};
+
+IN_PROC_BROWSER_TEST_P(ClassManagementEnabledMetricsProviderTest, Uma) {
+  base::HistogramTester histogram_tester;
+
+  SetDevicePolicy();
+
+  // Simulate calling ProvideHistograms() prior to logging in.
+  ProvideHistograms();
+
+  // No metrics were recorded.
+  histogram_tester.ExpectTotalCount(kHistogramName, 0);
+
+  LogInUser();
+
+  // Simulate calling ProvideHistograms() after logging in.
+  ProvideHistograms();
+
+  histogram_tester.ExpectUniqueSample(kHistogramName, GetExpectedUmaValue(), 1);
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    All,
+    ClassManagementEnabledMetricsProviderTest,
+    testing::Values(
+        TestCase(kClassManagementDisabled, ClassManagementEnabled::kDisabled),
+        TestCase(kClassManagementStudent, ClassManagementEnabled::kStudent),
+        TestCase(kClassManagementTeacher, ClassManagementEnabled::kTeacher)));
+}  // namespace
diff --git a/chrome/browser/net/reporting_browsertest.cc b/chrome/browser/net/reporting_browsertest.cc
index f3d82158..58b5b93 100644
--- a/chrome/browser/net/reporting_browsertest.cc
+++ b/chrome/browser/net/reporting_browsertest.cc
@@ -94,7 +94,7 @@
                                        /*use_plus=*/false);
   }
 
-  std::string GetReportingEndpointsHeader() const {
+  virtual std::string GetReportingEndpointsHeader() const {
     return "Reporting-Endpoints: default=\"" + GetCollectorURL().spec() + "\"";
   }
 
@@ -222,6 +222,32 @@
   base::test::ScopedFeatureList scoped_feature_list_;
 };
 
+class ReportingBrowserTestSpecifyCrashEndpoint
+    : public BaseReportingBrowserTest {
+ public:
+  ReportingBrowserTestSpecifyCrashEndpoint() {
+    scoped_feature_list_.InitWithFeatureState(
+        blink::features::kOverrideCrashReportingEndpoint,
+        /*enabled=*/GetParam());
+  }
+
+  ReportingBrowserTestSpecifyCrashEndpoint(
+      const ReportingBrowserTestSpecifyCrashEndpoint&) = delete;
+  ReportingBrowserTestSpecifyCrashEndpoint& operator=(
+      const ReportingBrowserTestSpecifyCrashEndpoint&) = delete;
+
+  ~ReportingBrowserTestSpecifyCrashEndpoint() override = default;
+
+  std::string GetReportingEndpointsHeader() const override {
+    // Override the endpoint name of crash reporting.
+    return "Reporting-Endpoints: crash-reporting=\"" +
+           GetCollectorURL().spec() + "\"";
+  }
+
+ private:
+  base::test::ScopedFeatureList scoped_feature_list_;
+};
+
 class JSCallStackReportingBrowserTest : public BaseReportingBrowserTest {
  public:
   JSCallStackReportingBrowserTest() = default;
@@ -604,6 +630,7 @@
   DISABLED_IframeUnresponsiveWithJSCallStackOptedIn
 #define MAYBE_IframeUnresponsiveWithJSCallStackNotOptedIn \
   DISABLED_IframeUnresponsiveWithJSCallStackNotOptedIn
+#define MAYBE_SpecifyCrashEndpoint DISABLED_SpecifyCrashEndpoint
 #else
 #define MAYBE_CrashReport CrashReport
 #define MAYBE_CrashReportUnresponsive CrashReportUnresponsive
@@ -615,6 +642,7 @@
   IframeUnresponsiveWithJSCallStackOptedIn
 #define MAYBE_IframeUnresponsiveWithJSCallStackNotOptedIn \
   IframeUnresponsiveWithJSCallStackNotOptedIn
+#define MAYBE_SpecifyCrashEndpoint SpecifyCrashEndpoint
 #endif  // defined(ADDRESS_SANITIZER)
 
 IN_PROC_BROWSER_TEST_P(ReportingBrowserTest, MAYBE_CrashReport) {
@@ -814,6 +842,39 @@
   }
 }
 
+IN_PROC_BROWSER_TEST_P(ReportingBrowserTestSpecifyCrashEndpoint,
+                       MAYBE_SpecifyCrashEndpoint) {
+  content::WebContents* contents =
+      browser()->tab_strip_model()->GetActiveWebContents();
+
+  GURL main_url = server()->GetURL(
+      kReportingHost, "/set-header?" + GetAppropriateReportingHeader());
+  EXPECT_TRUE(NavigateToURL(contents, main_url));
+
+  // Simulate a crash on the page.
+  content::RenderProcessHostWatcher crash_observer(
+      contents, content::RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT);
+  contents->GetController().LoadURL(GURL(blink::kChromeUICrashURL),
+                                    content::Referrer(),
+                                    ui::PAGE_TRANSITION_TYPED, std::string());
+  crash_observer.Wait();
+
+  upload_response()->WaitForRequest();
+  base::Value::List response =
+      ParseReportUpload(upload_response()->http_request()->content);
+  upload_response()->Send("HTTP/1.1 200 OK\r\n");
+  upload_response()->Send("\r\n");
+  upload_response()->Done();
+
+  // Verify the contents of the report that we received.
+  const base::Value::Dict& report = response.begin()->GetDict();
+  const std::string* type = report.FindString("type");
+  const std::string* url = report.FindString("url");
+
+  EXPECT_EQ("crash", *type);
+  EXPECT_EQ(*url, main_url.spec());
+}
+
 IN_PROC_BROWSER_TEST_P(JSCallStackReportingBrowserTest, MAYBE_MainPageOptedIn) {
   content::WebContents* contents =
       browser()->tab_strip_model()->GetActiveWebContents();
@@ -1172,6 +1233,9 @@
                          ReportingBrowserTestMoreContextData,
                          ::testing::Bool());
 INSTANTIATE_TEST_SUITE_P(All,
+                         ReportingBrowserTestSpecifyCrashEndpoint,
+                         ::testing::Bool());
+INSTANTIATE_TEST_SUITE_P(All,
                          JSCallStackReportingBrowserTest,
                          ::testing::Bool());
 INSTANTIATE_TEST_SUITE_P(All, HistogramReportingBrowserTest, ::testing::Bool());
diff --git a/chrome/browser/new_tab_page/one_google_bar/one_google_bar_data.cc b/chrome/browser/new_tab_page/one_google_bar/one_google_bar_data.cc
index a16df6f..26fb7528 100644
--- a/chrome/browser/new_tab_page/one_google_bar/one_google_bar_data.cc
+++ b/chrome/browser/new_tab_page/one_google_bar/one_google_bar_data.cc
@@ -12,17 +12,3 @@
 OneGoogleBarData& OneGoogleBarData::operator=(const OneGoogleBarData&) =
     default;
 OneGoogleBarData& OneGoogleBarData::operator=(OneGoogleBarData&&) = default;
-
-bool operator==(const OneGoogleBarData& lhs, const OneGoogleBarData& rhs) {
-  return lhs.bar_html == rhs.bar_html &&
-         lhs.in_head_script == rhs.in_head_script &&
-         lhs.in_head_style == rhs.in_head_style &&
-         lhs.after_bar_script == rhs.after_bar_script &&
-         lhs.end_of_body_html == rhs.end_of_body_html &&
-         lhs.end_of_body_script == rhs.end_of_body_script &&
-         lhs.language_code == rhs.language_code;
-}
-
-bool operator!=(const OneGoogleBarData& lhs, const OneGoogleBarData& rhs) {
-  return !(lhs == rhs);
-}
diff --git a/chrome/browser/new_tab_page/one_google_bar/one_google_bar_data.h b/chrome/browser/new_tab_page/one_google_bar/one_google_bar_data.h
index 6aeec09..6ba5bb4 100644
--- a/chrome/browser/new_tab_page/one_google_bar/one_google_bar_data.h
+++ b/chrome/browser/new_tab_page/one_google_bar/one_google_bar_data.h
@@ -18,6 +18,9 @@
   OneGoogleBarData& operator=(const OneGoogleBarData&);
   OneGoogleBarData& operator=(OneGoogleBarData&&);
 
+  friend bool operator==(const OneGoogleBarData&,
+                         const OneGoogleBarData&) = default;
+
   // The main HTML for the bar itself.
   std::string bar_html;
 
@@ -32,7 +35,4 @@
   std::string language_code;
 };
 
-bool operator==(const OneGoogleBarData& lhs, const OneGoogleBarData& rhs);
-bool operator!=(const OneGoogleBarData& lhs, const OneGoogleBarData& rhs);
-
 #endif  // CHROME_BROWSER_NEW_TAB_PAGE_ONE_GOOGLE_BAR_ONE_GOOGLE_BAR_DATA_H_
diff --git a/chrome/browser/new_tab_page/promos/promo_data.cc b/chrome/browser/new_tab_page/promos/promo_data.cc
index 7abc3db3..0127d343 100644
--- a/chrome/browser/new_tab_page/promos/promo_data.cc
+++ b/chrome/browser/new_tab_page/promos/promo_data.cc
@@ -16,7 +16,3 @@
   return lhs.middle_slot_json == rhs.middle_slot_json &&
          lhs.promo_log_url == rhs.promo_log_url && lhs.promo_id == rhs.promo_id;
 }
-
-bool operator!=(const PromoData& lhs, const PromoData& rhs) {
-  return !(lhs == rhs);
-}
diff --git a/chrome/browser/new_tab_page/promos/promo_data.h b/chrome/browser/new_tab_page/promos/promo_data.h
index 4db7ae2..4c02ecb6 100644
--- a/chrome/browser/new_tab_page/promos/promo_data.h
+++ b/chrome/browser/new_tab_page/promos/promo_data.h
@@ -35,6 +35,5 @@
 };
 
 bool operator==(const PromoData& lhs, const PromoData& rhs);
-bool operator!=(const PromoData& lhs, const PromoData& rhs);
 
 #endif  // CHROME_BROWSER_NEW_TAB_PAGE_PROMOS_PROMO_DATA_H_
diff --git a/chrome/browser/optimization_guide/mock_optimization_guide_keyed_service.h b/chrome/browser/optimization_guide/mock_optimization_guide_keyed_service.h
index 5838343..03bae2c 100644
--- a/chrome/browser/optimization_guide/mock_optimization_guide_keyed_service.h
+++ b/chrome/browser/optimization_guide/mock_optimization_guide_keyed_service.h
@@ -32,6 +32,10 @@
 
   void Shutdown() override;
 
+  MOCK_METHOD(std::unique_ptr<optimization_guide::ModelBrokerClient>,
+              CreateModelBrokerClient,
+              (),
+              (override));
   MOCK_METHOD(void,
               RegisterOptimizationTypes,
               (const std::vector<optimization_guide::proto::OptimizationType>&),
diff --git a/chrome/browser/optimization_guide/optimization_guide_keyed_service.cc b/chrome/browser/optimization_guide/optimization_guide_keyed_service.cc
index 12487b2..db2f350 100644
--- a/chrome/browser/optimization_guide/optimization_guide_keyed_service.cc
+++ b/chrome/browser/optimization_guide/optimization_guide_keyed_service.cc
@@ -46,6 +46,7 @@
 #include "components/optimization_guide/core/command_line_top_host_provider.h"
 #include "components/optimization_guide/core/hints_processing_util.h"
 #include "components/optimization_guide/core/model_execution/feature_keys.h"
+#include "components/optimization_guide/core/model_execution/model_broker_client.h"
 #include "components/optimization_guide/core/model_execution/model_execution_features.h"
 #include "components/optimization_guide/core/model_execution/model_execution_features_controller.h"
 #include "components/optimization_guide/core/model_execution/model_execution_manager.h"
@@ -239,6 +240,16 @@
       ->BindBroker(std::move(receiver));
 }
 
+std::unique_ptr<optimization_guide::ModelBrokerClient>
+OptimizationGuideKeyedService::CreateModelBrokerClient() {
+  mojo::PendingRemote<optimization_guide::mojom::ModelBroker> remote;
+  GetOnDeviceModelServiceController(on_device_component_manager_->GetWeakPtr())
+      ->BindBroker(remote.InitWithNewPipeAndPassReceiver());
+  return std::make_unique<optimization_guide::ModelBrokerClient>(
+      std::move(remote), optimization_guide::CreateSessionArgs(
+                             optimization_guide_logger_->GetWeakPtr(), {}));
+}
+
 #if BUILDFLAG(IS_ANDROID)
 base::android::ScopedJavaLocalRef<jobject>
 OptimizationGuideKeyedService::GetJavaObject() {
diff --git a/chrome/browser/optimization_guide/optimization_guide_keyed_service.h b/chrome/browser/optimization_guide/optimization_guide_keyed_service.h
index 24d09d85..4efe844 100644
--- a/chrome/browser/optimization_guide/optimization_guide_keyed_service.h
+++ b/chrome/browser/optimization_guide/optimization_guide_keyed_service.h
@@ -16,6 +16,7 @@
 #include "chrome/browser/profiles/profile_observer.h"
 #include "components/keyed_service/core/keyed_service.h"
 #include "components/optimization_guide/core/model_execution/feature_keys.h"
+#include "components/optimization_guide/core/model_execution/model_broker_client.h"
 #include "components/optimization_guide/core/model_execution/model_execution_features_controller.h"
 #include "components/optimization_guide/core/model_execution/on_device_model_adaptation_loader.h"
 #include "components/optimization_guide/core/optimization_guide_decider.h"
@@ -122,6 +123,8 @@
   // Allow models to be subscribed via the broker.
   void BindModelBroker(
       mojo::PendingReceiver<optimization_guide::mojom::ModelBroker> receiver);
+  virtual std::unique_ptr<optimization_guide::ModelBrokerClient>
+  CreateModelBrokerClient();
 
   // optimization_guide::OptimizationGuideDecider implementation:
   void RegisterOptimizationTypes(
diff --git a/chrome/browser/permissions/BUILD.gn b/chrome/browser/permissions/BUILD.gn
index c674077..208a3ef 100644
--- a/chrome/browser/permissions/BUILD.gn
+++ b/chrome/browser/permissions/BUILD.gn
@@ -75,6 +75,7 @@
     "//chrome/browser/profiles:profiles",
     "//chrome/browser/safe_browsing",
     "//chrome/browser/search_engines",
+    "//chrome/browser/serial",
     "//chrome/browser/storage_access_api",
     "//chrome/browser/top_level_storage_access_api:permissions",
     "//chrome/browser/ui/hats",
diff --git a/chrome/browser/policy/cloud/user_policy_signin_service_unittest.cc b/chrome/browser/policy/cloud/user_policy_signin_service_unittest.cc
index 516eaf4d..bf4a45c7 100644
--- a/chrome/browser/policy/cloud/user_policy_signin_service_unittest.cc
+++ b/chrome/browser/policy/cloud/user_policy_signin_service_unittest.cc
@@ -27,6 +27,7 @@
 #include "chrome/browser/signin/identity_test_environment_profile_adaptor.h"
 #include "chrome/browser/signin/signin_util.h"
 #include "chrome/browser/signin/test_signin_client_builder.h"
+#include "chrome/test/base/scoped_testing_local_state.h"
 #include "chrome/test/base/testing_browser_process.h"
 #include "chrome/test/base/testing_profile.h"
 #include "components/account_id/account_id.h"
@@ -137,14 +138,12 @@
     UserPolicySigninServiceFactory::SetDeviceManagementServiceForTesting(
         &device_management_service_);
 
-    local_state_ = std::make_unique<TestingPrefServiceSimple>();
-    RegisterLocalState(local_state_->registry());
-    TestingBrowserProcess::GetGlobal()->SetLocalState(local_state_.get());
     TestingBrowserProcess::GetGlobal()->SetSharedURLLoaderFactory(
         test_url_loader_factory_.GetSafeWeakWrapper());
 
     g_browser_process->browser_policy_connector()->Init(
-        local_state_.get(), test_url_loader_factory_.GetSafeWeakWrapper());
+        scoped_testing_local_state_.Get(),
+        test_url_loader_factory_.GetSafeWeakWrapper());
 
     // Create a testing profile with cloud-policy-on-signin enabled, and bring
     // up a UserCloudPolicyManager with a MockUserCloudPolicyStore.
@@ -187,8 +186,6 @@
     profile_.reset();
     TestingBrowserProcess* testing_browser_process =
         TestingBrowserProcess::GetGlobal();
-    testing_browser_process->SetLocalState(NULL);
-    local_state_.reset();
     testing_browser_process->ShutdownBrowserPolicyConnector();
     base::RunLoop run_loop;
     run_loop.RunUntilIdle();
@@ -324,7 +321,8 @@
   FakeDeviceManagementService device_management_service_{
       &job_creation_handler_};
 
-  std::unique_ptr<TestingPrefServiceSimple> local_state_;
+  ScopedTestingLocalState scoped_testing_local_state_{
+      TestingBrowserProcess::GetGlobal()};
   network::TestURLLoaderFactory test_url_loader_factory_;
 
   base::test::ScopedFeatureList scoped_feature_list_;
diff --git a/chrome/browser/policy/configuration_policy_handler_list_factory.cc b/chrome/browser/policy/configuration_policy_handler_list_factory.cc
index 4d3600f..7ec1d8a 100644
--- a/chrome/browser/policy/configuration_policy_handler_list_factory.cc
+++ b/chrome/browser/policy/configuration_policy_handler_list_factory.cc
@@ -2392,8 +2392,8 @@
     base::Value::Type::BOOLEAN },
 #endif  // BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_FUCHSIA)
 #if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_WIN)
-  { key::kNTPFooterThemeAttributionEnabled,
-    prefs::kNTPFooterThemeAttributionEnabled,
+  { key::kNTPFooterExtensionAttributionEnabled,
+    prefs::kNTPFooterExtensionAttributionEnabled,
     base::Value::Type::BOOLEAN },
   { key::kNTPFooterManagementNoticeEnabled,
     prefs::kNTPFooterManagementNoticeEnabled,
diff --git a/chrome/browser/promos/promos_utils_unittest.cc b/chrome/browser/promos/promos_utils_unittest.cc
index fb47d79..b6af6294 100644
--- a/chrome/browser/promos/promos_utils_unittest.cc
+++ b/chrome/browser/promos/promos_utils_unittest.cc
@@ -27,18 +27,10 @@
 class IOSPromoOnDesktopTest : public ::testing::Test {
  public:
   void SetUp() override {
-    local_state_.registry()->RegisterBooleanPref(prefs::kPromotionsEnabled,
-                                                 true);
-    TestingBrowserProcess::GetGlobal()->SetLocalState(&local_state_);
-
     sync_service_.GetUserSettings()->SetSelectedTypes(/*sync_everything=*/true,
                                                       {});
   }
 
-  void TearDown() override {
-    TestingBrowserProcess::GetGlobal()->SetLocalState(nullptr);
-  }
-
   // Getter for the test syncable prefs service.
   sync_preferences::TestingPrefServiceSyncable* prefs() {
     return profile()->GetTestingPrefService();
@@ -60,7 +52,8 @@
   syncer::TestSyncService* sync_service() { return &sync_service_; }
 
  protected:
-  TestingPrefServiceSimple local_state_;
+  ScopedTestingLocalState scoped_testing_local_state_{
+      TestingBrowserProcess::GetGlobal()};
 
  private:
   content::BrowserTaskEnvironment task_environment_{
@@ -253,7 +246,8 @@
 // disabled.
 TEST_F(IOSPromoOnDesktopTest,
        ShouldShowIOSDesktopPromoTestFalsePromotionsDisabled) {
-  local_state_.SetBoolean(prefs::kPromotionsEnabled, false);
+  scoped_testing_local_state_.Get()->SetBoolean(prefs::kPromotionsEnabled,
+                                                false);
   EXPECT_FALSE(ShouldShowIOSDesktopPromo(profile(), sync_service(),
                                          IOSPromoType::kPassword));
 }
@@ -968,7 +962,8 @@
 // disabled.
 TEST_F(IOSPromoOnDesktopTest,
        ShouldShowIOSDesktopNtpPromoFalsePromotionsDisabled) {
-  local_state_.SetBoolean(prefs::kPromotionsEnabled, false);
+  scoped_testing_local_state_.Get()->SetBoolean(prefs::kPromotionsEnabled,
+                                                false);
   EXPECT_FALSE(ShouldShowIOSDesktopNtpPromo(profile(), sync_service()));
 }
 
diff --git a/chrome/browser/renderer_context_menu/link_to_text_menu_observer.cc b/chrome/browser/renderer_context_menu/link_to_text_menu_observer.cc
index 7effe7a..fcbd64e6 100644
--- a/chrome/browser/renderer_context_menu/link_to_text_menu_observer.cc
+++ b/chrome/browser/renderer_context_menu/link_to_text_menu_observer.cc
@@ -28,9 +28,11 @@
 #include "content/public/browser/browser_context.h"
 #include "content/public/browser/clipboard_types.h"
 #include "content/public/browser/context_menu_params.h"
+#include "content/public/browser/render_frame_host.h"
 #include "content/public/browser/render_view_host.h"
 #include "content/public/browser/web_contents.h"
 #include "extensions/browser/process_manager.h"
+#include "third_party/blink/public/mojom/annotation/annotation.mojom.h"
 #include "ui/base/clipboard/scoped_clipboard_writer.h"
 #include "ui/base/data_transfer_policy/data_transfer_endpoint.h"
 #include "ui/base/l10n/l10n_util.h"
@@ -115,22 +117,36 @@
   } else {
     url_ = params.page_url;
   }
+  annotation_type_ = params.annotation_type;
 
-  // It is possible that there is a new text selection on top of a highlight, in
-  // which case, both open_from_new_selection_ and opened_from_highlight are
-  // true. Consequently, a context menu for new text selection is created.
+  // It is possible that there is a new text selection on top of an annotation,
+  // in which case, both `open_from_new_selection_` and
+  // `annotation_type_.has_value()` are true. Consequently, a context menu for
+  // new text selection is created.
   if (open_from_new_selection_) {
     proxy_->AddMenuItem(
         IDC_CONTENT_CONTEXT_COPYLINKTOTEXT,
         l10n_util::GetStringUTF16(IDS_CONTENT_CONTEXT_COPYLINKTOTEXT));
     RequestLinkGeneration();
-  } else if (params.opened_from_highlight) {
-    proxy_->AddMenuItem(
-        IDC_CONTENT_CONTEXT_RESHARELINKTOTEXT,
-        l10n_util::GetStringUTF16(IDS_CONTENT_CONTEXT_RESHARELINKTOTEXT));
-    proxy_->AddMenuItem(
-        IDC_CONTENT_CONTEXT_REMOVELINKTOTEXT,
-        l10n_util::GetStringUTF16(IDS_CONTENT_CONTEXT_REMOVELINKTOTEXT));
+  } else if (annotation_type_.has_value()) {
+    switch (annotation_type_.value()) {
+      case blink::mojom::AnnotationType::kSharedHighlight:
+        proxy_->AddMenuItem(
+            IDC_CONTENT_CONTEXT_RESHARELINKTOTEXT,
+            l10n_util::GetStringUTF16(IDS_CONTENT_CONTEXT_RESHARELINKTOTEXT));
+        proxy_->AddMenuItem(
+            IDC_CONTENT_CONTEXT_REMOVELINKTOTEXT,
+            l10n_util::GetStringUTF16(IDS_CONTENT_CONTEXT_REMOVELINKTOTEXT));
+        break;
+      case blink::mojom::AnnotationType::kGlic:
+        proxy_->AddMenuItem(
+            IDC_CONTENT_CONTEXT_REMOVELINKTOTEXT,
+            l10n_util::GetStringUTF16(IDS_CONTENT_CONTEXT_REMOVELINKTOTEXT));
+        break;
+      case blink::mojom::AnnotationType::kTextFinder:
+      case blink::mojom::AnnotationType::kUserNote:
+        NOTIMPLEMENTED();
+    }
   }
 }
 
@@ -372,9 +388,28 @@
 }
 
 void LinkToTextMenuObserver::RemoveHighlights() {
-  // Remove highlights from all frames in the primary page.
-  proxy_->GetWebContents()->GetPrimaryMainFrame()->ForEachRenderFrameHost(
-      &RemoveHighlightsInFrame);
+  CHECK(annotation_type_.has_value());
+  switch (annotation_type_.value()) {
+    case blink::mojom::AnnotationType::kSharedHighlight:
+      proxy_->GetWebContents()->GetPrimaryMainFrame()->ForEachRenderFrameHost(
+          &RemoveHighlightsInFrame);
+      return;
+    case blink::mojom::AnnotationType::kGlic: {
+      mojo::Remote<blink::mojom::AnnotationAgentContainer>
+          annotation_agent_container;
+      proxy_->GetWebContents()
+          ->GetPrimaryMainFrame()
+          ->GetRemoteInterfaces()
+          ->GetInterface(
+              annotation_agent_container.BindNewPipeAndPassReceiver());
+      annotation_agent_container->RemoveAgentsOfType(
+          blink::mojom::AnnotationType::kGlic);
+      return;
+    }
+    case blink::mojom::AnnotationType::kTextFinder:
+    case blink::mojom::AnnotationType::kUserNote:
+      NOTIMPLEMENTED();
+  }
 }
 
 mojo::Remote<blink::mojom::TextFragmentReceiver>&
diff --git a/chrome/browser/renderer_context_menu/link_to_text_menu_observer.h b/chrome/browser/renderer_context_menu/link_to_text_menu_observer.h
index 0baac53..a946dff 100644
--- a/chrome/browser/renderer_context_menu/link_to_text_menu_observer.h
+++ b/chrome/browser/renderer_context_menu/link_to_text_menu_observer.h
@@ -10,6 +10,7 @@
 #include "components/shared_highlighting/core/common/shared_highlighting_metrics.h"
 #include "content/public/browser/render_frame_host.h"
 #include "services/service_manager/public/cpp/interface_provider.h"
+#include "third_party/blink/public/mojom/annotation/annotation.mojom-shared.h"
 #include "third_party/blink/public/mojom/link_to_text/link_to_text.mojom.h"
 #include "url/gurl.h"
 
@@ -117,6 +118,11 @@
   // True when generation is completed.
   bool is_generation_complete_ = false;
 
+  // Set when the context menu was opened with an annotation (with a value
+  // corresponding to the type of annotation). We show different menu items
+  // based on the type.
+  std::optional<blink::mojom::AnnotationType> annotation_type_;
+
   base::WeakPtrFactory<LinkToTextMenuObserver> weak_ptr_factory_{this};
 };
 
diff --git a/chrome/browser/renderer_context_menu/link_to_text_menu_observer_interactive_uitest.cc b/chrome/browser/renderer_context_menu/link_to_text_menu_observer_interactive_uitest.cc
index 22d8dc1e..a97c955b 100644
--- a/chrome/browser/renderer_context_menu/link_to_text_menu_observer_interactive_uitest.cc
+++ b/chrome/browser/renderer_context_menu/link_to_text_menu_observer_interactive_uitest.cc
@@ -26,8 +26,11 @@
 #include "content/public/test/browser_test.h"
 #include "content/public/test/browser_test_utils.h"
 #include "extensions/browser/process_manager.h"
+#include "mojo/public/cpp/system/message_pipe.h"
 #include "net/dns/mock_host_resolver.h"
 #include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/public/mojom/annotation/annotation.mojom-shared.h"
+#include "third_party/blink/public/mojom/annotation/annotation.mojom-test-utils.h"
 #include "ui/base/clipboard/clipboard.h"
 
 class MockLinkToTextMenuObserver : public LinkToTextMenuObserver {
@@ -83,6 +86,46 @@
   }
 };
 
+class MockAnnotationAgentContainer
+    : public blink::mojom::AnnotationAgentContainerInterceptorForTesting {
+ public:
+  MockAnnotationAgentContainer() : receiver_(this) {}
+
+  // Creates and returns a MockAnnotationAgentContainer instance and installs a
+  // binder to the new instance in `rfh`'s InterfaceProvider (overwriting the
+  // previous binder).
+  static std::unique_ptr<MockAnnotationAgentContainer>
+  InstallMockAnnotationAgentContainer(content::RenderFrameHost* rfh) {
+    service_manager::InterfaceProvider::TestApi test_api(
+        rfh->GetRemoteInterfaces());
+    auto mock = std::make_unique<MockAnnotationAgentContainer>();
+    test_api.SetBinderForName(
+        blink::mojom::AnnotationAgentContainer::Name_,
+        base::BindRepeating(&MockAnnotationAgentContainer::Bind,
+                            base::Unretained(mock.get())));
+    return mock;
+  }
+
+  void Bind(mojo::ScopedMessagePipeHandle handle) {
+    receiver_.Bind(
+        mojo::PendingReceiver<blink::mojom::AnnotationAgentContainer>(
+            std::move(handle)));
+  }
+
+  void FlushForTesting() { receiver_.FlushForTesting(); }
+
+  // blink::mojom::AnnotationAgentContainer overrides
+  MOCK_METHOD(void, RemoveAgentsOfType, (blink::mojom::AnnotationType));
+
+  // blink::mojom::AnnotationAgentContainerInterceptorForTesting overrides
+  blink::mojom::AnnotationAgentContainer* GetForwardingInterface() override {
+    NOTREACHED();
+  }
+
+ private:
+  mojo::Receiver<blink::mojom::AnnotationAgentContainer> receiver_;
+};
+
 namespace {
 
 class LinkToTextMenuObserverTest : public extensions::ExtensionBrowserTest {
@@ -168,7 +211,7 @@
 IN_PROC_BROWSER_TEST_F(LinkToTextMenuObserverTest, AddsCopyAndRemoveMenuItems) {
   content::ContextMenuParams params;
   params.page_url = GURL("http://foo.com/");
-  params.opened_from_highlight = true;
+  params.annotation_type = blink::mojom::AnnotationType::kSharedHighlight;
   observer()->SetGenerationResults(
       std::string(), shared_highlighting::LinkGenerationError::kEmptySelection,
       shared_highlighting::LinkGenerationReadyStatus::kRequestedAfterReady);
@@ -294,7 +337,7 @@
   content::ContextMenuParams params;
   params.page_url = GURL("http://foo.com/");
   params.selection_text = u"hello world";
-  params.opened_from_highlight = true;
+  params.annotation_type = blink::mojom::AnnotationType::kSharedHighlight;
   observer()->SetGenerationResults(
       "hello%20world", shared_highlighting::LinkGenerationError::kNone,
       shared_highlighting::LinkGenerationReadyStatus::kRequestedAfterReady);
@@ -339,7 +382,7 @@
   content::ContextMenuParams params;
   params.page_url = GURL("http://foo.com/#:~:text=hello%20world");
   params.selection_text = u"";
-  params.opened_from_highlight = true;
+  params.annotation_type = blink::mojom::AnnotationType::kSharedHighlight;
   observer()->SetReshareSelector("hello%20world");
   InitMenu(params);
   menu()->ExecuteCommand(IDC_CONTENT_CONTEXT_RESHARELINKTOTEXT, 0);
@@ -672,7 +715,7 @@
   content::ContextMenuParams params;
   params.page_url = GURL("http://foo.com/");
   params.selection_text = u"hello world";
-  params.opened_from_highlight = true;
+  params.annotation_type = blink::mojom::AnnotationType::kSharedHighlight;
   observer()->SetGenerationResults(
       "hello%20world", shared_highlighting::LinkGenerationError::kNone,
       shared_highlighting::LinkGenerationReadyStatus::kRequestedAfterReady);
@@ -681,3 +724,39 @@
 
   EXPECT_TRUE(browser()->GetFeatures().toast_controller()->IsShowingToast());
 }
+
+IN_PROC_BROWSER_TEST_F(LinkToTextMenuObserverTest,
+                       AddsRemoveMenuItemForGlicHighlight) {
+  content::ContextMenuParams params;
+  params.page_url = GURL("http://foo.com/");
+  params.annotation_type = blink::mojom::AnnotationType::kGlic;
+  InitMenu(params);
+  EXPECT_EQ(1u, menu()->GetMenuSize());
+  MockRenderViewContextMenu::MockMenuItem item;
+
+  // Check Remove item.
+  menu()->GetMenuItem(0, &item);
+  EXPECT_EQ(IDC_CONTENT_CONTEXT_REMOVELINKTOTEXT, item.command_id);
+  EXPECT_FALSE(item.checked);
+  EXPECT_FALSE(item.hidden);
+  EXPECT_TRUE(item.enabled);
+}
+
+IN_PROC_BROWSER_TEST_F(LinkToTextMenuObserverTest, RemovesGlicHighlight) {
+  content::BrowserTestClipboardScope test_clipboard_scope;
+  content::ContextMenuParams params;
+  params.page_url = GURL("http://foo.com/");
+  params.annotation_type = blink::mojom::AnnotationType::kGlic;
+  InitMenu(params);
+  std::unique_ptr<MockAnnotationAgentContainer>
+      mock_annotation_agent_container =
+          MockAnnotationAgentContainer::InstallMockAnnotationAgentContainer(
+              browser()
+                  ->tab_strip_model()
+                  ->GetActiveWebContents()
+                  ->GetPrimaryMainFrame());
+  EXPECT_CALL(*mock_annotation_agent_container,
+              RemoveAgentsOfType(blink::mojom::AnnotationType::kGlic));
+  menu()->ExecuteCommand(IDC_CONTENT_CONTEXT_REMOVELINKTOTEXT, 0);
+  mock_annotation_agent_container->FlushForTesting();
+}
diff --git a/chrome/browser/renderer_context_menu/render_view_context_menu.cc b/chrome/browser/renderer_context_menu/render_view_context_menu.cc
index 1b785aa..c2fcff7 100644
--- a/chrome/browser/renderer_context_menu/render_view_context_menu.cc
+++ b/chrome/browser/renderer_context_menu/render_view_context_menu.cc
@@ -1532,7 +1532,7 @@
         "ContextMenu.SelectedOptionDesktop.MisspelledWord", enum_id,
         GetUmaValueMax(UmaEnumIdLookupType::ContextSpecificEnumId));
   } else if ((!params_.selection_text.empty() ||
-              params_.opened_from_highlight) &&
+              params_.annotation_type.has_value()) &&
              params_.media_type == ContextMenuDataMediaType::kNone) {
     // Probably just text.
     UMA_HISTOGRAM_EXACT_LINEAR(
@@ -4438,7 +4438,7 @@
   LensSearchController* const controller =
       LensSearchController::FromTabWebContents(source_web_contents_);
   CHECK(controller);
-  controller->OpenLensOverlayWithPendingRegion(
+  controller->OpenLensOverlayWithPendingRegionFromBounds(
       lens::LensOverlayInvocationSource::kContentAreaContextMenuImage,
       tab_bounds, view_bounds, scaled_region_bounds, region_bitmap);
 }
diff --git a/chrome/browser/resources/glic/glic_api/glic_api.ts b/chrome/browser/resources/glic/glic_api/glic_api.ts
index 5b9bfa8..103fcbe5 100644
--- a/chrome/browser/resources/glic/glic_api/glic_api.ts
+++ b/chrome/browser/resources/glic/glic_api/glic_api.ts
@@ -236,6 +236,14 @@
   closePanel?(): Promise<void>;
 
   /**
+   * Similar to closePanel but also requests that the web client be torn down.
+   * Normally, Chrome manages creation and destruction of the web client. This
+   * function is a fallback solution to permit the web client to limit its
+   * lifetime, if needed.
+   */
+  closePanelAndShutdown?(): void;
+
+  /**
    * @deprecated The panel will only maintain the detached state.
    *
    * Requests that the web client's panel be attached to a browser window.
diff --git a/chrome/browser/resources/glic/glic_api_impl/glic_api_client.ts b/chrome/browser/resources/glic/glic_api_impl/glic_api_client.ts
index 39807366..906c637d 100644
--- a/chrome/browser/resources/glic/glic_api_impl/glic_api_client.ts
+++ b/chrome/browser/resources/glic/glic_api_impl/glic_api_client.ts
@@ -316,6 +316,11 @@
     return this.sender.requestWithResponse('glicBrowserClosePanel', undefined);
   }
 
+  closePanelAndShutdown(): void {
+    this.sender.requestNoResponse(
+        'glicBrowserClosePanelAndShutdown', undefined);
+  }
+
   attachPanel?(): void {
     this.sender.requestNoResponse('glicBrowserAttachPanel', undefined);
   }
diff --git a/chrome/browser/resources/glic/glic_api_impl/glic_api_host.ts b/chrome/browser/resources/glic/glic_api_impl/glic_api_host.ts
index b86fd33..5c4c02c 100644
--- a/chrome/browser/resources/glic/glic_api_impl/glic_api_host.ts
+++ b/chrome/browser/resources/glic/glic_api_impl/glic_api_host.ts
@@ -304,6 +304,10 @@
     return this.handler.closePanel();
   }
 
+  glicBrowserClosePanelAndShutdown(): void {
+    this.handler.closePanelAndShutdown();
+  }
+
   glicBrowserAttachPanel(): void {
     this.handler.attachPanel();
   }
diff --git a/chrome/browser/resources/glic/glic_api_impl/request_types.ts b/chrome/browser/resources/glic/glic_api_impl/request_types.ts
index 46bba39f..5dd39dc 100644
--- a/chrome/browser/resources/glic/glic_api_impl/request_types.ts
+++ b/chrome/browser/resources/glic/glic_api_impl/request_types.ts
@@ -61,6 +61,7 @@
     request: {options?: OpenSettingsOptions},
   };
   glicBrowserClosePanel: {};
+  glicBrowserClosePanelAndShutdown: {};
   glicBrowserShowProfilePicker: {};
   glicBrowserGetContextFromFocusedTab: {
     request: {
@@ -285,6 +286,7 @@
     CreateTab: 0,
     OpenGlicSettingsPage: 0,
     ClosePanel: 0,
+    ClosePanelAndShutdown: 0,
     ShowProfilePicker: 0,
     GetContextFromFocusedTab: 0,
     ActInFocusedTab: 0,
diff --git a/chrome/browser/resources/pdf/constants.ts b/chrome/browser/resources/pdf/constants.ts
index d50ea68..ab45b06 100644
--- a/chrome/browser/resources/pdf/constants.ts
+++ b/chrome/browser/resources/pdf/constants.ts
@@ -44,6 +44,9 @@
   // and is in page coordinates when this annotation is sent or received in
   // a message to/from the plugin.
   textBoxRect: TextBoxRect;
+  // Orientation of the text in the box relative to the PDF page, in number of
+  // clockwise rotations from 0 to 3.
+  textOrientation: number;
 }
 
 export enum TextAlignment {
diff --git a/chrome/browser/resources/pdf/elements/ink_text_box.css b/chrome/browser/resources/pdf/elements/ink_text_box.css
index dbf7752..f3ef6c22 100644
--- a/chrome/browser/resources/pdf/elements/ink_text_box.css
+++ b/chrome/browser/resources/pdf/elements/ink_text_box.css
@@ -93,3 +93,15 @@
   padding: calc(var(--handle-size) / 2 + 2px);
   resize: none;
 }
+
+:host([text-rotations_="1"]) textarea {
+  writing-mode: vertical-rl;
+}
+
+:host([text-rotations_="2"]) textarea {
+  transform: rotate(180deg);
+}
+
+:host([text-rotations_="3"]) textarea {
+  writing-mode: sideways-lr;
+}
diff --git a/chrome/browser/resources/pdf/elements/ink_text_box.ts b/chrome/browser/resources/pdf/elements/ink_text_box.ts
index 5dd8bc3..3dcee636 100644
--- a/chrome/browser/resources/pdf/elements/ink_text_box.ts
+++ b/chrome/browser/resources/pdf/elements/ink_text_box.ts
@@ -8,7 +8,7 @@
 import type {PropertyValues} from 'chrome://resources/lit/v3_0/lit.rollup.js';
 
 import type {TextAttributes, TextBoxRect} from '../constants.js';
-import {colorsEqual, Ink2Manager, stylesEqual} from '../ink2_manager.js';
+import {colorsEqual, convertRotatedCoordinates, Ink2Manager, stylesEqual} from '../ink2_manager.js';
 import type {TextBoxInit, ViewportParams} from '../ink2_manager.js';
 import {colorToHex} from '../pdf_viewer_utils.js';
 
@@ -57,7 +57,13 @@
       locationY_: {type: Number},
       minHeight_: {type: Number},
       state_: {type: Number},
+      textOrientation_: {type: Number},
+      textRotations_: {
+        type: Number,
+        reflect: true,
+      },
       textValue_: {type: String},
+      viewportRotations_: {type: Number},
       width_: {type: Number},
       zoom_: {type: Number},
     };
@@ -69,8 +75,11 @@
   private accessor locationY_: number = 0;
   private accessor minHeight_: number = 0;
   private accessor height_: number = 0;
-  protected accessor textValue_: string = '';
   private accessor state_: TextBoxState = TextBoxState.INACTIVE;
+  private accessor textOrientation_: number = 0;
+  protected accessor textRotations_: number = 0;
+  protected accessor textValue_: string = '';
+  private accessor viewportRotations_: number = 0;
   private accessor width_: number = 0;
   private accessor zoom_: number = 1.0;
 
@@ -133,6 +142,12 @@
       this.hidden = this.state_ === TextBoxState.INACTIVE;
       this.fire('state-changed', this.state_);
     }
+
+    if (changedPrivateProperties.has('viewportRotations_') ||
+        changedPrivateProperties.has('textOrientation_')) {
+      this.textRotations_ =
+          (this.viewportRotations_ + this.textOrientation_) % 4;
+    }
   }
 
   override updated(changedProperties: PropertyValues<this>) {
@@ -212,6 +227,7 @@
             locationY: this.locationY_,
             width: this.width_,
           },
+          textOrientation: this.textOrientation_,
         },
         this.state_ === TextBoxState.EDITED);
 
@@ -239,34 +255,35 @@
         data.annotation.text === '' ? 'Sample Text' : data.annotation.text;
     this.id_ = data.annotation.id;
     this.pageNumber_ = data.annotation.pageNumber;
+    this.textOrientation_ = data.annotation.textOrientation;
     this.updateTextAttributes_(data.annotation.textAttributes);
   }
 
   private onViewportChanged_(update: ViewportParams) {
     // Convert width, height, locationX, locationY to the new screen
     // coordinates.
-    if (update.zoom !== this.zoom_) {
-      this.width_ =
-          Math.max(this.width_ * update.zoom / this.zoom_, MIN_WIDTH_PX);
-      this.height_ = this.height_ * update.zoom / this.zoom_;
-    }
 
-    if (update.zoom !== this.zoom_ || update.pageX !== this.pageX_ ||
-        update.pageY !== this.pageY_) {
-      // Note that this.pageX_ and this.pageY_ are in the old screen
-      // coordinates, i.e. they were using the old zoom value.
-      this.locationX_ =
-          (this.locationX_ - this.pageX_) * update.zoom / this.zoom_ +
-          update.pageX;
-      this.locationY_ =
-          (this.locationY_ - this.pageY_) * update.zoom / this.zoom_ +
-          update.pageY;
-    }
+    // Note that this.pageX_ and this.pageY_ are in the old screen
+    // coordinates, i.e. they were using the old zoom value.
+    const adjusted = {
+      locationX: (this.locationX_ - this.pageX_) * update.zoom / this.zoom_,
+      locationY: (this.locationY_ - this.pageY_) * update.zoom / this.zoom_,
+      width: Math.max(this.width_ * update.zoom / this.zoom_, MIN_WIDTH_PX),
+      height: this.height_ * update.zoom / this.zoom_,
+    };
+    const rotated = convertRotatedCoordinates(
+        adjusted, this.viewportRotations_, update.clockwiseRotations,
+        update.pageDimensions.width, update.pageDimensions.height);
+    this.locationX_ = rotated.locationX + update.pageDimensions.x;
+    this.locationY_ = rotated.locationY + update.pageDimensions.y;
+    this.width_ = rotated.width;
+    this.height_ = rotated.height;
 
     // Update properties to the new values.
+    this.viewportRotations_ = update.clockwiseRotations;
     this.zoom_ = update.zoom;
-    this.pageX_ = update.pageX;
-    this.pageY_ = update.pageY;
+    this.pageX_ = update.pageDimensions.x;
+    this.pageY_ = update.pageDimensions.y;
   }
 
   protected onPointerDown_(e: PointerEvent) {
diff --git a/chrome/browser/resources/pdf/ink2_manager.ts b/chrome/browser/resources/pdf/ink2_manager.ts
index cd6d5fd6..ad92a061 100644
--- a/chrome/browser/resources/pdf/ink2_manager.ts
+++ b/chrome/browser/resources/pdf/ink2_manager.ts
@@ -2,18 +2,18 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import {assert} from 'chrome://resources/js/assert.js';
+import {assert, assertNotReached} from 'chrome://resources/js/assert.js';
 import {PromiseResolver} from 'chrome://resources/js/promise_resolver.js';
 import {isRTL} from 'chrome://resources/js/util.js';
 
 import type {AnnotationBrush, Color, Point, TextAnnotation, TextAttributes, TextBoxRect, TextStyles} from './constants.js';
 import {AnnotationBrushType, TextAlignment, TextStyle, TextTypeface} from './constants.js';
 import {PluginController, PluginControllerEventType} from './controller.js';
-import type {Viewport} from './viewport.js';
+import type {Viewport, ViewportRect} from './viewport.js';
 
 export interface ViewportParams {
-  pageX: number;
-  pageY: number;
+  clockwiseRotations: number;
+  pageDimensions: ViewportRect;
   zoom: number;
 }
 
@@ -34,6 +34,74 @@
   return style1.bold === style2.bold && style1.italic === style2.italic;
 }
 
+/**
+ * Converts `rect` from `oldRotations` clockwise rotations to `newRotations`
+ * clockwise rotations. `newPageWidth` should be the page width in
+ * `newRotations` coordinates, and `newPageHeight` should be the page height in
+ * `newRotations` coordinates.
+ */
+export function convertRotatedCoordinates(
+    rect: TextBoxRect, oldRotations: number, newRotations: number,
+    newPageWidth: number, newPageHeight: number): TextBoxRect {
+  const pageWidthNR = newRotations % 2 === 0 ? newPageWidth : newPageHeight;
+  const pageHeightNR = newRotations % 2 === 0 ? newPageHeight : newPageWidth;
+  const nonRotated: TextBoxRect = {
+    locationX: rect.locationX,
+    locationY: rect.locationY,
+    width: oldRotations % 2 === 0 ? rect.width : rect.height,
+    height: oldRotations % 2 === 0 ? rect.height : rect.width,
+  };
+  switch (oldRotations % 4) {
+    case 0:
+      // Already populated correctly.
+      break;
+    case 1:
+      nonRotated.locationX = rect.locationY;
+      nonRotated.locationY = pageHeightNR - rect.locationX - rect.width;
+      break;
+    case 2:
+      nonRotated.locationX = pageWidthNR - rect.locationX - rect.width;
+      nonRotated.locationY = pageHeightNR - rect.locationY - rect.height;
+      break;
+    case 3:
+      nonRotated.locationX = pageWidthNR - rect.locationY - rect.height;
+      nonRotated.locationY = rect.locationX;
+      break;
+    default:
+      assertNotReached();
+  }
+
+  const newRotated = {
+    locationX: nonRotated.locationX,
+    locationY: nonRotated.locationY,
+    width: newRotations % 2 === 0 ? nonRotated.width : nonRotated.height,
+    height: newRotations % 2 === 0 ? nonRotated.height : nonRotated.width,
+  };
+  switch (newRotations % 4) {
+    case 0:
+      break;
+    case 1:
+      newRotated.locationX =
+          pageHeightNR - nonRotated.locationY - nonRotated.height;
+      newRotated.locationY = nonRotated.locationX;
+      break;
+    case 2:
+      newRotated.locationX =
+          pageWidthNR - nonRotated.locationX - nonRotated.width;
+      newRotated.locationY =
+          pageHeightNR - nonRotated.locationY - nonRotated.height;
+      break;
+    case 3:
+      newRotated.locationX = nonRotated.locationY;
+      newRotated.locationY =
+          pageWidthNR - nonRotated.locationX - nonRotated.width;
+      break;
+    default:
+      assertNotReached();
+  }
+  return newRotated;
+}
+
 export class Ink2Manager extends EventTarget {
   private brush_: AnnotationBrush = {type: AnnotationBrushType.PEN};
   // Map from page numbers to annotations on that page.
@@ -58,7 +126,11 @@
   private pageNumber_: number = -1;
   private pluginController_: PluginController = PluginController.getInstance();
   private viewport_: Viewport|null = null;
-  private viewportParams_: ViewportParams = {pageX: 0, pageY: 0, zoom: 1.0};
+  private viewportParams_: ViewportParams = {
+    clockwiseRotations: 0,
+    pageDimensions: {x: 0, y: 0, width: 0, height: 0},
+    zoom: 1.0,
+  };
   private nextAnnotationId_: number = 0;
 
   setViewport(viewport: Viewport) {
@@ -90,7 +162,6 @@
       return;
     }
 
-    const zoom = this.viewport_.getZoom();
     const pageDimensions = this.viewport_.getPageScreenRect(page);
     // Is the click in an existing box?
     let existing = null;
@@ -100,16 +171,16 @@
         annotationsMap ? Array.from(annotationsMap.values()) : [];
     for (const annotation of annotations) {
       // Convert box to screen coordinates.
-      const x = annotation.textBoxRect.locationX * zoom + pageDimensions.x;
-      const width = annotation.textBoxRect.width * zoom;
-      const y = annotation.textBoxRect.locationY * zoom + pageDimensions.y;
-      const height = annotation.textBoxRect.height * zoom;
-      if (location.x >= x && location.x <= (x + width) && location.y >= y &&
-          location.y <= (y + height)) {
+      const screenBox =
+          this.pageToScreenCoordinates_(page, annotation.textBoxRect);
+      if (location.x >= screenBox.locationX &&
+          location.x <= (screenBox.locationX + screenBox.width) &&
+          location.y >= screenBox.locationY &&
+          location.y <= (screenBox.locationY + screenBox.height)) {
         // Don't update the original. Create a new object and update its
         // rectangle to use the computed screen coordinates.
         existing = structuredClone(annotation);
-        existing.textBoxRect = {height, locationX: x, locationY: y, width};
+        existing.textBoxRect = screenBox;
         break;
       }
     }
@@ -126,6 +197,7 @@
         locationY: location.y,
         width: DEFAULT_TEXTBOX_WIDTH,
       },
+      textOrientation: (4 - this.viewport_.getClockwiseRotations()) % 4,
     };
 
     if (existing) {
@@ -158,17 +230,21 @@
     const zoom = this.viewport_.getZoom();
     const page = this.pageNumber_ !== -1 ? this.pageNumber_ :
                                            this.viewport_.getMostVisiblePage();
-    const visiblePageDimensions = this.viewport_.getPageScreenRect(page);
-    if (visiblePageDimensions.x === this.viewportParams_.pageX &&
-        visiblePageDimensions.y === this.viewportParams_.pageY &&
+    const pageDimensions = this.viewport_.getPageScreenRect(page);
+    const rotations = this.viewport_.getClockwiseRotations();
+    if (rotations === this.viewportParams_.clockwiseRotations &&
+        pageDimensions.x === this.viewportParams_.pageDimensions.x &&
+        pageDimensions.y === this.viewportParams_.pageDimensions.y &&
+        pageDimensions.width === this.viewportParams_.pageDimensions.width &&
+        pageDimensions.height === this.viewportParams_.pageDimensions.height &&
         zoom === this.viewportParams_.zoom) {
       // Early return to avoid firing unnecessary events.
       return;
     }
 
     this.viewportParams_ = {
-      pageX: visiblePageDimensions.x,
-      pageY: visiblePageDimensions.y,
+      clockwiseRotations: rotations,
+      pageDimensions: pageDimensions,
       zoom,
     };
     this.dispatchEvent(
@@ -286,16 +362,65 @@
     this.fireAttributesChanged_();
   }
 
-  private screenToPageCoordinates_(pageNumber: number, screenRect: TextBoxRect):
+  private pageToScreenCoordinates_(pageNumber: number, pageRect: TextBoxRect):
       TextBoxRect {
     assert(this.viewport_);
     const pageDimensions = this.viewport_.getPageScreenRect(pageNumber);
     const zoom = this.viewport_.getZoom();
+
+    // Apply zoom.
+    const zoomed = {
+      locationX: pageRect.locationX * zoom,
+      locationY: pageRect.locationY * zoom,
+      width: pageRect.width * zoom,
+      height: pageRect.height * zoom,
+    };
+
+    // Apply rotation
+    const rotated = convertRotatedCoordinates(
+        zoomed, 0, this.viewport_.getClockwiseRotations(), pageDimensions.width,
+        pageDimensions.height);
+
+    // Apply offsets.
     return {
-      height: screenRect.height / zoom,
-      locationX: (screenRect.locationX - pageDimensions.x) / zoom,
-      locationY: (screenRect.locationY - pageDimensions.y) / zoom,
-      width: screenRect.width / zoom,
+      locationX: rotated.locationX + pageDimensions.x,
+      locationY: rotated.locationY + pageDimensions.y,
+      height: rotated.height,
+      width: rotated.width,
+    };
+  }
+
+  private screenToPageCoordinates_(pageNumber: number, screenRect: TextBoxRect):
+      TextBoxRect {
+    assert(this.viewport_);
+    const zoom = this.viewport_.getZoom();
+    const pageDimensions = this.viewport_.getPageScreenRect(pageNumber);
+
+    // Undo offset
+    const noOffset = {
+      locationX: screenRect.locationX - pageDimensions.x,
+      locationY: screenRect.locationY - pageDimensions.y,
+      width: screenRect.width,
+      height: screenRect.height,
+    };
+
+    // Undo rotation
+    const rotations = this.viewport_.getClockwiseRotations();
+    // Need to pass the width and height for the new number of desired rotations
+    // (0 in this case) to convertRotatedCoordinates().
+    const pageWidth =
+        rotations % 2 === 0 ? pageDimensions.width : pageDimensions.height;
+    const pageHeight =
+        rotations % 2 === 0 ? pageDimensions.height : pageDimensions.width;
+    const noRotation = convertRotatedCoordinates(
+        noOffset, rotations, 0, pageWidth, pageHeight);
+
+    // Undo zoom.
+    return {
+      height: noRotation.height / zoom,
+      locationX: noRotation.locationX / zoom,
+      locationY: noRotation.locationY / zoom,
+      width: noRotation.width / zoom,
     };
   }
 
diff --git a/chrome/browser/resources/print_preview/BUILD.gn b/chrome/browser/resources/print_preview/BUILD.gn
index 194b136..15673b0 100644
--- a/chrome/browser/resources/print_preview/BUILD.gn
+++ b/chrome/browser/resources/print_preview/BUILD.gn
@@ -91,6 +91,7 @@
     "ui/advanced_options_settings.css",
     "ui/advanced_settings_dialog.css",
     "ui/advanced_settings_item.css",
+    "ui/app.css",
     "ui/button_strip.css",
     "ui/copies_settings.css",
     "ui/destination_dialog.css",
diff --git a/chrome/browser/resources/print_preview/data/document_info.ts b/chrome/browser/resources/print_preview/data/document_info.ts
index 09a7b3d..95c7d28 100644
--- a/chrome/browser/resources/print_preview/data/document_info.ts
+++ b/chrome/browser/resources/print_preview/data/document_info.ts
@@ -34,6 +34,19 @@
   printableAreaHeight: number;
 }
 
+export function createDocumentSettings(): DocumentSettings {
+  return {
+    allPagesHaveCustomSize: false,
+    allPagesHaveCustomOrientation: false,
+    hasSelection: false,
+    isModifiable: true,
+    isScalingDisabled: false,
+    fitToPageScaling: 100,
+    pageCount: 0,
+    title: '',
+  };
+}
+
 const PrintPreviewDocumentInfoElementBase = WebUiListenerMixin(PolymerElement);
 
 export class PrintPreviewDocumentInfoElement extends
@@ -47,18 +60,7 @@
       documentSettings: {
         type: Object,
         notify: true,
-        value() {
-          return {
-            allPagesHaveCustomSize: false,
-            allPagesHaveCustomOrientation: false,
-            hasSelection: false,
-            isModifiable: true,
-            isScalingDisabled: false,
-            fitToPageScaling: 100,
-            pageCount: 0,
-            title: '',
-          };
-        },
+        value: () => createDocumentSettings(),
       },
 
       inFlightRequestId: {
@@ -120,14 +122,27 @@
                 allPagesHaveCustomOrientation));
   }
 
+  // Whenever documentSettings needs to be modified need to use
+  // cloneAndModify_(), which implements the immutable data pattern, because
+  // modifying in-place will not trigger Lit observers.
+  private cloneAndModify_(
+      callback: (documentSettings: DocumentSettings) => void) {
+    const clone = structuredClone(this.documentSettings);
+    callback(clone);
+    this.documentSettings = clone;
+  }
+
   /**
    * Initializes the state of the data model.
    */
   init(isModifiable: boolean, title: string, hasSelection: boolean) {
     this.isInitialized_ = true;
-    this.set('documentSettings.isModifiable', isModifiable);
-    this.set('documentSettings.title', title);
-    this.set('documentSettings.hasSelection', hasSelection);
+
+    this.cloneAndModify_(documentSettings => {
+      documentSettings.isModifiable = isModifiable;
+      documentSettings.title = title;
+      documentSettings.hasSelection = hasSelection;
+    });
   }
 
   /**
@@ -135,7 +150,9 @@
    */
   updateIsScalingDisabled(isScalingDisabled: boolean) {
     if (this.isInitialized_) {
-      this.set('documentSettings.isScalingDisabled', isScalingDisabled);
+      this.cloneAndModify_(documentSettings => {
+        documentSettings.isScalingDisabled = isScalingDisabled;
+      });
     }
   }
 
@@ -174,11 +191,11 @@
     if (this.isInitialized_) {
       this.printableArea = new PrintableArea(origin, size);
       this.pageSize = pageSize;
-      this.set(
-          'documentSettings.allPagesHaveCustomSize', allPagesHaveCustomSize);
-      this.set(
-          'documentSettings.allPagesHaveCustomOrientation',
-          allPagesHaveCustomOrientation);
+      this.cloneAndModify_(documentSettings => {
+        documentSettings.allPagesHaveCustomSize = allPagesHaveCustomSize;
+        documentSettings.allPagesHaveCustomOrientation =
+            allPagesHaveCustomOrientation;
+      });
       this.margins = margins;
     }
   }
@@ -195,8 +212,11 @@
     if (this.inFlightRequestId !== previewResponseId || !this.isInitialized_) {
       return;
     }
-    this.set('documentSettings.pageCount', pageCount);
-    this.set('documentSettings.fitToPageScaling', fitToPageScaling);
+
+    this.cloneAndModify_(documentSettings => {
+      documentSettings.pageCount = pageCount;
+      documentSettings.fitToPageScaling = fitToPageScaling;
+    });
   }
 }
 
diff --git a/chrome/browser/resources/print_preview/ui/app.css b/chrome/browser/resources/print_preview/ui/app.css
new file mode 100644
index 0000000..823da19
--- /dev/null
+++ b/chrome/browser/resources/print_preview/ui/app.css
@@ -0,0 +1,32 @@
+/* Copyright 2025 The Chromium Authors
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file. */
+
+/* #css_wrapper_metadata_start
+ * #type=style-lit
+ * #import=chrome://resources/cr_elements/cr_shared_vars.css.js
+ * #import=./print_preview_vars.css.js
+ * #css_wrapper_metadata_end */
+
+:host {
+  display: flex;
+  height: 100%;
+  user-select: none;
+}
+
+@media (prefers-color-scheme: dark) {
+  :host {
+    background: var(--google-grey-900);
+  }
+}
+
+print-preview-sidebar {
+  flex: none;
+  width: var(--print-preview-sidebar-width);
+}
+
+#preview-area-container {
+  align-items: center;
+  background-color: var(--preview-area-background-color);
+  flex: 1;
+}
diff --git a/chrome/browser/resources/print_preview/ui/app.html b/chrome/browser/resources/print_preview/ui/app.html
index 1799a94..8f42091 100644
--- a/chrome/browser/resources/print_preview/ui/app.html
+++ b/chrome/browser/resources/print_preview/ui/app.html
@@ -1,62 +1,45 @@
-<style>
-  :host {
-    display: flex;
-    height: 100%;
-    user-select: none;
-  }
-
-  @media (prefers-color-scheme: dark) {
-    :host {
-      background: var(--google-grey-900);
-    }
-  }
-
-  print-preview-sidebar {
-    flex: none;
-    width: var(--print-preview-sidebar-width);
-  }
-
-  #preview-area-container {
-    align-items: center;
-    background-color: var(--preview-area-background-color);
-    flex: 1;
-  }
-</style>
-<print-preview-state id="state" state="{{state}}" error="{{error_}}">
+<print-preview-state id="state" state="${this.state}"
+    @state-changed="${this.onStateChanged_}" error="${this.error_}"
+    @error-changed="${this.onErrorChanged_}">
 </print-preview-state>
-<print-preview-model id="model" settings="{{settings}}"
-    settings-managed="{{settingsManaged_}}" destination="[[destination_]]"
-    document-settings="[[documentSettings_]]"
-    margins="[[margins_]]" page-size="[[pageSize_]]"
-    on-preview-setting-changed="onPreviewSettingChanged_"
-    on-sticky-setting-changed="onStickySettingChanged_"
-    on-setting-valid-changed="onSettingValidChanged_">
+<print-preview-model id="model"
+    @settings-managed-changed="${this.onSettingsManagedChanged_}"
+    .destination="${this.destination_}"
+    .documentSettings="${this.documentSettings_}"
+    .margins="${this.margins_}" .pageSize="${this.pageSize_}"
+    @preview-setting-changed="${this.onPreviewSettingChanged_}"
+    @sticky-setting-changed="${this.onStickySettingChanged_}"
+    @setting-valid-changed="${this.onSettingValidChanged_}">
 </print-preview-model>
 <print-preview-document-info id="documentInfo"
-    document-settings="{{documentSettings_}}" margins="{{margins_}}"
-    page-size="{{pageSize_}}">
+    @document-settings-changed="${this.onDocumentSettingsChanged_}"
+    @margins-changed="${this.onMarginsChanged_}"
+    @page-size-changed="${this.onPageSizeChanged_}">
 </print-preview-document-info>
 <div id="preview-area-container">
   <print-preview-preview-area id="previewArea"
-      destination="[[destination_]]" error="{{error_}}"
-      document-modifiable="[[documentSettings_.isModifiable]]"
-      margins="[[margins_]]" page-size="[[pageSize_]]" state="[[state]]"
-      measurement-system="[[measurementSystem_]]"
-      preview-state="{{previewState_}}" on-preview-start="onPreviewStart_">
+      .destination="${this.destination_}" error="${this.error_}"
+      @error-changed="${this.onErrorChanged_}"
+      ?document-modifiable="${this.documentSettings_.isModifiable}"
+      .margins="${this.margins_}" .pageSize="${this.pageSize_}"
+      state="${this.state}" .measurementSystem="${this.measurementSystem_}"
+      @preview-state-changed="${this.onPreviewStateChanged_}"
+      @preview-start="${this.onPreviewStart_}">
   </print-preview-preview-area>
 </div>
 <print-preview-sidebar id="sidebar"
-    destination-state="{{destinationState_}}"
-    controls-managed="[[controlsManaged_]]" destination="[[destination_]]"
-    error="{{error_}}" is-pdf="[[!documentSettings_.isModifiable]]"
-    page-count="[[documentSettings_.pageCount]]" state="[[state]]"
-    on-focus="onSidebarFocus_"
-    on-destination-changed="onDestinationChanged_"
-    on-destination-capabilities-changed="onDestinationCapabilitiesChanged_"
+    @destination-state-changed="${this.onDestinationStateChanged_}"
+    ?controls-managed="${this.controlsManaged_}"
+    error="${this.error_}" @error-changed="${this.onErrorChanged_}"
+    ?is-pdf="${!this.documentSettings_.isModifiable}"
+    page-count="${this.documentSettings_.pageCount}" state="${this.state}"
+    @focus="${this.onSidebarFocus_}"
+    @destination-changed="${this.onDestinationChanged_}"
+    @destination-capabilities-changed="${this.onDestinationCapabilitiesChanged_}"
 <if expr="is_macosx">
-    on-open-pdf-in-preview="onOpenPdfInPreview_"
+    @open-pdf-in-preview="${this.onOpenPdfInPreview_}"
 </if>
-    on-print-with-system-dialog="onPrintWithSystemDialog_"
-    on-print-requested="onPrintRequested_"
-    on-cancel-requested="onCancelRequested_">
+    @print-with-system-dialog="${this.onPrintWithSystemDialog_}"
+    @print-requested="${this.onPrintRequested_}"
+    @cancel-requested="${this.onCancelRequested_}">
 </print-preview-sidebar>
diff --git a/chrome/browser/resources/print_preview/ui/app.ts b/chrome/browser/resources/print_preview/ui/app.ts
index fa1b0ad..cc19030c 100644
--- a/chrome/browser/resources/print_preview/ui/app.ts
+++ b/chrome/browser/resources/print_preview/ui/app.ts
@@ -2,39 +2,43 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import 'chrome://resources/cr_elements/cr_shared_vars.css.js';
-import './print_preview_vars.css.js';
 import '/strings.m.js';
 import '../data/document_info.js';
+import '../data/model.js';
+import '../data/state.js';
+import './preview_area.js';
 import './sidebar.js';
 
 import type {CrDialogElement} from 'chrome://resources/cr_elements/cr_dialog/cr_dialog.js';
-import {WebUiListenerMixin} from 'chrome://resources/cr_elements/web_ui_listener_mixin.js';
+import {WebUiListenerMixinLit} from 'chrome://resources/cr_elements/web_ui_listener_mixin_lit.js';
+import {assert} from 'chrome://resources/js/assert.js';
 import {EventTracker} from 'chrome://resources/js/event_tracker.js';
 import {FocusOutlineManager} from 'chrome://resources/js/focus_outline_manager.js';
 import {isMac, isWindows} from 'chrome://resources/js/platform.js';
 import {hasKeyModifiers} from 'chrome://resources/js/util.js';
-import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
+import {CrLitElement} from 'chrome://resources/lit/v3_0/lit.rollup.js';
+import type {PropertyValues} from 'chrome://resources/lit/v3_0/lit.rollup.js';
 
 import type {Destination} from '../data/destination.js';
 import {PrinterType} from '../data/destination.js';
 import type {DocumentSettings, PrintPreviewDocumentInfoElement} from '../data/document_info.js';
+import {createDocumentSettings} from '../data/document_info.js';
 import type {Margins} from '../data/margins.js';
 import {MeasurementSystem} from '../data/measurement_system.js';
 import type {PrintPreviewModelElement} from '../data/model.js';
 import {DuplexMode, whenReady} from '../data/model.js';
-import type {PrintableArea} from '../data/printable_area.js';
 import type {Size} from '../data/size.js';
 import type {PrintPreviewStateElement} from '../data/state.js';
 import {Error, State} from '../data/state.js';
 import type {NativeInitialSettings, NativeLayer} from '../native_layer.js';
 import {NativeLayerImpl} from '../native_layer.js';
 
-import {getTemplate} from './app.html.js';
+import {getCss} from './app.css.js';
+import {getHtml} from './app.html.js';
 import {DestinationState} from './destination_settings.js';
 import type {PrintPreviewPreviewAreaElement} from './preview_area.js';
 import {PreviewAreaState} from './preview_area.js';
-import {SettingsMixin} from './settings_mixin.js';
+import {SettingsMixinLit} from './settings_mixin_lit.js';
 import type {PrintPreviewSidebarElement} from './sidebar.js';
 
 export interface PrintPreviewAppElement {
@@ -48,85 +52,47 @@
 }
 
 const PrintPreviewAppElementBase =
-    WebUiListenerMixin(SettingsMixin(PolymerElement));
+    WebUiListenerMixinLit(SettingsMixinLit(CrLitElement));
 
 export class PrintPreviewAppElement extends PrintPreviewAppElementBase {
   static get is() {
     return 'print-preview-app';
   }
 
-  static get template() {
-    return getTemplate();
+  static override get styles() {
+    return getCss();
   }
 
-  static get properties() {
+  override render() {
+    return getHtml.bind(this)();
+  }
+
+  static override get properties() {
     return {
-      state: {
-        type: Number,
-        observer: 'onStateChanged_',
-      },
-
-      controlsManaged_: {
-        type: Boolean,
-        computed: 'computeControlsManaged_(destinationsManaged_, ' +
-            'settingsManaged_)',
-      },
-
-      destination_: Object,
-
-      destinationsManaged_: {
-        type: Boolean,
-        value: false,
-      },
-
-      destinationState_: {
-        type: Number,
-        observer: 'onDestinationStateChange_',
-      },
-
-      documentSettings_: Object,
-
-      error_: {
-        type: Number,
-        observer: 'onErrorChange_',
-      },
-
-      margins_: Object,
-
-      pageSize_: Object,
-
-      previewState_: {
-        type: String,
-        observer: 'onPreviewStateChange_',
-      },
-
-      printableArea_: Object,
-
-      settingsManaged_: {
-        type: Boolean,
-        value: false,
-      },
-
-      measurementSystem_: {
-        type: Object,
-        value: null,
-      },
+      state: {type: Number},
+      controlsManaged_: {type: Boolean},
+      destination_: {type: Object},
+      destinationsManaged_: {type: Boolean},
+      documentSettings_: {type: Object},
+      error_: {type: Number},
+      margins_: {type: Object},
+      pageSize_: {type: Object},
+      settingsManaged_: {type: Boolean},
+      measurementSystem_: {type: Object},
     };
   }
 
-  declare state: State;
-  declare private controlsManaged_: boolean;
-  declare private destination_: Destination;
-  declare private destinationsManaged_: boolean;
-  declare private destinationState_: DestinationState;
-  declare private documentSettings_: DocumentSettings;
-  declare private error_: Error;
-  declare private margins_: Margins;
-  declare private pageSize_: Size;
-  declare private previewState_: PreviewAreaState;
-  declare private printableArea_: PrintableArea;
-  declare private settingsManaged_: boolean;
-  declare private measurementSystem_: MeasurementSystem|null;
+  accessor state: State;
+  protected accessor controlsManaged_: boolean = false;
+  protected accessor destination_: Destination;
+  private accessor destinationsManaged_: boolean = false;
+  protected accessor documentSettings_: DocumentSettings =
+      createDocumentSettings();
+  protected accessor error_: Error;
+  protected accessor margins_: Margins;
+  protected accessor pageSize_: Size;
+  protected accessor settingsManaged_: boolean = false;
+  protected accessor measurementSystem_: MeasurementSystem|null = null;
 
   private nativeLayer_: NativeLayer|null = null;
   private tracker_: EventTracker = new EventTracker();
@@ -151,9 +117,7 @@
     }
   }
 
-  override ready() {
-    super.ready();
-
+  override firstUpdated() {
     FocusOutlineManager.forDocument(document);
   }
 
@@ -180,7 +144,38 @@
     this.whenReady_ = null;
   }
 
-  private onSidebarFocus_() {
+  override willUpdate(changedProperties: PropertyValues<this>) {
+    super.willUpdate(changedProperties);
+
+    const changedPrivateProperties =
+        changedProperties as Map<PropertyKey, unknown>;
+
+    if (changedPrivateProperties.has('destinationsManaged_') ||
+        changedPrivateProperties.has('settingsManaged_')) {
+      this.controlsManaged_ =
+          this.destinationsManaged_ || this.settingsManaged_;
+    }
+  }
+
+  override updated(changedProperties: PropertyValues<this>) {
+    super.updated(changedProperties);
+
+    const changedPrivateProperties =
+        changedProperties as Map<PropertyKey, unknown>;
+
+    if (changedProperties.has('state')) {
+      this.updateUiForStateChange_();
+    }
+
+    if (changedPrivateProperties.has('error_')) {
+      if (this.error_ !== Error.NONE) {
+        this.nativeLayer_!.recordInHistogram(
+            'PrintPreview.StateError', this.error_, Error.MAX_BUCKET);
+      }
+    }
+  }
+
+  protected onSidebarFocus_() {
     this.$.previewArea.hideToolbar();
   }
 
@@ -308,16 +303,11 @@
     });
   }
 
-  /**
-   * @return Whether any of the print preview settings or destinations
-   *     are managed.
-   */
-  private computeControlsManaged_(): boolean {
-    return this.destinationsManaged_ || this.settingsManaged_;
-  }
+  protected onDestinationStateChanged_(
+      e: CustomEvent<{value: DestinationState}>) {
+    const destinationState = e.detail.value;
 
-  private onDestinationStateChange_() {
-    switch (this.destinationState_) {
+    switch (destinationState) {
       case DestinationState.SET:
         if (this.state !== State.NOT_READY &&
             this.state !== State.FATAL_ERROR) {
@@ -358,12 +348,18 @@
   /**
    * @param e Event containing the new sticky settings.
    */
-  private onStickySettingChanged_(e: CustomEvent<string>) {
+  protected onStickySettingChanged_(e: CustomEvent<string>) {
     this.nativeLayer_!.saveAppState(e.detail);
   }
 
-  private onPreviewSettingChanged_() {
+  protected async onPreviewSettingChanged_() {
     if (this.state === State.READY) {
+      // Need to wait for rendering to finish, to ensure that the `destination`
+      // is synced across print-preview-app, print-preview-model and
+      // print-preview-area.
+      await this.updateComplete;
+      assert(this.destination_.id === this.$.previewArea.destination.id);
+      assert(this.destination_.id === this.$.model.destination.id);
       this.$.previewArea.startPreview(false);
       this.startPreviewWhenReady_ = false;
     } else {
@@ -371,7 +367,7 @@
     }
   }
 
-  private onStateChanged_() {
+  private updateUiForStateChange_() {
     if (this.state === State.READY) {
       if (this.startPreviewWhenReady_) {
         this.$.previewArea.startPreview(false);
@@ -410,7 +406,7 @@
     }
   }
 
-  private onPrintRequested_() {
+  protected onPrintRequested_() {
     if (this.state === State.NOT_READY) {
       this.printRequested_ = true;
       return;
@@ -421,7 +417,7 @@
                                              State.PRINT_PENDING);
   }
 
-  private onCancelRequested_() {
+  protected onCancelRequested_() {
     this.cancelled_ = true;
     this.$.state.transitTo(State.CLOSING);
   }
@@ -429,7 +425,7 @@
   /**
    * @param e The event containing the new validity.
    */
-  private onSettingValidChanged_(e: CustomEvent<boolean>) {
+  protected onSettingValidChanged_(e: CustomEvent<boolean>) {
     if (e.detail) {
       this.$.state.transitTo(State.READY);
     } else {
@@ -442,7 +438,7 @@
     this.$.state.transitTo(State.READY);
   }
 
-  private onPrintWithSystemDialog_() {
+  protected onPrintWithSystemDialog_() {
     // <if expr="is_win">
     this.showSystemDialogBeforePrint_ = true;
     this.onPrintRequested_();
@@ -454,7 +450,7 @@
   }
 
   // <if expr="is_macosx">
-  private onOpenPdfInPreview_() {
+  protected onOpenPdfInPreview_() {
     this.openPdfInPreview_ = true;
     this.$.previewArea.setOpeningPdfInPreview();
     this.onPrintRequested_();
@@ -472,8 +468,10 @@
     this.$.state.transitTo(State.FATAL_ERROR);
   }
 
-  private onPreviewStateChange_() {
-    switch (this.previewState_) {
+  protected onPreviewStateChanged_(e: CustomEvent<{value: PreviewAreaState}>) {
+    const previewState = e.detail.value;
+
+    switch (previewState) {
       case PreviewAreaState.DISPLAY_PREVIEW:
       case PreviewAreaState.OPEN_IN_PREVIEW_LOADED:
         if (this.state === State.PRINT_PENDING || this.state === State.HIDDEN) {
@@ -529,7 +527,7 @@
   /**
    * @param e Contains the new preview request ID.
    */
-  private onPreviewStart_(e: CustomEvent<number>) {
+  protected onPreviewStart_(e: CustomEvent<number>) {
     this.$.documentInfo.inFlightRequestId = e.detail;
   }
 
@@ -537,15 +535,42 @@
     this.$.state.transitTo(State.CLOSING);
   }
 
-  private onDestinationChanged_(e: CustomEvent<{value: Destination}>) {
+  protected onDestinationChanged_(e: CustomEvent<{value: Destination}>) {
     this.destination_ = e.detail.value;
   }
 
-  private onDestinationCapabilitiesChanged_() {
+  protected onDestinationCapabilitiesChanged_() {
     this.$.model.updateSettingsFromDestination();
   }
+
+  protected onStateChanged_(e: CustomEvent<{value: State}>) {
+    this.state = e.detail.value;
+  }
+
+  protected onErrorChanged_(e: CustomEvent<{value: Error}>) {
+    this.error_ = e.detail.value;
+  }
+
+  protected onSettingsManagedChanged_(e: CustomEvent<{value: boolean}>) {
+    this.settingsManaged_ = e.detail.value;
+  }
+
+  protected onDocumentSettingsChanged_(
+      e: CustomEvent<{value: DocumentSettings}>) {
+    this.documentSettings_ = e.detail.value;
+  }
+
+  protected onMarginsChanged_(e: CustomEvent<{value: Margins}>) {
+    this.margins_ = e.detail.value;
+  }
+
+  protected onPageSizeChanged_(e: CustomEvent<{value: Size}>) {
+    this.pageSize_ = e.detail.value;
+  }
 }
 
+export type AppElement = PrintPreviewAppElement;
+
 declare global {
   interface HTMLElementTagNameMap {
     'print-preview-app': PrintPreviewAppElement;
diff --git a/chrome/browser/resources/print_preview/ui/sidebar.html b/chrome/browser/resources/print_preview/ui/sidebar.html
index 3cd8c296..8371d689 100644
--- a/chrome/browser/resources/print_preview/ui/sidebar.html
+++ b/chrome/browser/resources/print_preview/ui/sidebar.html
@@ -4,9 +4,8 @@
 </print-preview-header>
 <div id="container" show-bottom-shadow>
   <print-preview-destination-settings id="destinationSettings"
-      ?dark="${this.inDarkMode}" .destination="${this.destination}"
+      ?dark="${this.inDarkMode}"
       @destination-changed="${this.onDestinationChanged_}"
-      destination-state="${this.destinationState}"
       @destination-state-changed="${this.onDestinationStateChanged_}"
       error="${this.error}" @error-changed="${this.onErrorChanged_}"
       ?first-load="${this.firstLoad_}"
diff --git a/chrome/browser/resources/print_preview/ui/sidebar.ts b/chrome/browser/resources/print_preview/ui/sidebar.ts
index 7b9f3e0..a6a97c8 100644
--- a/chrome/browser/resources/print_preview/ui/sidebar.ts
+++ b/chrome/browser/resources/print_preview/ui/sidebar.ts
@@ -120,7 +120,7 @@
   }
 
   accessor controlsManaged: boolean;
-  accessor destination: Destination|null;
+  accessor destination: Destination|null = null;
   accessor destinationCapabilities_: Cdd|null;
   accessor destinationState: DestinationState;
   accessor error: Error;
diff --git a/chrome/browser/resources/settings/performance_page/tab_discard/exception_tabbed_add_dialog.html b/chrome/browser/resources/settings/performance_page/tab_discard/exception_tabbed_add_dialog.html
index 794b501..4c35a91 100644
--- a/chrome/browser/resources/settings/performance_page/tab_discard/exception_tabbed_add_dialog.html
+++ b/chrome/browser/resources/settings/performance_page/tab_discard/exception_tabbed_add_dialog.html
@@ -40,6 +40,10 @@
     padding-bottom: 20px;
   }
 
+  #helpText > a {
+    color: var(--cr-link-color);
+  }
+
   /*
    * adds horizontal padding to account for the removed padding from the dialog
    * body
diff --git a/chrome/browser/resources/side_panel/read_anything/app.ts b/chrome/browser/resources/side_panel/read_anything/app.ts
index 2ff44f3..09e1c82 100644
--- a/chrome/browser/resources/side_panel/read_anything/app.ts
+++ b/chrome/browser/resources/side_panel/read_anything/app.ts
@@ -306,7 +306,7 @@
     };
 
     chrome.readingMode.restoreSettingsFromPrefs = () => {
-      this.restoreSettingsFromPrefs();
+      this.restoreSettingsFromPrefs_();
     };
 
     chrome.readingMode.languageChanged = () => {
@@ -538,10 +538,11 @@
         // reading mode believes it has selected content to distll but
         // nothing valid is selected. This can cause the loading screen
         // to never switch to the empty state.
-        // TODO: crbug.com/411198154- Longer term, once reading mode and read aloud
-        // traversal is more in line, the renderer should be able to call showEmpty
-        // directly, rather than signaling to the WebUI to update content and then WebUI
-        // signaling back to the renderer that there is no text content.
+        // TODO: crbug.com/411198154- Longer term, once reading mode and read
+        // aloud traversal is more in line, the renderer should be able to call
+        // showEmpty directly, rather than signaling to the WebUI to update
+        // content and then WebUI signaling back to the renderer that there is
+        // no text content.
         chrome.readingMode.onNoTextContent(/* previouslyHadContent*/ false);
       }
       return;
@@ -920,7 +921,7 @@
     }
   }
 
-  restoreSettingsFromPrefs() {
+  private restoreSettingsFromPrefs_() {
     if (this.isReadAloudEnabled_) {
       this.voicePackController_.restoreFromPrefs();
     }
diff --git a/chrome/browser/resources/side_panel/read_anything/read_aloud/highlighter.ts b/chrome/browser/resources/side_panel/read_anything/read_aloud/highlighter.ts
index d69ed38..67c5ce2 100644
--- a/chrome/browser/resources/side_panel/read_anything/read_aloud/highlighter.ts
+++ b/chrome/browser/resources/side_panel/read_anything/read_aloud/highlighter.ts
@@ -43,7 +43,7 @@
         highlight => highlight.classList.contains(currentReadHighlightClass));
   }
 
-  getCurrentHighlights(): HTMLElement[] {
+  private getCurrentHighlights_(): HTMLElement[] {
     return this.previousHighlights_.filter(
         highlight => highlight.classList.contains(currentReadHighlightClass));
   }
@@ -142,7 +142,7 @@
 
   private getCurrentHighlightBounds_(): DOMRect {
     const bounds = new DOMRect();
-    const currentHighlights = this.getCurrentHighlights();
+    const currentHighlights = this.getCurrentHighlights_();
     if (!currentHighlights || !currentHighlights.length) {
       return bounds;
     }
@@ -373,7 +373,7 @@
     // using word boundaries to know when we've reached the bottom of the
     // window and need to scroll so the rest of the current highlight is
     // showing.
-    const firstHighlight = this.getCurrentHighlights().at(0);
+    const firstHighlight = this.getCurrentHighlights_().at(0);
     if (!firstHighlight) {
       return;
     }
diff --git a/chrome/browser/resources/side_panel/read_anything/read_aloud/speech_controller.ts b/chrome/browser/resources/side_panel/read_anything/read_aloud/speech_controller.ts
index ab656e71..ac750c15 100644
--- a/chrome/browser/resources/side_panel/read_anything/read_aloud/speech_controller.ts
+++ b/chrome/browser/resources/side_panel/read_anything/read_aloud/speech_controller.ts
@@ -190,8 +190,6 @@
       this.highlightCurrentGranularity(chrome.readingMode.getCurrentText());
     }
 
-    // Log these highlight granularity changes when the phrase menu is shown.
-    // (Toggles are already logged in the toolbar.)
     this.logger_.logHighlightGranularity(newGranularity);
   }
 
diff --git a/chrome/browser/resources/side_panel/read_anything/read_aloud/voice_pack_controller.ts b/chrome/browser/resources/side_panel/read_anything/read_aloud/voice_pack_controller.ts
index 594509b..d2144c4d 100644
--- a/chrome/browser/resources/side_panel/read_anything/read_aloud/voice_pack_controller.ts
+++ b/chrome/browser/resources/side_panel/read_anything/read_aloud/voice_pack_controller.ts
@@ -105,7 +105,7 @@
     const hadAvailableVoices = this.hasAvailableVoices();
     // Get a new list of voices. This should be done before we call
     // updateUnavailableVoiceToDefaultVoice_();
-    this.refreshAvailableVoices(/*forceRefresh=*/ true);
+    this.refreshAvailableVoices_(/*forceRefresh=*/ true);
 
     // TODO: crbug.com/390435037 - Simplify logic around loading voices and
     // language availability, especially around the new TTS engine.
@@ -153,7 +153,6 @@
 
     const langCodeForVoicePackManager = convertLangOrLocaleForVoicePackManager(
         langOrLocale, this.getEnabledLangs(), this.getAvailableLangs());
-
     if (!langCodeForVoicePackManager) {
       this.autoSwitchVoice_(langOrLocale);
       return;
@@ -180,7 +179,7 @@
 
     // Enable the preferred locale for this lang if one exists. Otherwise,
     // enable a Google TTS supported locale for this language if one exists.
-    this.refreshAvailableVoices();
+    this.refreshAvailableVoices_();
     const preferredVoice = chrome.readingMode.getStoredVoice();
     const preferredVoiceLang = this.getAvailableVoices()
                                    .find(voice => voice.name === preferredVoice)
@@ -247,7 +246,7 @@
       return;
     }
 
-    this.refreshAvailableVoices();
+    this.refreshAvailableVoices_();
     const selectedVoice = this.getAvailableVoices().filter(
         voice => voice.name === storedVoiceName);
     const newVoice = (selectedVoice.length && selectedVoice[0]) ?
@@ -322,7 +321,7 @@
 
     // If the default voice won't work, try another voice in that language.
     const baseLang = this.getCurrentLanguage();
-    this.refreshAvailableVoices();
+    this.refreshAvailableVoices_();
     const voicesForLanguage = this.getAvailableVoices().filter(
         voice => voice.lang.startsWith(baseLang));
 
@@ -353,7 +352,7 @@
 
   private disableLangIfNoVoices_(lang: string): void {
     const lowerLang = lang.toLowerCase();
-    this.refreshAvailableVoices();
+    this.refreshAvailableVoices_();
     const availableVoicesForLang = this.getAvailableVoicesForLang_(lowerLang);
 
     let disableLang = false;
@@ -424,7 +423,7 @@
   restoreFromPrefs(): void {
     // We need to make sure the languages we choose correspond to voices, so
     // refresh the list of voices and available langs
-    this.refreshAvailableVoices();
+    this.refreshAvailableVoices_();
     this.setCurrentLanguage(chrome.readingMode.baseLanguageForSpeech);
     const storedLanguagesPref = chrome.readingMode.getLanguagesEnabledInPref();
     const langOfDefaultVoice = this.getDefaultVoice_()?.lang;
@@ -441,7 +440,7 @@
     this.setUserPreferredVoiceFromPrefs_();
   }
 
-  refreshAvailableVoices(forceRefresh: boolean = false): void {
+  private refreshAvailableVoices_(forceRefresh: boolean = false): void {
     if (!this.hasAvailableVoices() || forceRefresh) {
       const availableVoices = getFilteredVoiceList(this.speech_.getVoices());
       this.setAvailableVoices(availableVoices);
@@ -530,7 +529,7 @@
         case VoicePackServerStatusSuccessCode.INSTALLED:
           // Force a refresh of the voices list since we might not get an update
           // the voices have changed.
-          this.refreshAvailableVoices(/*forceRefresh=*/ true);
+          this.refreshAvailableVoices_(/*forceRefresh=*/ true);
           this.autoSwitchVoice_(lang);
 
           // Some languages may require a download from the voice pack
@@ -746,7 +745,7 @@
   }
 
   private getDefaultVoice_(): SpeechSynthesisVoice|null {
-    this.refreshAvailableVoices();
+    this.refreshAvailableVoices_();
     const allPossibleVoices = this.getAvailableVoices();
     const voicesForLanguage = allPossibleVoices.filter(
         voice => voice.lang.startsWith(this.getCurrentLanguage()));
diff --git a/chrome/browser/resources/side_panel/read_anything/read_anything.ts b/chrome/browser/resources/side_panel/read_anything/read_anything.ts
index f941149..3da9491 100644
--- a/chrome/browser/resources/side_panel/read_anything/read_anything.ts
+++ b/chrome/browser/resources/side_panel/read_anything/read_anything.ts
@@ -10,7 +10,7 @@
 export type {CrActionMenuElement} from '//resources/cr_elements/cr_action_menu/cr_action_menu.js';
 export type {AppElement} from './app.js';
 export {AppStyleUpdater} from './app_style_updater.js';
-export {getCurrentSpeechRate, playFromSelectionTimeout, spinnerDebounceTimeout, ToolbarEvent} from './common.js';
+export {getCurrentSpeechRate, isRectVisible, playFromSelectionTimeout, spinnerDebounceTimeout, ToolbarEvent} from './common.js';
 export {getNewIndex, isArrow, isForwardArrow, isHorizontalArrow} from './keyboard_util.js';
 export type {LanguageMenuElement} from './language_menu.js';
 export type {LanguageToastElement} from './language_toast.js';
diff --git a/chrome/browser/resources/signin/history_sync_optin/BUILD.gn b/chrome/browser/resources/signin/history_sync_optin/BUILD.gn
index 8c4277ff..0bdf5a8 100644
--- a/chrome/browser/resources/signin/history_sync_optin/BUILD.gn
+++ b/chrome/browser/resources/signin/history_sync_optin/BUILD.gn
@@ -17,6 +17,8 @@
     "history_sync_optin_app.html.ts",
   ]
 
+  css_files = [ "history_sync_optin_app.css" ]
+
   mojo_files_deps = [ "//chrome/browser/ui/webui/signin/history_sync_optin:mojo_bindings_ts__generator" ]
   mojo_files = [ "$root_gen_dir/chrome/browser/ui/webui/signin/history_sync_optin/history_sync_optin.mojom-webui.ts" ]
 
@@ -24,6 +26,8 @@
 
   ts_deps = [
     "//third_party/lit/v3_0:build_ts",
+    "//ui/webui/resources/cr_elements:build_ts",
+    "//ui/webui/resources/js:build_ts",
     "//ui/webui/resources/mojo:build_ts",
   ]
 }
diff --git a/chrome/browser/resources/signin/history_sync_optin/history_sync_optin_app.css b/chrome/browser/resources/signin/history_sync_optin/history_sync_optin_app.css
new file mode 100644
index 0000000..ee02346b
--- /dev/null
+++ b/chrome/browser/resources/signin/history_sync_optin/history_sync_optin_app.css
@@ -0,0 +1,57 @@
+/* Copyright 2025 The Chromium Authors
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file. */
+
+/* #css_wrapper_metadata_start
+ * #type=style-lit
+ * #import=//resources/cr_elements/cr_shared_style_lit.css.js
+ * #import=//resources/cr_elements/cr_shared_vars.css.js
+ * #scheme=relative
+ * #include=cr-shared-style-lit
+ * #css_wrapper_metadata_end */
+
+:host {
+  color: var(--cr-primary-text-color);
+  display: block;
+  border-radius: 12px;
+  overflow: hidden;
+}
+
+#contentContainer {
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+  padding: 20px;
+}
+
+#avatar {
+  /** The user avatar may be transparent, add a background */
+  background-color: var(--md-background-color);
+  border: solid var(--md-background-color);
+  border-radius: 50%;
+  height: 64px;
+  width: 64px;
+  align-self: center;
+}
+
+#buttonRow {
+  display: flex;
+  justify-content: flex-end;
+  gap: 8px;
+}
+
+#modalDescription {
+  /* layout */
+  padding: 12px 20px 12px 20px;
+  /* colors */
+  border-top: 1px solid var(--cr-fallback-color-neutral-outline);
+  background-color: var(--cr-fallback-color-neutral-container);
+  /* fonts */
+  font-size: 11px;
+  line-height: 20px;
+}
+
+h1 {
+  font-size: 16px;
+  font-weight: 500;
+}
diff --git a/chrome/browser/resources/signin/history_sync_optin/history_sync_optin_app.html.ts b/chrome/browser/resources/signin/history_sync_optin/history_sync_optin_app.html.ts
index 485f6d0..c4c5639 100644
--- a/chrome/browser/resources/signin/history_sync_optin/history_sync_optin_app.html.ts
+++ b/chrome/browser/resources/signin/history_sync_optin/history_sync_optin_app.html.ts
@@ -8,6 +8,24 @@
 
 export function getHtml(this: HistorySyncOptinAppElement) {
   return html`
-<div>
-</div>`;
+  <!--_html_template_start_-->
+  <div id="contentContainer">
+    <img id="avatar" alt="" src="${this.accountImageSrc_}">
+    <div id="textContainer">
+      <h1 class="title">$i18n{historySyncOptInTitle}</h1>
+      <div id="subtitle">$i18n{historySyncOptInSubtitle}</div>
+    </div>
+    <div id="buttonRow">
+      <cr-button id="cancelButton" class="tonal-button"
+          @click="${this.onCancel_}">
+        $i18n{historySyncOptInCancelButtonLabel}
+      </cr-button>
+      <cr-button id="acceptButton" class="action-button"
+          @click="${this.onAccept_}">
+        $i18n{historySyncOptInAcceptButtonLabel}
+      </cr-button>
+    </div>
+  </div>
+  <div id="modalDescription">$i18n{historySyncOptInDescription}</div>
+  <!--_html_template_end_-->`;
 }
diff --git a/chrome/browser/resources/signin/history_sync_optin/history_sync_optin_app.ts b/chrome/browser/resources/signin/history_sync_optin/history_sync_optin_app.ts
index b8bf8f75..a9c06082 100644
--- a/chrome/browser/resources/signin/history_sync_optin/history_sync_optin_app.ts
+++ b/chrome/browser/resources/signin/history_sync_optin/history_sync_optin_app.ts
@@ -3,19 +3,42 @@
 // found in the LICENSE file.
 
 import '/strings.m.js';
+import '//resources/cr_elements/cr_button/cr_button.js';
 
+import {I18nMixinLit} from '//resources/cr_elements/i18n_mixin_lit.js';
 import {CrLitElement} from '//resources/lit/v3_0/lit.rollup.js';
+import {loadTimeData} from '//resources/js/load_time_data.js';
 
+import {getCss} from './history_sync_optin_app.css.js';
 import {getHtml} from './history_sync_optin_app.html.js';
 
-export class HistorySyncOptinAppElement extends CrLitElement {
+const HistorySyncOptinAppElementBase = I18nMixinLit(CrLitElement);
+
+export class HistorySyncOptinAppElement extends HistorySyncOptinAppElementBase {
   static get is() {
     return 'history-sync-optin-app';
   }
 
+  static override get styles() {
+    return getCss();
+  }
+
   override render() {
     return getHtml.bind(this)();
   }
+
+  static override get properties() {
+    return {
+      accountImageSrc_: {type: String},
+    };
+  }
+
+  protected accessor accountImageSrc_: string =
+      loadTimeData.getString('accountPictureUrl');
+
+  // TODO(crbug.com/326912202): Wire the keys.
+  protected onCancel_() {}
+  protected onAccept_() {}
 }
 
 declare global {
diff --git a/chrome/browser/resources/tab_strip/BUILD.gn b/chrome/browser/resources/tab_strip/BUILD.gn
index 28ec5f3..ec4d25085 100644
--- a/chrome/browser/resources/tab_strip/BUILD.gn
+++ b/chrome/browser/resources/tab_strip/BUILD.gn
@@ -39,13 +39,16 @@
     "drag_manager.ts",
     "tab_swiper.ts",
     "tabs_api_proxy.ts",
+    "tab_strip_api.ts",
   ]
 
   mojo_files_deps = [
+    "//chrome/browser/ui/tabs/tab_strip_api:mojom_ts__generator",
     "//chrome/browser/ui/webui/tab_strip:mojo_bindings_ts__generator",
     "//chrome/browser/ui/webui/tabs:mojo_bindings_ts__generator",
   ]
   mojo_files = [
+    "$root_gen_dir/chrome/browser/ui/tabs/tab_strip_api/tab_strip_api.mojom-webui.ts",
     "$root_gen_dir/chrome/browser/ui/webui/tab_strip/tab_strip.mojom-webui.ts",
     "$root_gen_dir/chrome/browser/ui/webui/tabs/tabs.mojom-webui.ts",
   ]
diff --git a/chrome/browser/resources/tab_strip/tab_strip_api.ts b/chrome/browser/resources/tab_strip/tab_strip_api.ts
new file mode 100644
index 0000000..d43fd39
--- /dev/null
+++ b/chrome/browser/resources/tab_strip/tab_strip_api.ts
@@ -0,0 +1,27 @@
+// Copyright 2025 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import type {Url} from '//resources/mojo/url/mojom/url.mojom-webui.js';
+
+import {TabsObserverCallbackRouter, TabStripService} from './tab_strip_api.mojom-webui.js';
+import type {Position, TabStripServiceRemote} from './tab_strip_api.mojom-webui.js';
+
+export interface TabStripApiProxy {
+  createTabAt(pos: Position|null, url: Url|null): Promise<boolean>;
+}
+
+export class TabStripApiProxyImpl implements TabStripApiProxy {
+  service: TabStripServiceRemote = TabStripService.getRemote();
+  observer: TabsObserverCallbackRouter = new TabsObserverCallbackRouter();
+
+  createTabAt(pos: Position|null, url: Url|null) {
+    return this.service.createTabAt(pos, url);
+  }
+
+  static getInstance(): TabStripApiProxy {
+    return instance || (instance = new TabStripApiProxyImpl());
+  }
+}
+
+let instance: TabStripApiProxy|null = null;
diff --git a/chrome/browser/safe_browsing/download_protection/check_client_download_request.cc b/chrome/browser/safe_browsing/download_protection/check_client_download_request.cc
index 0a2b169..8f26d7ed 100644
--- a/chrome/browser/safe_browsing/download_protection/check_client_download_request.cc
+++ b/chrome/browser/safe_browsing/download_protection/check_client_download_request.cc
@@ -149,29 +149,29 @@
 }
 
 // static
-bool CheckClientDownloadRequest::IsSupportedDownload(
+MayCheckDownloadResult CheckClientDownloadRequest::IsSupportedDownload(
     const download::DownloadItem& item,
     const base::FilePath& file_name,
     DownloadCheckResultReason* reason) {
   if (item.GetUrlChain().empty()) {
     *reason = REASON_EMPTY_URL_CHAIN;
-    return false;
+    return MayCheckDownloadResult::kMayNotCheckDownload;
   }
   const GURL& final_url = item.GetUrlChain().back();
   if (!final_url.is_valid() || final_url.is_empty()) {
     *reason = REASON_INVALID_URL;
-    return false;
+    return MayCheckDownloadResult::kMayNotCheckDownload;
   }
   if (!final_url.IsStandard() && !final_url.SchemeIsBlob() &&
       !final_url.SchemeIs(url::kDataScheme)) {
     *reason = REASON_UNSUPPORTED_URL_SCHEME;
-    return false;
+    return MayCheckDownloadResult::kMayNotCheckDownload;
   }
   // TODO(crbug.com/41372015): Remove duplicated counting of REMOTE_FILE
   // and LOCAL_FILE in SBClientDownload.UnsupportedScheme.*.
   if (final_url.SchemeIsFile()) {
     *reason = final_url.has_host() ? REASON_REMOTE_FILE : REASON_LOCAL_FILE;
-    return false;
+    return MayCheckDownloadResult::kMayNotCheckDownload;
   }
   // On Android, do not use FileTypePolicies, which are currently only
   // applicable to desktop platforms. Instead, hardcode the APK filetype check
@@ -185,9 +185,9 @@
   if (!FileTypePolicies::GetInstance()->IsCheckedBinaryFile(file_name)) {
 #endif
     *reason = REASON_NOT_BINARY_FILE;
-    return false;
+    return MayCheckDownloadResult::kMaySendSampledPingOnly;
   }
-  return true;
+  return MayCheckDownloadResult::kMayCheckDownload;
 }
 
 CheckClientDownloadRequest::~CheckClientDownloadRequest() {
@@ -195,26 +195,15 @@
   item_->RemoveObserver(this);
 }
 
-bool CheckClientDownloadRequest::IsSupportedDownload(
+MayCheckDownloadResult CheckClientDownloadRequest::IsSupportedDownload(
     DownloadCheckResultReason* reason) {
-  bool is_supported_download =
-      IsSupportedDownload(*item_,
+  return IsSupportedDownload(*item_,
 #if BUILDFLAG(IS_ANDROID)
-                          /*file_name=*/item_->GetFileNameToReportUser(),
+                             /*file_name=*/item_->GetFileNameToReportUser(),
 #else
-                          /*file_name=*/item_->GetTargetFilePath(),
+                             /*file_name=*/item_->GetTargetFilePath(),
 #endif
-                          reason);
-
-#if BUILDFLAG(IS_ANDROID)
-  if (!is_supported_download) {
-    DownloadProtectionMetricsData::SetOutcome(
-        item_, DownloadProtectionMetricsData::ConvertDownloadCheckResultReason(
-                   *reason));
-  }
-#endif
-
-  return is_supported_download;
+                             reason);
 }
 
 download::DownloadItem* CheckClientDownloadRequest::item() const {
diff --git a/chrome/browser/safe_browsing/download_protection/check_client_download_request.h b/chrome/browser/safe_browsing/download_protection/check_client_download_request.h
index bc411fd..8c6d68e7 100644
--- a/chrome/browser/safe_browsing/download_protection/check_client_download_request.h
+++ b/chrome/browser/safe_browsing/download_protection/check_client_download_request.h
@@ -50,17 +50,22 @@
   void OnDownloadDestroyed(download::DownloadItem* download) override;
   void OnDownloadUpdated(download::DownloadItem* download) override;
 
-  // Returns whether `item` is eligible for CheckClientDownloadRequest.
+  // Returns enum value indicating whether `item` is eligible for
+  // CheckClientDownloadRequest. If return value is not kMayCheckDownload, then
+  // `reason` will be populated with the reason why.
   // Note: Behavior is platform-specific.
-  static bool IsSupportedDownload(const download::DownloadItem& item,
-                                  const base::FilePath& file_name,
-                                  DownloadCheckResultReason* reason);
+  // TODO(chlily): Rename this method since it does not return a bool.
+  static MayCheckDownloadResult IsSupportedDownload(
+      const download::DownloadItem& item,
+      const base::FilePath& file_name,
+      DownloadCheckResultReason* reason);
 
   download::DownloadItem* item() const override;
 
  private:
   // CheckClientDownloadRequestBase overrides:
-  bool IsSupportedDownload(DownloadCheckResultReason* reason) override;
+  MayCheckDownloadResult IsSupportedDownload(
+      DownloadCheckResultReason* reason) override;
   content::BrowserContext* GetBrowserContext() const override;
   bool IsCancelled() override;
   base::WeakPtr<CheckClientDownloadRequestBase> GetWeakPtr() override;
diff --git a/chrome/browser/safe_browsing/download_protection/check_client_download_request_base.cc b/chrome/browser/safe_browsing/download_protection/check_client_download_request_base.cc
index 15a81484..0517960 100644
--- a/chrome/browser/safe_browsing/download_protection/check_client_download_request_base.cc
+++ b/chrome/browser/safe_browsing/download_protection/check_client_download_request_base.cc
@@ -201,33 +201,35 @@
   }
 
   DownloadCheckResultReason reason = REASON_MAX;
-  if (!IsSupportedDownload(&reason)) {
-    switch (reason) {
-      case REASON_EMPTY_URL_CHAIN:
-      case REASON_INVALID_URL:
-      case REASON_LOCAL_FILE:
-      case REASON_REMOTE_FILE:
-        FinishRequest(DownloadCheckResult::UNKNOWN, reason);
-        return;
-      case REASON_UNSUPPORTED_URL_SCHEME:
-        FinishRequest(DownloadCheckResult::UNKNOWN, reason);
-        return;
-      case REASON_NOT_BINARY_FILE:
-        if (ShouldSampleUnsupportedFile(target_file_path_)) {
-          // Send a "light ping" and don't use the verdict.
-          sampled_unsupported_file_ = true;
-          break;
-        }
-        RecordFileExtensionType(kDownloadExtensionUmaName, target_file_path_);
-        FinishRequest(DownloadCheckResult::UNKNOWN, reason);
-        return;
+  MayCheckDownloadResult may_check_download_result =
+      IsSupportedDownload(&reason);
 
-      default:
-        // We only expect the reasons explicitly handled above.
-        NOTREACHED();
+  if (may_check_download_result ==
+      MayCheckDownloadResult::kMayNotCheckDownload) {
+    CHECK(reason == REASON_EMPTY_URL_CHAIN || reason == REASON_INVALID_URL ||
+          reason == REASON_LOCAL_FILE || reason == REASON_REMOTE_FILE ||
+          reason == REASON_UNSUPPORTED_URL_SCHEME ||
+          reason == REASON_DOWNLOAD_DESTROYED);
+    FinishRequest(DownloadCheckResult::UNKNOWN, reason);
+    return;
+  }
+
+  RecordFileExtensionType(kDownloadExtensionUmaName, target_file_path_);
+
+  if (may_check_download_result ==
+      MayCheckDownloadResult::kMaySendSampledPingOnly) {
+    CHECK(reason == REASON_NOT_BINARY_FILE);
+    // Send a "light ping" and don't use the verdict.
+    sampled_unsupported_file_ = ShouldSampleUnsupportedFile(target_file_path_);
+    if (!sampled_unsupported_file_) {
+      FinishRequest(DownloadCheckResult::UNKNOWN, reason);
+      return;
     }
   }
-  RecordFileExtensionType(kDownloadExtensionUmaName, target_file_path_);
+
+  CHECK(may_check_download_result ==
+            MayCheckDownloadResult::kMayCheckDownload ||
+        sampled_unsupported_file_);
   download_request_maker_->Start(base::BindOnce(
       &CheckClientDownloadRequestBase::OnRequestBuilt, GetWeakPtr()));
 }
diff --git a/chrome/browser/safe_browsing/download_protection/check_client_download_request_base.h b/chrome/browser/safe_browsing/download_protection/check_client_download_request_base.h
index 5f8da46..cc26c0db 100644
--- a/chrome/browser/safe_browsing/download_protection/check_client_download_request_base.h
+++ b/chrome/browser/safe_browsing/download_protection/check_client_download_request_base.h
@@ -101,7 +101,13 @@
                                  DownloadCheckResultReason* reason,
                                  std::string* token) const;
 
-  virtual bool IsSupportedDownload(DownloadCheckResultReason* reason) = 0;
+  // Returns enum value indicating whether `item` is eligible for
+  // CheckClientDownloadRequest. If return value is not kMayCheckDownload, then
+  // `reason` will be populated with the reason why.
+  // TODO(chlily): Rename this method since it does not return a bool.
+  virtual MayCheckDownloadResult IsSupportedDownload(
+      DownloadCheckResultReason* reason) = 0;
+
   virtual content::BrowserContext* GetBrowserContext() const = 0;
   virtual bool IsCancelled() = 0;
   virtual base::WeakPtr<CheckClientDownloadRequestBase> GetWeakPtr() = 0;
diff --git a/chrome/browser/safe_browsing/download_protection/check_file_system_access_write_request.cc b/chrome/browser/safe_browsing/download_protection/check_file_system_access_write_request.cc
index a6e4c4bd..243e080e 100644
--- a/chrome/browser/safe_browsing/download_protection/check_file_system_access_write_request.cc
+++ b/chrome/browser/safe_browsing/download_protection/check_file_system_access_write_request.cc
@@ -61,18 +61,20 @@
   return nullptr;
 }
 
-bool CheckFileSystemAccessWriteRequest::IsSupportedDownload(
+MayCheckDownloadResult CheckFileSystemAccessWriteRequest::IsSupportedDownload(
     DownloadCheckResultReason* reason) {
   if (!weak_metadata_) {
-    return false;
+    *reason = REASON_DOWNLOAD_DESTROYED;
+    return MayCheckDownloadResult::kMayNotCheckDownload;
   }
 
+  // TODO(crbug.com/416080485): This should only check for APK files on Android.
   if (!FileTypePolicies::GetInstance()->IsCheckedBinaryFile(
           weak_metadata_->GetTargetFilePath())) {
     *reason = REASON_NOT_BINARY_FILE;
-    return false;
+    return MayCheckDownloadResult::kMaySendSampledPingOnly;
   }
-  return true;
+  return MayCheckDownloadResult::kMayCheckDownload;
 }
 
 content::BrowserContext* CheckFileSystemAccessWriteRequest::GetBrowserContext()
diff --git a/chrome/browser/safe_browsing/download_protection/check_file_system_access_write_request.h b/chrome/browser/safe_browsing/download_protection/check_file_system_access_write_request.h
index 367010c..14cb3e8 100644
--- a/chrome/browser/safe_browsing/download_protection/check_file_system_access_write_request.h
+++ b/chrome/browser/safe_browsing/download_protection/check_file_system_access_write_request.h
@@ -45,7 +45,8 @@
 
  private:
   // CheckClientDownloadRequestBase overrides:
-  bool IsSupportedDownload(DownloadCheckResultReason* reason) override;
+  MayCheckDownloadResult IsSupportedDownload(
+      DownloadCheckResultReason* reason) override;
   content::BrowserContext* GetBrowserContext() const override;
   bool IsCancelled() override;
   base::WeakPtr<CheckClientDownloadRequestBase> GetWeakPtr() override;
diff --git a/chrome/browser/safe_browsing/download_protection/download_protection_delegate.h b/chrome/browser/safe_browsing/download_protection/download_protection_delegate.h
index be97111..5b1113fc 100644
--- a/chrome/browser/safe_browsing/download_protection/download_protection_delegate.h
+++ b/chrome/browser/safe_browsing/download_protection/download_protection_delegate.h
@@ -7,6 +7,7 @@
 
 #include <memory>
 
+#include "chrome/browser/safe_browsing/download_protection/download_protection_util.h"
 #include "net/traffic_annotation/network_traffic_annotation.h"
 
 class GURL;
@@ -41,22 +42,26 @@
   // preferences.
   virtual bool ShouldCheckDownloadUrl(download::DownloadItem* item) const = 0;
 
-  // Returns whether the download item should be checked by
-  // CheckClientDownload() based on user preferences, properties of the file,
-  // and potentially random sampling.
+  // Returns whether the download item may be checked by CheckClientDownload().
+  // This is based on user preferences, properties of the file, and potentially
+  // random sampling.
+  // A return value of false indicates that the delegate does not permit the
+  // download to be checked.
+  // A return value of true indicates that checking the download is permitted,
+  // but caller may apply further logic to determine whether a check occurs.
   // TODO(chlily): Implementations of this method currently rely on the checks
   // in IsSupportedDownload. This is redundant. Refactor this logic to eliminate
   // IsSupportedDownload().
-  virtual bool ShouldCheckClientDownload(
-      download::DownloadItem* item) const = 0;
+  virtual bool MayCheckClientDownload(download::DownloadItem* item) const = 0;
 
-  // Returns whether the download item should be checked by
+  // Returns enum value indicating whether the download item may be checked by
   // CheckClientDownload() based on whether the file supports the check.
   // TODO(chlily): Remove this method. The only place where it is called seems
   // to be vestigial, and does not affect whether CheckClientDownload ultimately
   // happens.
-  virtual bool IsSupportedDownload(download::DownloadItem& item,
-                                   const base::FilePath& target_path) const = 0;
+  virtual MayCheckDownloadResult IsSupportedDownload(
+      download::DownloadItem& item,
+      const base::FilePath& target_path) const = 0;
 
   // Called immediately prior to serializing the ClientDownloadRequest into the
   // string to send in the POST request body, which is followed by sending out
diff --git a/chrome/browser/safe_browsing/download_protection/download_protection_delegate_android.cc b/chrome/browser/safe_browsing/download_protection/download_protection_delegate_android.cc
index 5a41eb5f..7488c4b3 100644
--- a/chrome/browser/safe_browsing/download_protection/download_protection_delegate_android.cc
+++ b/chrome/browser/safe_browsing/download_protection/download_protection_delegate_android.cc
@@ -122,7 +122,7 @@
   return IsAndroidDownloadProtectionEnabledForDownloadProfile(item);
 }
 
-bool DownloadProtectionDelegateAndroid::ShouldCheckClientDownload(
+bool DownloadProtectionDelegateAndroid::MayCheckClientDownload(
     download::DownloadItem* item) const {
   bool is_enabled = IsAndroidDownloadProtectionEnabledForDownloadProfile(item);
   if (is_enabled && !IsDownloadRequestUrlValid(download_request_url_)) {
@@ -133,19 +133,34 @@
     return false;
   }
 
-  if (!IsSupportedDownload(*item, item->GetFileNameToReportUser())) {
+  MayCheckDownloadResult may_check_download_result =
+      IsSupportedDownload(*item, item->GetFileNameToReportUser());
+  if (may_check_download_result ==
+      MayCheckDownloadResult::kMayNotCheckDownload) {
     return false;
   }
 
-  bool should_sample = should_sample_override_.value_or(ShouldSample());
-  if (!should_sample) {
-    DownloadProtectionMetricsData::SetOutcome(item, Outcome::kNotSampled);
+  // Apply random sampling only to eligible files (APK files).
+  // Note: this sampling performed by DownloadProtectionDelegateAndroid is
+  // distinct from sampling for "light" pings for unsupported filetypes.
+  if (may_check_download_result == MayCheckDownloadResult::kMayCheckDownload) {
+    bool should_sample = should_sample_override_.value_or(ShouldSample());
+    if (!should_sample) {
+      DownloadProtectionMetricsData::SetOutcome(item, Outcome::kNotSampled);
+    }
+    should_sample_override_ = std::nullopt;
+    return should_sample;
   }
-  should_sample_override_ = std::nullopt;
-  return should_sample;
+
+  // "Light" sampled pings for unsupported filetypes are not supported on
+  // Android. GetUnsupportedFileSampleRate() enforces that later, so return true
+  // here to be consistent with the semantics of MayCheckDownloadResult.
+  CHECK_EQ(may_check_download_result,
+           MayCheckDownloadResult::kMaySendSampledPingOnly);
+  return true;
 }
 
-bool DownloadProtectionDelegateAndroid::IsSupportedDownload(
+MayCheckDownloadResult DownloadProtectionDelegateAndroid::IsSupportedDownload(
     download::DownloadItem& item,
     const base::FilePath& target_path) const {
   // On Android, the target path is likely a content-URI. Therefore, use the
@@ -156,14 +171,14 @@
   base::FilePath file_name = item.GetFileNameToReportUser();
 
   DownloadCheckResultReason reason = REASON_MAX;
-  if (!CheckClientDownloadRequest::IsSupportedDownload(item, file_name,
-                                                       &reason)) {
+  MayCheckDownloadResult may_check_download_result =
+      CheckClientDownloadRequest::IsSupportedDownload(item, file_name, &reason);
+  if (may_check_download_result != MayCheckDownloadResult::kMayCheckDownload) {
     DownloadProtectionMetricsData::SetOutcome(
         &item, DownloadProtectionMetricsData::ConvertDownloadCheckResultReason(
                    reason));
-    return false;
   }
-  return true;
+  return may_check_download_result;
 }
 
 void DownloadProtectionDelegateAndroid::PreSerializeRequest(
diff --git a/chrome/browser/safe_browsing/download_protection/download_protection_delegate_android.h b/chrome/browser/safe_browsing/download_protection/download_protection_delegate_android.h
index 4e552c4..321be2bc 100644
--- a/chrome/browser/safe_browsing/download_protection/download_protection_delegate_android.h
+++ b/chrome/browser/safe_browsing/download_protection/download_protection_delegate_android.h
@@ -8,6 +8,7 @@
 #include <optional>
 
 #include "chrome/browser/safe_browsing/download_protection/download_protection_delegate.h"
+#include "chrome/browser/safe_browsing/download_protection/download_protection_util.h"
 #include "net/traffic_annotation/network_traffic_annotation.h"
 #include "url/gurl.h"
 
@@ -34,9 +35,10 @@
 
   // DownloadProtectionDelegate:
   bool ShouldCheckDownloadUrl(download::DownloadItem* item) const override;
-  bool ShouldCheckClientDownload(download::DownloadItem* item) const override;
-  bool IsSupportedDownload(download::DownloadItem& item,
-                           const base::FilePath& target_path) const override;
+  bool MayCheckClientDownload(download::DownloadItem* item) const override;
+  MayCheckDownloadResult IsSupportedDownload(
+      download::DownloadItem& item,
+      const base::FilePath& target_path) const override;
   void PreSerializeRequest(const download::DownloadItem* item,
                            ClientDownloadRequest& request_proto) override;
   void FinalizeResourceRequest(
diff --git a/chrome/browser/safe_browsing/download_protection/download_protection_delegate_desktop.cc b/chrome/browser/safe_browsing/download_protection/download_protection_delegate_desktop.cc
index 4fe1e8b..b46c234 100644
--- a/chrome/browser/safe_browsing/download_protection/download_protection_delegate_desktop.cc
+++ b/chrome/browser/safe_browsing/download_protection/download_protection_delegate_desktop.cc
@@ -64,22 +64,27 @@
   return IsSafeBrowsingEnabledForDownloadProfile(item);
 }
 
-bool DownloadProtectionDelegateDesktop::ShouldCheckClientDownload(
+bool DownloadProtectionDelegateDesktop::MayCheckClientDownload(
     download::DownloadItem* item) const {
-  return IsSafeBrowsingEnabledForDownloadProfile(item) &&
-         IsSupportedDownload(*item, item->GetTargetFilePath());
+  if (!IsSafeBrowsingEnabledForDownloadProfile(item)) {
+    return false;
+  }
+  return IsSupportedDownload(*item, item->GetTargetFilePath()) !=
+         MayCheckDownloadResult::kMayNotCheckDownload;
 }
 
-bool DownloadProtectionDelegateDesktop::IsSupportedDownload(
+MayCheckDownloadResult DownloadProtectionDelegateDesktop::IsSupportedDownload(
     download::DownloadItem& item,
     const base::FilePath& target_path) const {
-  DownloadCheckResultReason ignored_reason = REASON_MAX;
   // TODO(nparker): Remove the CRX check here once can support
   // UNKNOWN types properly.  http://crbug.com/581044
+  if (download_type_util::GetDownloadType(target_path) ==
+      ClientDownloadRequest::CHROME_EXTENSION) {
+    return MayCheckDownloadResult::kMayNotCheckDownload;
+  }
+  DownloadCheckResultReason ignored_reason = REASON_MAX;
   return CheckClientDownloadRequest::IsSupportedDownload(item, target_path,
-                                                         &ignored_reason) &&
-         download_type_util::GetDownloadType(target_path) !=
-             ClientDownloadRequest::CHROME_EXTENSION;
+                                                         &ignored_reason);
 }
 
 void DownloadProtectionDelegateDesktop::FinalizeResourceRequest(
diff --git a/chrome/browser/safe_browsing/download_protection/download_protection_delegate_desktop.h b/chrome/browser/safe_browsing/download_protection/download_protection_delegate_desktop.h
index bff9409..99f9886 100644
--- a/chrome/browser/safe_browsing/download_protection/download_protection_delegate_desktop.h
+++ b/chrome/browser/safe_browsing/download_protection/download_protection_delegate_desktop.h
@@ -6,6 +6,7 @@
 #define CHROME_BROWSER_SAFE_BROWSING_DOWNLOAD_PROTECTION_DOWNLOAD_PROTECTION_DELEGATE_DESKTOP_H_
 
 #include "chrome/browser/safe_browsing/download_protection/download_protection_delegate.h"
+#include "chrome/browser/safe_browsing/download_protection/download_protection_util.h"
 #include "net/traffic_annotation/network_traffic_annotation.h"
 #include "url/gurl.h"
 
@@ -30,9 +31,10 @@
 
   // DownloadProtectionDelegate:
   bool ShouldCheckDownloadUrl(download::DownloadItem* item) const override;
-  bool ShouldCheckClientDownload(download::DownloadItem* item) const override;
-  bool IsSupportedDownload(download::DownloadItem& item,
-                           const base::FilePath& target_path) const override;
+  bool MayCheckClientDownload(download::DownloadItem* item) const override;
+  MayCheckDownloadResult IsSupportedDownload(
+      download::DownloadItem& item,
+      const base::FilePath& target_path) const override;
   void FinalizeResourceRequest(
       network::ResourceRequest& resource_request) override;
   const GURL& GetDownloadRequestUrl() const override;
diff --git a/chrome/browser/safe_browsing/download_protection/download_protection_service.cc b/chrome/browser/safe_browsing/download_protection/download_protection_service.cc
index 99d3115..d9c0c258f 100644
--- a/chrome/browser/safe_browsing/download_protection/download_protection_service.cc
+++ b/chrome/browser/safe_browsing/download_protection/download_protection_service.cc
@@ -242,21 +242,16 @@
   CHECK(!settings.has_value());
 #endif
 
-  if (delegate_->ShouldCheckClientDownload(item)) {
+  if (delegate_->MayCheckClientDownload(item)) {
     CheckClientDownload(item, std::move(callback), /*password=*/std::nullopt);
     return true;
   }
 
 #if !BUILDFLAG(IS_ANDROID)
   if (settings.has_value()) {
-    Profile* profile = Profile::FromBrowserContext(
-        content::DownloadItemUtils::GetBrowserContext(item));
-    bool safe_browsing_enabled =
-        profile && IsSafeBrowsingEnabled(*profile->GetPrefs());
     DCHECK(report_only_scan);
-    DCHECK(!safe_browsing_enabled);
-    // Since this branch implies that Safe Browsing is disabled, the pre-deep
-    // scanning DownloadCheckResult is considered UNKNOWN.
+    // Since this branch implies that CheckClientDownload was not called, the
+    // pre-deep scanning DownloadCheckResult is considered UNKNOWN.
     UploadForDeepScanning(
         std::make_unique<DownloadItemMetadata>(item), std::move(callback),
         DownloadItemWarningData::DeepScanTrigger::TRIGGER_POLICY,
@@ -320,7 +315,8 @@
 bool DownloadProtectionService::IsSupportedDownload(
     download::DownloadItem& item,
     const base::FilePath& target_path) const {
-  return delegate_->IsSupportedDownload(item, target_path);
+  return delegate_->IsSupportedDownload(item, target_path) !=
+         MayCheckDownloadResult::kMayNotCheckDownload;
 }
 
 void DownloadProtectionService::CheckPPAPIDownloadRequest(
diff --git a/chrome/browser/safe_browsing/download_protection/download_protection_service.h b/chrome/browser/safe_browsing/download_protection/download_protection_service.h
index 904f036..f2db12b 100644
--- a/chrome/browser/safe_browsing/download_protection/download_protection_service.h
+++ b/chrome/browser/safe_browsing/download_protection/download_protection_service.h
@@ -110,7 +110,11 @@
       base::optional_ref<const std::string> password = std::nullopt);
 
   // Checks the user permissions, then calls |CheckClientDownload| if
-  // appropriate. Returns whether we began scanning.
+  // appropriate. Returns whether we may do more asynchronous work to check the
+  // download, such as sending a ping (potentially subject to sampling or other
+  // eligibility checks). If this returns true, the `callback` will be invoked
+  // with the result of a check. If this returns false, the `callback` will not
+  // be run.
   virtual bool MaybeCheckClientDownload(
       download::DownloadItem* item,
       CheckDownloadRepeatingCallback callback);
@@ -134,8 +138,9 @@
   virtual void CheckDownloadUrl(download::DownloadItem* item,
                                 CheckDownloadCallback callback);
 
-  // Returns true iff the download specified by |info| should be scanned by
-  // CheckClientDownload() for malicious content.
+  // Returns true if the download specified by |info| may be scanned by
+  // CheckClientDownload() for malicious content. (Caller may apply further
+  // logic to determine if a scan occurs, such as sampling or other checks.)
   // May modify the DownloadItem with a SupportsUserData::Data.
   virtual bool IsSupportedDownload(download::DownloadItem& item,
                                    const base::FilePath& target_path) const;
diff --git a/chrome/browser/safe_browsing/download_protection/download_protection_service_unittest.cc b/chrome/browser/safe_browsing/download_protection/download_protection_service_unittest.cc
index 7decb7e..c983ee2 100644
--- a/chrome/browser/safe_browsing/download_protection/download_protection_service_unittest.cc
+++ b/chrome/browser/safe_browsing/download_protection/download_protection_service_unittest.cc
@@ -384,7 +384,11 @@
       : ChromeRenderViewHostTestHarness(time_source),
         in_process_utility_thread_helper_(
             std::make_unique<content::InProcessUtilityThreadHelper>()),
-        testing_profile_manager_(TestingBrowserProcess::GetGlobal()) {}
+        testing_profile_manager_(TestingBrowserProcess::GetGlobal()) {
+#if BUILDFLAG(IS_ANDROID)
+    EnableFeatures({kMaliciousApkDownloadCheck});
+#endif
+  }
 
   void SetUp() override {
     ChromeRenderViewHostTestHarness::SetUp();
@@ -945,7 +949,12 @@
 class EnhancedProtectionDownloadTest : public DownloadProtectionServiceTest {
  public:
   EnhancedProtectionDownloadTest() {
-    EnableFeatures({kSafeBrowsingRemoveCookiesInAuthRequests});
+    EnableFeatures({
+        kSafeBrowsingRemoveCookiesInAuthRequests,
+#if BUILDFLAG(IS_ANDROID)
+        kMaliciousApkDownloadCheck,
+#endif
+    });
   }
 };
 
@@ -1005,6 +1014,10 @@
                              "http://www.google.com/",     // referrer
                              FILE_PATH_LITERAL("a.tmp"),   // tmp_path
                              FILE_PATH_LITERAL("a.exe"));  // final_path
+
+    // Though MayCheckClientDownload returns false, if CheckClientDownload()
+    // were in fact called anyway, test that we do not ultimately send a ping.
+    EXPECT_FALSE(download_service_->delegate()->MayCheckClientDownload(&item));
     RunLoop run_loop;
     download_service_->CheckClientDownload(
         &item,
@@ -1021,6 +1034,10 @@
                              "http://www.google.com/",           // referrer
                              FILE_PATH_LITERAL("a.tmp"),         // tmp_path
                              FILE_PATH_LITERAL("a.exe"));        // final_path
+
+    // Though MayCheckClientDownload returns false, if CheckClientDownload()
+    // were in fact called anyway, test that we do not ultimately send a ping.
+    EXPECT_FALSE(download_service_->delegate()->MayCheckClientDownload(&item));
     RunLoop run_loop;
     download_service_->CheckClientDownload(
         &item,
@@ -1034,18 +1051,30 @@
 
 TEST_F(DownloadProtectionServiceTest, CheckClientDownloadNotABinary) {
   NiceMockDownloadItem item;
-  PrepareBasicDownloadItem(&item,
-                           std::vector<std::string>(),   // empty url_chain
-                           "http://www.google.com/",     // referrer
-                           FILE_PATH_LITERAL("a.tmp"),   // tmp_path
-                           FILE_PATH_LITERAL("a.txt"));  // final_path
+  PrepareBasicDownloadItem(
+      &item, {"http://www.evil.com/a.txt"},  // url_chain
+      "http://www.google.com/",              // referrer
+      FILE_PATH_LITERAL("a.tmp"),            // tmp_path
+      FILE_PATH_LITERAL("a.txt"),            // final_path
+      // Do not use the default APK filename override for Android in
+      // PrepareBasicDownloadItem.
+      base::FilePath(FILE_PATH_LITERAL("a.txt")));  // display_name
+  content::DownloadItemUtils::AttachInfoForTesting(&item, profile(), nullptr);
+
+  EXPECT_EQ(
+      download_service_->delegate()->IsSupportedDownload(item, final_path_),
+      MayCheckDownloadResult::kMaySendSampledPingOnly);
+
+  // This returns true because of the possibility of sampling an unsupported
+  // file type.
   RunLoop run_loop;
-  download_service_->CheckClientDownload(
+  EXPECT_TRUE(download_service_->MaybeCheckClientDownload(
       &item,
       base::BindRepeating(&DownloadProtectionServiceTest::CheckDoneCallback,
-                          base::Unretained(this), run_loop.QuitClosure()));
+                          base::Unretained(this), run_loop.QuitClosure())));
   run_loop.Run();
   EXPECT_TRUE(IsResult(DownloadCheckResult::UNKNOWN));
+  // But the ping is not ultimately sent because binary sampling is off.
   EXPECT_FALSE(HasClientDownloadRequest());
 }
 
@@ -1069,7 +1098,7 @@
                   tmp_path_, BinaryFeatureExtractor::kDefaultOptions, _, _))
       .Times(3);
 
-  // We should not get whilelist checks for other URLs than specified below.
+  // We should not get allowlist checks for other URLs than specified below.
   EXPECT_CALL(*sb_service_->mock_database_manager(),
               MatchDownloadAllowlistUrl(_, _))
       .Times(0);
@@ -1092,10 +1121,10 @@
     // With no referrer and just the bad url, should be marked DANGEROUS.
     url_chain_.emplace_back("http://www.evil.com/bla.exe");
     RunLoop run_loop;
-    download_service_->CheckClientDownload(
+    EXPECT_TRUE(download_service_->MaybeCheckClientDownload(
         &item,
         base::BindRepeating(&DownloadProtectionServiceTest::CheckDoneCallback,
-                            base::Unretained(this), run_loop.QuitClosure()));
+                            base::Unretained(this), run_loop.QuitClosure())));
     run_loop.Run();
     EXPECT_TRUE(IsResult(DownloadCheckResult::DANGEROUS));
     ASSERT_TRUE(HasClientDownloadRequest());
@@ -1107,10 +1136,10 @@
     // Check that the referrer is not matched against the allowlist.
     referrer_ = GURL("http://www.google.com/");
     RunLoop run_loop;
-    download_service_->CheckClientDownload(
+    EXPECT_TRUE(download_service_->MaybeCheckClientDownload(
         &item,
         base::BindRepeating(&DownloadProtectionServiceTest::CheckDoneCallback,
-                            base::Unretained(this), run_loop.QuitClosure()));
+                            base::Unretained(this), run_loop.QuitClosure())));
     run_loop.Run();
 
     EXPECT_TRUE(IsResult(DownloadCheckResult::DANGEROUS));
@@ -1124,10 +1153,10 @@
     // Redirect from a site shouldn't be checked either.
     url_chain_.emplace(url_chain_.begin(), "http://www.google.com/redirect");
     RunLoop run_loop;
-    download_service_->CheckClientDownload(
+    EXPECT_TRUE(download_service_->MaybeCheckClientDownload(
         &item,
         base::BindRepeating(&DownloadProtectionServiceTest::CheckDoneCallback,
-                            base::Unretained(this), run_loop.QuitClosure()));
+                            base::Unretained(this), run_loop.QuitClosure())));
     run_loop.Run();
     EXPECT_TRUE(IsResult(DownloadCheckResult::DANGEROUS));
     ASSERT_TRUE(HasClientDownloadRequest());
@@ -1139,10 +1168,10 @@
     // Only if the final url is allowlisted should it be SAFE.
     url_chain_.emplace_back("http://www.google.com/a.exe");
     RunLoop run_loop;
-    download_service_->CheckClientDownload(
+    EXPECT_TRUE(download_service_->MaybeCheckClientDownload(
         &item,
         base::BindRepeating(&DownloadProtectionServiceTest::CheckDoneCallback,
-                            base::Unretained(this), run_loop.QuitClosure()));
+                            base::Unretained(this), run_loop.QuitClosure())));
     run_loop.Run();
     EXPECT_TRUE(IsResult(DownloadCheckResult::SAFE));
     // TODO(grt): Make the service produce the request even when the URL is
@@ -1193,10 +1222,10 @@
         &item, profile()->GetPrimaryOTRProfile(/*create_if_needed=*/true),
         nullptr);
     RunLoop run_loop;
-    download_service_->CheckClientDownload(
+    EXPECT_TRUE(download_service_->MaybeCheckClientDownload(
         &item,
         base::BindRepeating(&DownloadProtectionServiceTest::CheckDoneCallback,
-                            base::Unretained(this), run_loop.QuitClosure()));
+                            base::Unretained(this), run_loop.QuitClosure())));
     run_loop.Run();
     EXPECT_TRUE(IsResult(DownloadCheckResult::SAFE));
     EXPECT_FALSE(HasClientDownloadRequest());
@@ -1209,10 +1238,10 @@
         &item, profile()->GetPrimaryOTRProfile(/*create_if_needed=*/true),
         nullptr);
     RunLoop run_loop;
-    download_service_->CheckClientDownload(
+    EXPECT_TRUE(download_service_->MaybeCheckClientDownload(
         &item,
         base::BindRepeating(&DownloadProtectionServiceTest::CheckDoneCallback,
-                            base::Unretained(this), run_loop.QuitClosure()));
+                            base::Unretained(this), run_loop.QuitClosure())));
     run_loop.Run();
     EXPECT_TRUE(IsResult(DownloadCheckResult::SAFE));
     EXPECT_FALSE(HasClientDownloadRequest());
@@ -1222,10 +1251,10 @@
     //           ClientDownloadRequest should NOT be sent.
     content::DownloadItemUtils::AttachInfoForTesting(&item, profile(), nullptr);
     RunLoop run_loop;
-    download_service_->CheckClientDownload(
+    EXPECT_TRUE(download_service_->MaybeCheckClientDownload(
         &item,
         base::BindRepeating(&DownloadProtectionServiceTest::CheckDoneCallback,
-                            base::Unretained(this), run_loop.QuitClosure()));
+                            base::Unretained(this), run_loop.QuitClosure())));
     run_loop.Run();
     EXPECT_TRUE(IsResult(DownloadCheckResult::SAFE));
     EXPECT_FALSE(HasClientDownloadRequest());
@@ -1237,10 +1266,10 @@
     SetExtendedReportingPreference(true);
     content::DownloadItemUtils::AttachInfoForTesting(&item, profile(), nullptr);
     RunLoop run_loop;
-    download_service_->CheckClientDownload(
+    EXPECT_TRUE(download_service_->MaybeCheckClientDownload(
         &item,
         base::BindRepeating(&DownloadProtectionServiceTest::CheckDoneCallback,
-                            base::Unretained(this), run_loop.QuitClosure()));
+                            base::Unretained(this), run_loop.QuitClosure())));
     run_loop.Run();
     EXPECT_TRUE(IsResult(DownloadCheckResult::SAFE));
     ASSERT_TRUE(HasClientDownloadRequest());
@@ -1284,10 +1313,12 @@
         &item, profile()->GetPrimaryOTRProfile(/*create_if_needed=*/true),
         nullptr);
     RunLoop run_loop;
-    download_service_->CheckClientDownload(
+    // This returns true because of the possibility of sampling an unsupported
+    // file type.
+    EXPECT_TRUE(download_service_->MaybeCheckClientDownload(
         &item,
         base::BindRepeating(&DownloadProtectionServiceTest::CheckDoneCallback,
-                            base::Unretained(this), run_loop.QuitClosure()));
+                            base::Unretained(this), run_loop.QuitClosure())));
     run_loop.Run();
     EXPECT_TRUE(IsResult(DownloadCheckResult::UNKNOWN));
     EXPECT_FALSE(HasClientDownloadRequest());
@@ -1297,10 +1328,10 @@
     //           A "light" ClientDownloadRequest should be sent.
     content::DownloadItemUtils::AttachInfoForTesting(&item, profile(), nullptr);
     RunLoop run_loop;
-    download_service_->CheckClientDownload(
+    EXPECT_TRUE(download_service_->MaybeCheckClientDownload(
         &item,
         base::BindRepeating(&DownloadProtectionServiceTest::CheckDoneCallback,
-                            base::Unretained(this), run_loop.QuitClosure()));
+                            base::Unretained(this), run_loop.QuitClosure())));
     run_loop.Run();
     EXPECT_TRUE(IsResult(DownloadCheckResult::UNKNOWN));
     ASSERT_TRUE(HasClientDownloadRequest());
@@ -1326,10 +1357,10 @@
         &item, profile()->GetPrimaryOTRProfile(/*create_if_needed=*/true),
         nullptr);
     RunLoop run_loop;
-    download_service_->CheckClientDownload(
+    EXPECT_TRUE(download_service_->MaybeCheckClientDownload(
         &item,
         base::BindRepeating(&DownloadProtectionServiceTest::CheckDoneCallback,
-                            base::Unretained(this), run_loop.QuitClosure()));
+                            base::Unretained(this), run_loop.QuitClosure())));
     run_loop.Run();
     EXPECT_TRUE(IsResult(DownloadCheckResult::UNKNOWN));
     EXPECT_FALSE(HasClientDownloadRequest());
@@ -1339,10 +1370,10 @@
     //           ClientDownloadRequest should NOT be sent.
     content::DownloadItemUtils::AttachInfoForTesting(&item, profile(), nullptr);
     RunLoop run_loop;
-    download_service_->CheckClientDownload(
+    EXPECT_TRUE(download_service_->MaybeCheckClientDownload(
         &item,
         base::BindRepeating(&DownloadProtectionServiceTest::CheckDoneCallback,
-                            base::Unretained(this), run_loop.QuitClosure()));
+                            base::Unretained(this), run_loop.QuitClosure())));
     run_loop.Run();
     EXPECT_TRUE(IsResult(DownloadCheckResult::UNKNOWN));
     EXPECT_FALSE(HasClientDownloadRequest());
@@ -5636,6 +5667,8 @@
         ->SetNextShouldSampleForTesting(should_sample);
   }
 
+  // Expects MaybeCheckClientDownload to return false, and therefore no ping to
+  // be sent.
   void ExpectNoCheckClientDownload(download::DownloadItem* item) {
     EXPECT_CALL(*binary_feature_extractor_.get(), CheckSignature(_, _))
         .Times(0);
@@ -5650,9 +5683,11 @@
     Mock::VerifyAndClearExpectations(binary_feature_extractor_.get());
   }
 
+  // Expects MaybeCheckClientDownload to return true, and to send a ping over
+  // the network.
   void ExpectCheckClientDownload(
       download::DownloadItem* item,
-      base::optional_ref<GURL> download_request_url) {
+      base::optional_ref<GURL> download_request_url = std::nullopt) {
     EXPECT_CALL(*binary_feature_extractor_.get(), CheckSignature(tmp_path_, _))
         .Times(1);
     EXPECT_CALL(*binary_feature_extractor_.get(),
@@ -5831,6 +5866,10 @@
 
 TEST_P(AndroidDownloadProtectionTest,
        NoCheckClientDownloadForNonSupportedType) {
+  if (!ShouldAndroidDownloadProtectionBeActive()) {
+    return;
+  }
+
   // Response to any requests will be DANGEROUS.
   PrepareResponse(ClientDownloadResponse::DANGEROUS, net::HTTP_OK, net::OK);
   // Override the random sampling.
@@ -5846,15 +5885,43 @@
         /*display_name=*/
         base::FilePath(FILE_PATH_LITERAL("not_supported_filetype.dex")));
 
-    ExpectNoCheckClientDownload(&item);
+    EXPECT_EQ(
+        download_service_->delegate()->IsSupportedDownload(item, display_name_),
+        MayCheckDownloadResult::kMaySendSampledPingOnly);
+
+    EXPECT_CALL(*binary_feature_extractor_.get(), CheckSignature(_, _))
+        .Times(0);
+    EXPECT_CALL(*binary_feature_extractor_.get(),
+                ExtractImageFeatures(_, _, _, _))
+        .Times(0);
+
+    // This returns true because there may be a ping sent, as far as
+    // DownloadProtectionService knows at this point...
+    RunLoop run_loop;
+    EXPECT_TRUE(download_service_->MaybeCheckClientDownload(
+        &item,
+        base::BindRepeating(&AndroidDownloadProtectionTest::CheckDoneCallback,
+                            base::Unretained(this), run_loop.QuitClosure())));
+    run_loop.Run();
+
+    // ... except that we are guaranteed not to send a ping because the sample
+    // rate for unsupported file types is 0.
+    EXPECT_EQ(download_service_->delegate()->GetUnsupportedFileSampleRate(
+                  display_name_),
+              0.0);
+    EXPECT_FALSE(HasClientDownloadRequest());
+
+    Mock::VerifyAndClearExpectations(binary_feature_extractor_.get());
   }
   // The histogram is logged when the item goes out of scope.
-  ExpectHistogramUniqueSample(ShouldAndroidDownloadProtectionBeActive()
-                                  ? Outcome::kDownloadNotSupportedType
-                                  : Outcome::kDownloadProtectionDisabled);
+  ExpectHistogramUniqueSample(Outcome::kDownloadNotSupportedType);
 }
 
 TEST_P(AndroidDownloadProtectionTest, NoCheckClientDownloadNotSampled) {
+  if (!ShouldAndroidDownloadProtectionBeActive()) {
+    return;
+  }
+
   // Response to any requests will be DANGEROUS.
   PrepareResponse(ClientDownloadResponse::DANGEROUS, net::HTTP_OK, net::OK);
   // Override the random sampling to guarantee we won't sample.
@@ -5870,12 +5937,12 @@
         /*display_name=*/
         base::FilePath(kApkFilename));
 
+    // The sampling from the delegate results in MaybeCheckClientDownload
+    // returning false.
     ExpectNoCheckClientDownload(&item);
   }
   // The histogram is logged when the item goes out of scope.
-  ExpectHistogramUniqueSample(ShouldAndroidDownloadProtectionBeActive()
-                                  ? Outcome::kNotSampled
-                                  : Outcome::kDownloadProtectionDisabled);
+  ExpectHistogramUniqueSample(Outcome::kNotSampled);
 }
 
 // Tests the various false outcomes of IsSupportedDownload().
@@ -5893,8 +5960,9 @@
     EXPECT_CALL(item_empty_url_chain, GetUrlChain())
         .WillRepeatedly(ReturnRef(empty_url_chain));
 
-    EXPECT_FALSE(download_service_->IsSupportedDownload(item_empty_url_chain,
-                                                        final_path_));
+    EXPECT_EQ(download_service_->delegate()->IsSupportedDownload(
+                  item_empty_url_chain, final_path_),
+              MayCheckDownloadResult::kMayNotCheckDownload);
   }
   ExpectHistogramUniqueSample(Outcome::kEmptyUrlChain);
 
@@ -5908,8 +5976,9 @@
         /*referrer_url=*/"",
         /*display_name=*/base::FilePath(kApkFilename));
 
-    EXPECT_FALSE(
-        download_service_->IsSupportedDownload(item_invalid_url, final_path_));
+    EXPECT_EQ(download_service_->delegate()->IsSupportedDownload(
+                  item_invalid_url, final_path_),
+              MayCheckDownloadResult::kMayNotCheckDownload);
   }
   ExpectHistogramUniqueSample(Outcome::kInvalidUrl);
 
@@ -5924,8 +5993,9 @@
         /*referrer_url=*/"",
         /*display_name=*/base::FilePath(kApkFilename));
 
-    EXPECT_FALSE(download_service_->IsSupportedDownload(
-        item_unsupported_url_scheme, final_path_));
+    EXPECT_EQ(download_service_->delegate()->IsSupportedDownload(
+                  item_unsupported_url_scheme, final_path_),
+              MayCheckDownloadResult::kMayNotCheckDownload);
   }
   ExpectHistogramUniqueSample(Outcome::kUnsupportedUrlScheme);
 
@@ -5940,8 +6010,9 @@
         /*referrer_url=*/"",
         /*display_name=*/base::FilePath(kApkFilename));
 
-    EXPECT_FALSE(download_service_->IsSupportedDownload(item_remote_file_url,
-                                                        final_path_));
+    EXPECT_EQ(download_service_->delegate()->IsSupportedDownload(
+                  item_remote_file_url, final_path_),
+              MayCheckDownloadResult::kMayNotCheckDownload);
   }
   ExpectHistogramUniqueSample(Outcome::kRemoteFile);
 
@@ -5955,8 +6026,9 @@
         /*referrer_url=*/"",
         /*display_name=*/base::FilePath(kApkFilename));
 
-    EXPECT_FALSE(download_service_->IsSupportedDownload(item_local_file_url,
-                                                        final_path_));
+    EXPECT_EQ(download_service_->delegate()->IsSupportedDownload(
+                  item_local_file_url, final_path_),
+              MayCheckDownloadResult::kMayNotCheckDownload);
   }
   ExpectHistogramUniqueSample(Outcome::kLocalFile);
 
@@ -5971,8 +6043,9 @@
         /*referrer_url=*/"",
         /*display_name=*/base::FilePath(FILE_PATH_LITERAL("a.dex")));
 
-    EXPECT_FALSE(download_service_->IsSupportedDownload(
-        item_display_name_not_apk, final_path_));
+    EXPECT_EQ(download_service_->delegate()->IsSupportedDownload(
+                  item_display_name_not_apk, final_path_),
+              MayCheckDownloadResult::kMaySendSampledPingOnly);
   }
   ExpectHistogramUniqueSample(Outcome::kDownloadNotSupportedType);
 }
diff --git a/chrome/browser/safe_browsing/download_protection/download_protection_util.h b/chrome/browser/safe_browsing/download_protection/download_protection_util.h
index 437259b..08a9299 100644
--- a/chrome/browser/safe_browsing/download_protection/download_protection_util.h
+++ b/chrome/browser/safe_browsing/download_protection/download_protection_util.h
@@ -111,6 +111,21 @@
   kIncorrectPassword = 8,
   kMaxValue = kIncorrectPassword,
 };
+
+// Describes whether a given download may send a download ping.
+enum class MayCheckDownloadResult {
+  // The download may not send a ping. This may be due to properties of the
+  // download/file itself (see DownloadCheckResultReason) or due to other logic
+  // applied by DownloadProtection{Service,Delegate}.
+  kMayNotCheckDownload,
+  // The download may send a ping, but only a "light" ping may be sent if the
+  // download is sampled.
+  kMaySendSampledPingOnly,
+  // The download is fully supported for CheckClientDownload and may send a full
+  // download ping.
+  kMayCheckDownload,
+};
+
 void LogDeepScanEvent(download::DownloadItem* item, DeepScanEvent event);
 void LogDeepScanEvent(const DeepScanningMetadata& metadata,
                       DeepScanEvent event);
diff --git a/chrome/browser/safe_browsing/phishy_interaction_tracker_unittest.cc b/chrome/browser/safe_browsing/phishy_interaction_tracker_unittest.cc
index 3b2831c..f40b805 100644
--- a/chrome/browser/safe_browsing/phishy_interaction_tracker_unittest.cc
+++ b/chrome/browser/safe_browsing/phishy_interaction_tracker_unittest.cc
@@ -29,6 +29,10 @@
 #include "testing/gtest/include/gtest/gtest.h"
 #include "url/gurl.h"
 
+#if BUILDFLAG(IS_CHROMEOS)
+#include "chrome/test/base/scoped_testing_local_state.h"
+#endif  // BUILDFLAG(IS_CHROMEOS)
+
 using content::WebContents;
 
 using safe_browsing::PhishyInteractionTracker;
@@ -82,6 +86,7 @@
 
   void SetUp() override {
     browser_process_ = TestingBrowserProcess::GetGlobal();
+
     sb_service_ =
         base::MakeRefCounted<safe_browsing::TestSafeBrowsingService>();
     sb_service_->SetUseTestUrlLoaderFactory(true);
@@ -96,12 +101,6 @@
         base::WrapUnique(new PhishyInteractionTracker(web_contents()));
     phishy_interaction_tracker_->SetUIManagerForTesting(ui_manager_.get());
     phishy_interaction_tracker_->HandlePageChanged();
-
-#if BUILDFLAG(IS_CHROMEOS)
-    // Local state is needed to construct ProxyConfigService, which is a
-    // dependency of PingManager on ChromeOS.
-    TestingBrowserProcess::GetGlobal()->SetLocalState(profile()->GetPrefs());
-#endif
   }
 
   void TearDown() override {
@@ -113,9 +112,6 @@
         FROM_HERE, phishy_interaction_tracker_.release());
     ui_manager_.reset();
     phishy_interaction_tracker_.reset();
-#if BUILDFLAG(IS_CHROMEOS)
-    TestingBrowserProcess::GetGlobal()->SetLocalState(nullptr);
-#endif
     base::RunLoop().RunUntilIdle();
     ChromeRenderViewHostTestHarness::TearDown();
   }
@@ -211,6 +207,14 @@
 
  protected:
   raw_ptr<TestingBrowserProcess> browser_process_;
+
+#if BUILDFLAG(IS_CHROMEOS)
+  // Local state is needed to construct ProxyConfigService, which is a
+  // dependency of PingManager on ChromeOS.
+  ScopedTestingLocalState scoped_testing_local_state_{
+      TestingBrowserProcess::GetGlobal()};
+#endif  // BUILDFLAG(IS_CHROMEOS)
+
   scoped_refptr<safe_browsing::TestSafeBrowsingService> sb_service_;
   std::unique_ptr<PhishyInteractionTracker> phishy_interaction_tracker_;
   scoped_refptr<MockSafeBrowsingUIManager> ui_manager_;
diff --git a/chrome/browser/safe_browsing/safe_browsing_service_unittest.cc b/chrome/browser/safe_browsing/safe_browsing_service_unittest.cc
index 19ea4de1..7f4ff795 100644
--- a/chrome/browser/safe_browsing/safe_browsing_service_unittest.cc
+++ b/chrome/browser/safe_browsing/safe_browsing_service_unittest.cc
@@ -32,6 +32,10 @@
 #include "services/network/test/test_url_loader_factory.h"
 #include "services/network/test/test_utils.h"
 
+#if BUILDFLAG(IS_CHROMEOS)
+#include "chrome/test/base/scoped_testing_local_state.h"
+#endif  // BUILDFLAG(IS_CHROMEOS)
+
 using network::GetUploadData;
 using testing::Return;
 using testing::ReturnRef;
@@ -86,12 +90,6 @@
 
     profile_ = std::make_unique<TestingProfile>();
     profile2_ = std::make_unique<TestingProfile>();
-#if BUILDFLAG(IS_CHROMEOS)
-    // Local state is needed to construct ProxyConfigService, which is a
-    // dependency of PingManager on ChromeOS.
-    TestingBrowserProcess::GetGlobal()->SetLocalState(&local_state_);
-    RegisterLocalState(local_state_.registry());
-#endif
   }
 
   void TearDown() override {
@@ -99,9 +97,6 @@
     browser_process_->safe_browsing_service()->ShutDown();
     browser_process_->SetSafeBrowsingService(nullptr);
     safe_browsing::SafeBrowsingServiceInterface::RegisterFactory(nullptr);
-#if BUILDFLAG(IS_CHROMEOS)
-    TestingBrowserProcess::GetGlobal()->SetLocalState(nullptr);
-#endif
     base::RunLoop().RunUntilIdle();
   }
 
@@ -218,8 +213,15 @@
   content::BrowserTaskEnvironment task_environment_{
       base::test::TaskEnvironment::TimeSource::MOCK_TIME};
   raw_ptr<TestingBrowserProcess> browser_process_;
+
+#if BUILDFLAG(IS_CHROMEOS)
+  // Local state is needed to construct ProxyConfigService, which is a
+  // dependency of PingManager on ChromeOS.
+  ScopedTestingLocalState scoped_testing_local_state_{
+      TestingBrowserProcess::GetGlobal()};
+#endif  // BUILDFLAG(IS_CHROMEOS)
+
   scoped_refptr<SafeBrowsingService> sb_service_;
-  TestingPrefServiceSimple local_state_;
   TestingProfile::Builder profile_builder_;
   std::unique_ptr<TestingProfile> profile_;
   std::unique_ptr<TestingProfile> profile2_;
diff --git a/chrome/browser/screen_ai/BUILD.gn b/chrome/browser/screen_ai/BUILD.gn
index 30c325f..1853d91 100644
--- a/chrome/browser/screen_ai/BUILD.gn
+++ b/chrome/browser/screen_ai/BUILD.gn
@@ -46,6 +46,8 @@
 
 source_set("screen_ai_service_router_factory") {
   sources = [
+    "screen_ai_service_handler.cc",
+    "screen_ai_service_handler.h",
     "screen_ai_service_router.cc",
     "screen_ai_service_router.h",
     "screen_ai_service_router_factory.cc",
diff --git a/chrome/browser/screen_ai/optical_character_recognizer_browsertest.cc b/chrome/browser/screen_ai/optical_character_recognizer_browsertest.cc
index d3b8dcd4..1c9d0325 100644
--- a/chrome/browser/screen_ai/optical_character_recognizer_browsertest.cc
+++ b/chrome/browser/screen_ai/optical_character_recognizer_browsertest.cc
@@ -90,7 +90,9 @@
 void WaitForDisconnecting(screen_ai::ScreenAIServiceRouter* router,
                           base::OnceCallback<void()> callback,
                           int remaining_tries) {
-  if (!router->IsProcessRunningForTesting() || !remaining_tries) {
+  if (!router->IsProcessRunningForTesting(
+          screen_ai::ScreenAIServiceRouter::Service::kOCR) ||
+      !remaining_tries) {
     std::move(callback).Run();
     return;
   }
@@ -487,18 +489,21 @@
 
   // Init OCR once and verify service availability.
   ASSERT_TRUE(CreateAndInitOCR(mojom::OcrClientType::kTest));
-  ASSERT_TRUE(router->IsProcessRunningForTesting());
+  ASSERT_TRUE(router->IsProcessRunningForTesting(
+      screen_ai::ScreenAIServiceRouter::Service::kOCR));
 
   // Release it and wait for shutdown due to being idle.
   ocr().reset();
   base::test::TestFuture<void> future;
   WaitForDisconnecting(router, future.GetCallback(), /*remaining_tries=*/2);
   ASSERT_TRUE(future.Wait());
-  ASSERT_FALSE(router->IsProcessRunningForTesting());
+  ASSERT_FALSE(router->IsProcessRunningForTesting(
+      screen_ai::ScreenAIServiceRouter::Service::kOCR));
 
   // Init OCR again.
   ASSERT_TRUE(CreateAndInitOCR(mojom::OcrClientType::kTest));
-  ASSERT_TRUE(router->IsProcessRunningForTesting());
+  ASSERT_TRUE(router->IsProcessRunningForTesting(
+      screen_ai::ScreenAIServiceRouter::Service::kOCR));
 
   // Perform OCR.
   SkBitmap bitmap = LoadImageFromTestFile(
diff --git a/chrome/browser/screen_ai/screen_ai_service_handler.cc b/chrome/browser/screen_ai/screen_ai_service_handler.cc
new file mode 100644
index 0000000..67f0203
--- /dev/null
+++ b/chrome/browser/screen_ai/screen_ai_service_handler.cc
@@ -0,0 +1,473 @@
+// Copyright 2025 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/screen_ai/screen_ai_service_handler.h"
+
+#include <utility>
+#include <vector>
+
+#include "base/containers/flat_map.h"
+#include "base/files/file.h"
+#include "base/files/file_path.h"
+#include "base/files/file_util.h"
+#include "base/functional/bind.h"
+#include "base/location.h"
+#include "base/memory/memory_pressure_monitor.h"
+#include "base/metrics/histogram_functions.h"
+#include "base/strings/string_split.h"
+#include "base/strings/stringprintf.h"
+#include "base/system/sys_info.h"
+#include "base/task/sequenced_task_runner.h"
+#include "base/task/thread_pool.h"
+#include "chrome/browser/screen_ai/screen_ai_install_state.h"
+#include "chrome/browser/screen_ai/screen_ai_service_router.h"
+#include "content/public/browser/network_service_instance.h"
+#include "content/public/browser/service_process_host.h"
+#include "content/public/browser/service_process_host_passkeys.h"
+#include "mojo/public/mojom/base/file_path.mojom.h"
+#include "services/network/public/mojom/network_change_manager.mojom.h"
+#include "services/screen_ai/public/cpp/utilities.h"
+#include "ui/accessibility/accessibility_features.h"
+
+#if BUILDFLAG(IS_WIN)
+#include "base/strings/utf_string_conversions.h"
+#endif
+
+namespace screen_ai {
+
+bool IsModelFileContentReadable(base::File& file) {
+  if (!file.IsValid()) {
+    return false;
+  }
+  int file_size = file.GetLength();
+  if (!file_size) {
+    return false;
+  }
+  std::vector<uint8_t> buffer(file_size);
+  return file.ReadAndCheck(0, base::span(buffer));
+}
+
+// The name of the file that contains the list of files that are downloaded with
+// the component and are required to initialize the library.
+const base::FilePath::CharType kMainContentExtractionFilesList[] =
+    FILE_PATH_LITERAL("files_list_main_content_extraction.txt");
+const base::FilePath::CharType kOcrFilesList[] =
+    FILE_PATH_LITERAL("files_list_ocr.txt");
+
+class ComponentFiles {
+ public:
+  explicit ComponentFiles(const base::FilePath& library_binary_path,
+                          const base::FilePath::CharType* files_list_file_name);
+  ComponentFiles(const ComponentFiles&) = delete;
+  ComponentFiles& operator=(const ComponentFiles&) = delete;
+  ~ComponentFiles();
+
+  static std::unique_ptr<ComponentFiles> Load(
+      const base::FilePath::CharType* files_list_file_name);
+
+  base::flat_map<base::FilePath, base::File> model_files_;
+  base::FilePath library_binary_path_;
+};
+
+ComponentFiles::ComponentFiles(
+    const base::FilePath& library_binary_path,
+    const base::FilePath::CharType* files_list_file_name)
+    : library_binary_path_(library_binary_path) {
+  base::FilePath component_folder = library_binary_path.DirName();
+
+  // Get the files list.
+  std::string file_content;
+  if (!base::ReadFileToString(component_folder.Append(files_list_file_name),
+                              &file_content)) {
+    VLOG(0) << "Could not read list of files for " << files_list_file_name;
+    return;
+  }
+  std::vector<std::string> files_list = base::SplitString(
+      file_content, "\n", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
+  if (files_list.empty()) {
+    VLOG(0) << "Could not parse files list for " << files_list_file_name;
+    return;
+  }
+
+  for (auto& relative_file_path : files_list) {
+    // Ignore comment lines.
+    if (relative_file_path.empty() || relative_file_path[0] == '#') {
+      continue;
+    }
+
+#if BUILDFLAG(IS_WIN)
+    base::FilePath relative_path(base::UTF8ToWide(relative_file_path));
+#else
+    base::FilePath relative_path(relative_file_path);
+#endif
+    const base::FilePath full_path = component_folder.Append(relative_path);
+    model_files_[relative_path] =
+        base::File(full_path, base::File::FLAG_OPEN | base::File::FLAG_READ);
+    if (!IsModelFileContentReadable(model_files_[relative_path])) {
+      VLOG(0) << "Could not open " << full_path;
+      model_files_.clear();
+      return;
+    }
+  }
+}
+
+ComponentFiles::~ComponentFiles() {
+  if (model_files_.empty()) {
+    return;
+  }
+
+  // Transfer ownership of the file handles to a thread that may block, and let
+  // them get destroyed there.
+  base::ThreadPool::PostTask(
+      FROM_HERE,
+      {base::MayBlock(), base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN},
+      base::BindOnce(
+          [](base::flat_map<base::FilePath, base::File> model_files) {},
+          std::move(model_files_)));
+}
+
+std::unique_ptr<ComponentFiles> ComponentFiles::Load(
+    const base::FilePath::CharType* files_list_file_name) {
+  return std::make_unique<ComponentFiles>(
+      screen_ai::ScreenAIInstallState::GetInstance()
+          ->get_component_binary_path(),
+      files_list_file_name);
+}
+
+ScreenAIServiceHandler::ScreenAIServiceHandler(bool is_ocr)
+    : screen_ai_service_shutdown_handler_(this), is_ocr_(is_ocr) {}
+
+ScreenAIServiceHandler::~ScreenAIServiceHandler() = default;
+
+std::string ScreenAIServiceHandler::GetMetricFullName(
+    std::string_view metric_name) const {
+  return base::StringPrintf("Accessibility.%s.Service.%s",
+                            is_ocr_ ? "OCR" : "MainContentExtraction",
+                            metric_name);
+}
+
+std::optional<bool> ScreenAIServiceHandler::GetServiceState() {
+  if (GetAndRecordSuspendedState()) {
+    return false;
+  }
+
+  if (is_ocr_) {
+    if (ocr_service_.is_bound()) {
+      return true;
+    } else if (features::IsScreenAIOCREnabled()) {
+      return std::nullopt;
+    } else {
+      return false;
+    }
+  } else {
+    if (main_content_extraction_service_.is_bound()) {
+      return true;
+    } else if (features::IsScreenAIMainContentExtractionEnabled()) {
+      return std::nullopt;
+    } else {
+      return false;
+    }
+  }
+}
+
+std::optional<bool> ScreenAIServiceHandler::GetServiceStateAsync(
+    ServiceStateCallback callback) {
+  auto service_state = GetServiceState();
+
+  // If `service_state` has value, the service is already initialized or
+  // disabled.
+  if (service_state) {
+    std::move(callback).Run(*service_state);
+  } else {
+    // Put the request in queue and wait for ScreenAIServiceRouter to announce
+    // that library download state is changed.
+    pending_state_requests_.emplace_back(std::move(callback));
+  }
+
+  return service_state;
+}
+
+void ScreenAIServiceHandler::OnLibraryAvailablityChanged(bool available) {
+  if (pending_state_requests_.empty()) {
+    return;
+  }
+
+  if (available) {
+    InitializeServiceIfNeeded();
+  } else {
+    CallPendingStatusRequests(false);
+  }
+}
+
+void ScreenAIServiceHandler::ShuttingDownOnIdle() {
+  shutdown_handler_data_.shutdown_message_received = true;
+}
+
+bool ScreenAIServiceHandler::GetAndRecordSuspendedState() {
+  base::UmaHistogramBoolean(GetMetricFullName("IsSuspended"),
+                            shutdown_handler_data_.suspended);
+  return shutdown_handler_data_.suspended;
+}
+
+void ScreenAIServiceHandler::OnScreenAIServiceDisconnected() {
+  screen_ai_service_factory_.reset();
+  CallPendingStatusRequests(false);
+
+  screen_ai_service_shutdown_handler_.reset();
+  if (shutdown_handler_data_.shutdown_message_received) {
+    if (shutdown_handler_data_.crash_count) {
+      base::UmaHistogramCounts100(GetMetricFullName("CrashCountBeforeResume"),
+                                  shutdown_handler_data_.crash_count);
+    }
+    shutdown_handler_data_.crash_count = 0;
+    RecordMemoryMetrics(false);
+    return;
+  }
+
+  // Crashed!
+  shutdown_handler_data_.crash_count++;
+  shutdown_handler_data_.suspended = true;
+  base::TimeDelta suspense_time =
+      ScreenAIServiceRouter::SuggestedWaitTimeBeforeReAttempt(
+          shutdown_handler_data_.crash_count);
+  base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
+      FROM_HERE,
+      base::BindOnce(&ScreenAIServiceHandler::ResetSuspend,
+                     weak_ptr_factory_.GetWeakPtr()),
+      suspense_time);
+  VLOG(0) << "Service suspended due to crash for: " << suspense_time;
+  RecordMemoryMetrics(true);
+}
+
+void ScreenAIServiceHandler::RecordMemoryMetrics(bool crashed) {
+  if (!ocr_initialized_) {
+    return;
+  }
+  std::string prefix = GetMetricFullName("MemoryBefore.");
+  prefix += crashed ? "Crash." : "Shutdown.";
+  if (memory_stats_before_launch_.pressure_available) {
+    base::UmaHistogramEnumeration(prefix + "Pressure",
+                                  memory_stats_before_launch_.pressure_level);
+  }
+  base::UmaHistogramCounts100000(prefix + "Total",
+                                 memory_stats_before_launch_.total_memory);
+  base::UmaHistogramCounts100000(prefix + "Available",
+                                 memory_stats_before_launch_.available_memory);
+}
+
+void ScreenAIServiceHandler::CallPendingStatusRequests(bool successful) {
+  std::vector<ServiceStateCallback> requests;
+  pending_state_requests_.swap(requests);
+  for (auto& callback : requests) {
+    std::move(callback).Run(successful);
+  }
+}
+
+void ScreenAIServiceHandler::BindScreenAIAnnotator(
+    mojo::PendingReceiver<mojom::ScreenAIAnnotator> receiver) {
+  CHECK(is_ocr_);
+  InitializeServiceIfNeeded();
+
+  if (ocr_service_.is_bound()) {
+    ocr_service_->BindAnnotator(std::move(receiver));
+  }
+}
+
+void ScreenAIServiceHandler::BindMainContentExtractor(
+    mojo::PendingReceiver<mojom::Screen2xMainContentExtractor> receiver) {
+  CHECK(!is_ocr_);
+  InitializeServiceIfNeeded();
+
+  if (main_content_extraction_service_.is_bound()) {
+    main_content_extraction_service_->BindMainContentExtractor(
+        std::move(receiver));
+  }
+}
+
+void ScreenAIServiceHandler::LaunchIfNotRunning() {
+  ScreenAIInstallState::GetInstance()->SetLastUsageTime();
+  if (screen_ai_service_factory_.is_bound()) {
+    return;
+  }
+
+  auto* state_instance = ScreenAIInstallState::GetInstance();
+
+  // To have a smooth user experience, the callers of the service should ensure
+  // that the component is downloaded before promising it to the users and
+  // triggering its launch.
+  // If it is not done, the calling feature will receive no reply when it tries
+  // to use this service. However, they can detect it by using an on-disconnect
+  // handler.
+  if (!state_instance->IsComponentAvailable()) {
+    VLOG(0) << "ScreenAI service launch triggered when component is not "
+               "available.";
+    state_instance->DownloadComponent();
+    return;
+  }
+
+  if (GetAndRecordSuspendedState()) {
+    VLOG(0) << "ScreenAI service triggered while suspended.";
+    return;
+  }
+
+  // Keep memory stats for metrics after shutdown or crash.
+  memory_stats_before_launch_.total_memory =
+      base::SysInfo::AmountOfPhysicalMemoryMB();
+  memory_stats_before_launch_.available_memory = static_cast<int>(
+      base::SysInfo::AmountOfAvailablePhysicalMemory() / (1024 * 1024));
+
+  const auto* const memory_monitor = base::MemoryPressureMonitor::Get();
+  if (memory_monitor) {
+    memory_stats_before_launch_.pressure_available = true;
+    memory_stats_before_launch_.pressure_level =
+        memory_monitor->GetCurrentPressureLevel();
+  } else {
+    memory_stats_before_launch_.pressure_available = false;
+  }
+  ocr_initialized_ = false;
+
+  base::FilePath binary_path = state_instance->get_component_binary_path();
+#if BUILDFLAG(IS_WIN)
+  std::vector<base::FilePath> preload_libraries = {binary_path};
+#elif BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS)
+  std::vector<std::string> extra_switches = {
+      base::StringPrintf("--%s=%s", screen_ai::GetBinaryPathSwitch(),
+                         binary_path.MaybeAsASCII().c_str())};
+#endif  // BUILDFLAG(IS_WIN)
+
+  content::ServiceProcessHost::Launch(
+      screen_ai_service_factory_.BindNewPipeAndPassReceiver(),
+      content::ServiceProcessHost::Options()
+          .WithDisplayName(is_ocr_ ? "OCR Service"
+                                   : "Main Content Extraction Service")
+#if BUILDFLAG(IS_WIN)
+          .WithPreloadedLibraries(
+              preload_libraries,
+              content::ServiceProcessHostPreloadLibraries::GetPassKey())
+#elif BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS)
+          .WithExtraCommandLineSwitches(extra_switches)
+#endif  // BUILDFLAG(IS_WIN)
+          .Pass());
+
+  shutdown_handler_data_.shutdown_message_received = false;
+  screen_ai_service_factory_->BindShutdownHandler(
+      screen_ai_service_shutdown_handler_.BindNewPipeAndPassRemote());
+
+  screen_ai_service_factory_.set_disconnect_handler(
+      base::BindOnce(&ScreenAIServiceHandler::OnScreenAIServiceDisconnected,
+                     weak_ptr_factory_.GetWeakPtr()));
+}
+
+void ScreenAIServiceHandler::InitializeServiceIfNeeded() {
+  std::optional<bool> service_state = GetServiceState();
+  if (service_state) {
+    // Either service is already initialized or disabled.
+    CallPendingStatusRequests(*service_state);
+    return;
+  }
+
+  base::TimeTicks request_start_time = base::TimeTicks::Now();
+  LaunchIfNotRunning();
+
+  if (!screen_ai_service_factory_.is_bound()) {
+    SetLibraryLoadState(request_start_time, false);
+    return;
+  }
+
+  if (is_ocr_) {
+    base::ThreadPool::PostTaskAndReplyWithResult(
+        FROM_HERE,
+        {base::MayBlock(), base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN},
+        base::BindOnce(&ComponentFiles::Load, kOcrFilesList),
+        base::BindOnce(&ScreenAIServiceHandler::InitializeOCR,
+                       weak_ptr_factory_.GetWeakPtr(), request_start_time,
+                       ocr_service_.BindNewPipeAndPassReceiver()));
+    ocr_service_.reset_on_disconnect();
+  } else {
+    base::ThreadPool::PostTaskAndReplyWithResult(
+        FROM_HERE,
+        {base::MayBlock(), base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN},
+        base::BindOnce(&ComponentFiles::Load, kMainContentExtractionFilesList),
+        base::BindOnce(
+            &ScreenAIServiceHandler::InitializeMainContentExtraction,
+            weak_ptr_factory_.GetWeakPtr(), request_start_time,
+            main_content_extraction_service_.BindNewPipeAndPassReceiver()));
+    main_content_extraction_service_.reset_on_disconnect();
+  }
+}
+
+void ScreenAIServiceHandler::InitializeOCR(
+    base::TimeTicks request_start_time,
+    mojo::PendingReceiver<mojom::OCRService> receiver,
+    std::unique_ptr<ComponentFiles> component_files) {
+  CHECK(is_ocr_);
+  if (component_files->model_files_.empty() ||
+      !screen_ai_service_factory_.is_bound()) {
+    ScreenAIServiceHandler::SetLibraryLoadState(request_start_time, false);
+    return;
+  }
+
+  CHECK(features::IsScreenAIOCREnabled());
+  screen_ai_service_factory_->InitializeOCR(
+      component_files->library_binary_path_,
+      std::move(component_files->model_files_), std::move(receiver),
+      base::BindOnce(&ScreenAIServiceHandler::SetLibraryLoadState,
+                     weak_ptr_factory_.GetWeakPtr(), request_start_time));
+  ocr_initialized_ = true;
+}
+
+void ScreenAIServiceHandler::InitializeMainContentExtraction(
+    base::TimeTicks request_start_time,
+    mojo::PendingReceiver<mojom::MainContentExtractionService> receiver,
+    std::unique_ptr<ComponentFiles> component_files) {
+  CHECK(!is_ocr_);
+  if (component_files->model_files_.empty() ||
+      !screen_ai_service_factory_.is_bound()) {
+    ScreenAIServiceHandler::SetLibraryLoadState(request_start_time, false);
+    return;
+  }
+
+  CHECK(features::IsScreenAIMainContentExtractionEnabled());
+  screen_ai_service_factory_->InitializeMainContentExtraction(
+      component_files->library_binary_path_,
+      std::move(component_files->model_files_), std::move(receiver),
+      base::BindOnce(&ScreenAIServiceHandler::SetLibraryLoadState,
+                     weak_ptr_factory_.GetWeakPtr(), request_start_time));
+}
+
+void ScreenAIServiceHandler::SetLibraryLoadState(
+    base::TimeTicks request_start_time,
+    bool successful) {
+  base::TimeDelta elapsed_time = base::TimeTicks::Now() - request_start_time;
+  base::UmaHistogramBoolean(GetMetricFullName("Initialization"), successful);
+  base::UmaHistogramTimes(successful
+                              ? GetMetricFullName("InitializationTime.Success")
+                              : GetMetricFullName("InitializationTime.Failure"),
+                          elapsed_time);
+
+  CallPendingStatusRequests(successful);
+
+  if (successful) {
+    return;
+  }
+  if (is_ocr_) {
+    ocr_service_.reset();
+  } else {
+    main_content_extraction_service_.reset();
+  }
+}
+
+bool ScreenAIServiceHandler::IsConnectionBoundForTesting() {
+  if (is_ocr_) {
+    return ocr_service_.is_bound();
+  } else {
+    return main_content_extraction_service_.is_bound();
+  }
+}
+
+bool ScreenAIServiceHandler::IsProcessRunningForTesting() {
+  return screen_ai_service_factory_.is_bound();
+}
+
+}  // namespace screen_ai
diff --git a/chrome/browser/screen_ai/screen_ai_service_handler.h b/chrome/browser/screen_ai/screen_ai_service_handler.h
new file mode 100644
index 0000000..4be347fb
--- /dev/null
+++ b/chrome/browser/screen_ai/screen_ai_service_handler.h
@@ -0,0 +1,137 @@
+// Copyright 2025 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_SCREEN_AI_SCREEN_AI_SERVICE_HANDLER_H_
+#define CHROME_BROWSER_SCREEN_AI_SCREEN_AI_SERVICE_HANDLER_H_
+
+#include <optional>
+#include <set>
+
+#include "base/memory/memory_pressure_listener.h"
+#include "base/memory/weak_ptr.h"
+#include "base/time/time.h"
+#include "mojo/public/cpp/bindings/pending_receiver.h"
+#include "mojo/public/cpp/bindings/receiver.h"
+#include "mojo/public/cpp/bindings/remote.h"
+#include "services/screen_ai/public/mojom/screen_ai_factory.mojom.h"
+#include "services/screen_ai/public/mojom/screen_ai_service.mojom.h"
+
+namespace screen_ai {
+
+class ComponentFiles;
+
+using ServiceStateCallback = base::OnceCallback<void(bool)>;
+
+// TODO(crbug.com/408174918): Rename this class to `ScreenAIServiceHandlerBase`,
+// make all functions specific to OCR or MCE virtual, and create two separate
+// classes for OCR and MCE.
+class ScreenAIServiceHandler
+    : screen_ai::mojom::ScreenAIServiceShutdownHandler {
+ public:
+  explicit ScreenAIServiceHandler(bool is_ocr);
+  ScreenAIServiceHandler(const ScreenAIServiceHandler&) = delete;
+  ScreenAIServiceHandler& operator=(const ScreenAIServiceHandler&) = delete;
+  ~ScreenAIServiceHandler() override;
+
+  void BindScreenAIAnnotator(
+      mojo::PendingReceiver<mojom::ScreenAIAnnotator> receiver);
+
+  void BindMainContentExtractor(
+      mojo::PendingReceiver<mojom::Screen2xMainContentExtractor> receiver);
+
+  // Schedules library download and initializes the service if needed, and
+  // calls `callback` with initialization result when service is ready or
+  // failed to initialize.
+  // Returns the already known state of the service (see `GetServiceState`).
+  std::optional<bool> GetServiceStateAsync(ServiceStateCallback callback);
+
+  // Called when library availability state is changed.
+  void OnLibraryAvailablityChanged(bool available);
+
+  // screen_ai::mojom::ScreenAIServiceShutdownHandler::
+  void ShuttingDownOnIdle() override;
+
+  // Returns true if the connection for the service is bound.
+  bool IsConnectionBoundForTesting();
+
+  // Returns true if sandboxed process is running.
+  bool IsProcessRunningForTesting();
+
+ private:
+  friend class ScreenAIServiceRouterFactory;
+  friend class ScreenAIServiceShutdownHandlerTest;
+
+  std::string GetMetricFullName(std::string_view metric_name) const;
+
+  bool GetAndRecordSuspendedState();
+  void ResetSuspend() { shutdown_handler_data_.suspended = false; }
+
+  // Initialzies the service if it's not already done.
+  void InitializeServiceIfNeeded();
+
+  void InitializeOCR(base::TimeTicks request_start_time,
+                     mojo::PendingReceiver<mojom::OCRService> receiver,
+                     std::unique_ptr<ComponentFiles> model_files);
+
+  void InitializeMainContentExtraction(
+      base::TimeTicks request_start_time,
+      mojo::PendingReceiver<mojom::MainContentExtractionService> receiver,
+      std::unique_ptr<ComponentFiles> model_files);
+
+  // Launches the service if it's not already launched.
+  void LaunchIfNotRunning();
+
+  // True if service is already initialized, false if it is disabled, and
+  // nullopt if not known.
+  std::optional<bool> GetServiceState();
+
+  // Callback from Screen AI service with library load result.
+  void SetLibraryLoadState(base::TimeTicks request_start_time, bool successful);
+
+  // Calls back all pendnding service state requests.
+  void CallPendingStatusRequests(bool successful);
+
+  // Called when ScreenAI service factory is disconnected.
+  void OnScreenAIServiceDisconnected();
+
+  // Records memory metrics when service shutsdown or crashes.
+  void RecordMemoryMetrics(bool crashed);
+
+  // Pending requests to receive service state for each service type.
+  std::vector<ServiceStateCallback> pending_state_requests_;
+
+  struct ShutdownHandlerData {
+    bool shutdown_message_received = false;
+    bool suspended = false;
+    int crash_count = 0;
+  } shutdown_handler_data_;
+
+  struct MemoryStatsBeforeLaunch {
+    int total_memory;      // in MB.
+    int available_memory;  // in MB.
+    bool pressure_available;
+    base::MemoryPressureListener::MemoryPressureLevel pressure_level;
+  } memory_stats_before_launch_;
+
+  bool ocr_initialized_ = false;
+
+  mojo::Receiver<screen_ai::mojom::ScreenAIServiceShutdownHandler>
+      screen_ai_service_shutdown_handler_;
+
+  mojo::Remote<mojom::ScreenAIServiceFactory> screen_ai_service_factory_;
+
+  // Bassed on the value of `is_ocr_`, only one of the following two connections
+  // will be connected.
+  // TODO(crbug.com/408174918): Split this class into two.
+  mojo::Remote<mojom::OCRService> ocr_service_;
+  mojo::Remote<mojom::MainContentExtractionService>
+      main_content_extraction_service_;
+
+  const bool is_ocr_;
+  base::WeakPtrFactory<ScreenAIServiceHandler> weak_ptr_factory_{this};
+};
+
+}  // namespace screen_ai
+
+#endif  // CHROME_BROWSER_SCREEN_AI_SCREEN_AI_SERVICE_HANDLER_H_
diff --git a/chrome/browser/screen_ai/screen_ai_service_router.cc b/chrome/browser/screen_ai/screen_ai_service_router.cc
index 6c3e292..88125b6 100644
--- a/chrome/browser/screen_ai/screen_ai_service_router.cc
+++ b/chrome/browser/screen_ai/screen_ai_service_router.cc
@@ -47,104 +47,6 @@
   kMaxValue = kUnavailableWithoutNetwork,
 };
 
-bool IsModelFileContentReadable(base::File& file) {
-  if (!file.IsValid()) {
-    return false;
-  }
-  int file_size = file.GetLength();
-  if (!file_size) {
-    return false;
-  }
-  std::vector<uint8_t> buffer(file_size);
-  return file.ReadAndCheck(0, base::span(buffer));
-}
-
-// The name of the file that contains the list of files that are downloaded with
-// the component and are required to initialize the library.
-const base::FilePath::CharType kMainContentExtractionFilesList[] =
-    FILE_PATH_LITERAL("files_list_main_content_extraction.txt");
-const base::FilePath::CharType kOcrFilesList[] =
-    FILE_PATH_LITERAL("files_list_ocr.txt");
-
-class ComponentFiles {
- public:
-  explicit ComponentFiles(const base::FilePath& library_binary_path,
-                          const base::FilePath::CharType* files_list_file_name);
-  ComponentFiles(const ComponentFiles&) = delete;
-  ComponentFiles& operator=(const ComponentFiles&) = delete;
-  ~ComponentFiles();
-
-  static std::unique_ptr<ComponentFiles> Load(
-      const base::FilePath::CharType* files_list_file_name);
-
-  base::flat_map<base::FilePath, base::File> model_files_;
-  base::FilePath library_binary_path_;
-};
-
-ComponentFiles::ComponentFiles(
-    const base::FilePath& library_binary_path,
-    const base::FilePath::CharType* files_list_file_name)
-    : library_binary_path_(library_binary_path) {
-  base::FilePath component_folder = library_binary_path.DirName();
-
-  // Get the files list.
-  std::string file_content;
-  if (!base::ReadFileToString(component_folder.Append(files_list_file_name),
-                              &file_content)) {
-    VLOG(0) << "Could not read list of files for " << files_list_file_name;
-    return;
-  }
-  std::vector<std::string> files_list = base::SplitString(
-      file_content, "\n", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
-  if (files_list.empty()) {
-    VLOG(0) << "Could not parse files list for " << files_list_file_name;
-    return;
-  }
-
-  for (auto& relative_file_path : files_list) {
-    // Ignore comment lines.
-    if (relative_file_path.empty() || relative_file_path[0] == '#') {
-      continue;
-    }
-
-#if BUILDFLAG(IS_WIN)
-    base::FilePath relative_path(base::UTF8ToWide(relative_file_path));
-#else
-    base::FilePath relative_path(relative_file_path);
-#endif
-    const base::FilePath full_path = component_folder.Append(relative_path);
-    model_files_[relative_path] =
-        base::File(full_path, base::File::FLAG_OPEN | base::File::FLAG_READ);
-    if (!IsModelFileContentReadable(model_files_[relative_path])) {
-      VLOG(0) << "Could not open " << full_path;
-      model_files_.clear();
-      return;
-    }
-  }
-}
-
-ComponentFiles::~ComponentFiles() {
-  if (model_files_.empty()) {
-    return;
-  }
-
-  // Transfer ownership of the file handles to a thread that may block, and let
-  // them get destroyed there.
-  base::ThreadPool::PostTask(
-      FROM_HERE,
-      {base::MayBlock(), base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN},
-      base::BindOnce(
-          [](base::flat_map<base::FilePath, base::File> model_files) {},
-          std::move(model_files_)));
-}
-
-std::unique_ptr<ComponentFiles> ComponentFiles::Load(
-    const base::FilePath::CharType* files_list_file_name) {
-  return std::make_unique<ComponentFiles>(
-      screen_ai::ScreenAIInstallState::GetInstance()
-          ->get_component_binary_path(),
-      files_list_file_name);
-}
 
 void RecordComponentAvailability(bool available) {
   bool network = !content::GetNetworkConnectionTracker()->IsOffline();
@@ -160,8 +62,7 @@
 
 namespace screen_ai {
 
-ScreenAIServiceRouter::ScreenAIServiceRouter()
-    : screen_ai_service_shutdown_handler_(this) {}
+ScreenAIServiceRouter::ScreenAIServiceRouter() = default;
 
 ScreenAIServiceRouter::~ScreenAIServiceRouter() = default;
 
@@ -173,45 +74,36 @@
 }
 // LINT.ThenChange(//chrome/browser/ash/app_list/search/local_image_search/image_annotation_worker.cc:SuggestedWaitTimeBeforeReAttempt)
 
-std::optional<bool> ScreenAIServiceRouter::GetServiceState(Service service) {
-  if (GetAndRecordSuspendedState()) {
-    return false;
-  }
-
+ScreenAIServiceHandler* ScreenAIServiceRouter::GetHandler(Service service) {
   switch (service) {
-    case Service::kOCR:
-      if (ocr_service_.is_bound()) {
-        return true;
-      } else if (features::IsScreenAIOCREnabled()) {
-        return std::nullopt;
-      } else {
-        return false;
-      }
-
     case Service::kMainContentExtraction:
-      if (main_content_extraction_service_.is_bound()) {
-        return true;
-      } else if (features::IsScreenAIMainContentExtractionEnabled()) {
-        return std::nullopt;
-      } else {
-        return false;
+      if (!mce_handler_) {
+        mce_handler_ = std::make_unique<ScreenAIServiceHandler>(false);
       }
+      return mce_handler_.get();
+
+    case Service::kOCR:
+      if (!ocr_handler_) {
+        ocr_handler_ = std::make_unique<ScreenAIServiceHandler>(true);
+      }
+      return ocr_handler_.get();
   }
 }
 
 void ScreenAIServiceRouter::GetServiceStateAsync(
     Service service,
     ServiceStateCallback callback) {
-  auto service_state = GetServiceState(service);
+  std::optional<bool> service_state =
+      GetHandler(service)->GetServiceStateAsync(std::move(callback));
+
+  // If `service_state` has value, either the service is already initialized or
+  // disabled. In both cases we can can assume the component was ready.
+  // Otherwise its download should be triggered.
   if (service_state) {
-    // Either service is already initialized or disabled.
-    std::move(callback).Run(*service_state);
     RecordComponentAvailability(true);
     return;
   }
 
-  pending_state_requests_[service].emplace_back(std::move(callback));
-
   auto* install_state = ScreenAIInstallState::GetInstance();
 
   // If download has previously failed, reset it.
@@ -229,332 +121,62 @@
   }
 }
 
-std::set<ScreenAIServiceRouter::Service>
-ScreenAIServiceRouter::GetAllPendingStatusServices() {
-  std::set<Service> services;
-  for (const auto& it : pending_state_requests_) {
-    services.insert(it.first);
-  }
-  return services;
-}
-
 void ScreenAIServiceRouter::StateChanged(ScreenAIInstallState::State state) {
+  bool available = true;
   switch (state) {
     case ScreenAIInstallState::State::kNotDownloaded:
     case ScreenAIInstallState::State::kDownloading:
       return;
 
-    case ScreenAIInstallState::State::kDownloadFailed: {
-      std::set<Service> all_services = GetAllPendingStatusServices();
-      for (Service service : all_services) {
-        CallPendingStatusRequests(service, false);
+    case ScreenAIInstallState::State::kDownloadFailed:
+      available = false;
+      ABSL_FALLTHROUGH_INTENDED;
+    case ScreenAIInstallState::State::kDownloaded:
+      if (mce_handler_) {
+        mce_handler_->OnLibraryAvailablityChanged(available);
       }
-      RecordComponentAvailability(false);
-      break;
-    }
-
-    case ScreenAIInstallState::State::kDownloaded: {
-      std::set<Service> all_services = GetAllPendingStatusServices();
-      for (Service service : all_services) {
-        InitializeServiceIfNeeded(service);
+      if (ocr_handler_) {
+        ocr_handler_->OnLibraryAvailablityChanged(available);
       }
-      RecordComponentAvailability(true);
+      RecordComponentAvailability(available);
       break;
-    }
   }
 
   // No need to observe after library is downloaded or download has failed.
   component_ready_observer_.Reset();
 }
 
-void ScreenAIServiceRouter::ShuttingDownOnIdle() {
-  shutdown_handler_data_.shutdown_message_received = true;
-}
-
-bool ScreenAIServiceRouter::GetAndRecordSuspendedState() {
-  base::UmaHistogramBoolean("Accessibility.ScreenAI.Service.IsSuspended",
-                            shutdown_handler_data_.suspended);
-  return shutdown_handler_data_.suspended;
-}
-
-void ScreenAIServiceRouter::OnScreenAIServiceDisconnected() {
-  screen_ai_service_factory_.reset();
-  std::set<Service> all_services = GetAllPendingStatusServices();
-  for (Service service : all_services) {
-    CallPendingStatusRequests(service, false);
-  }
-
-  screen_ai_service_shutdown_handler_.reset();
-  if (shutdown_handler_data_.shutdown_message_received) {
-    if (shutdown_handler_data_.crash_count) {
-      base::UmaHistogramCounts100(
-          "Accessibility.ScreenAI.Service.CrashCountBeforeResume",
-          shutdown_handler_data_.crash_count);
-    }
-    shutdown_handler_data_.crash_count = 0;
-    RecordMemoryMetrics(false);
-    return;
-  }
-
-  // Crashed!
-  shutdown_handler_data_.crash_count++;
-  shutdown_handler_data_.suspended = true;
-  base::TimeDelta suspense_time =
-      SuggestedWaitTimeBeforeReAttempt(shutdown_handler_data_.crash_count);
-  base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
-      FROM_HERE,
-      base::BindOnce(&ScreenAIServiceRouter::ResetSuspend,
-                     weak_ptr_factory_.GetWeakPtr()),
-      suspense_time);
-  VLOG(0) << "Service suspended due to crash for: " << suspense_time;
-  RecordMemoryMetrics(true);
-}
-
-void ScreenAIServiceRouter::RecordMemoryMetrics(bool crashed) {
-  if (!ocr_initialized_) {
-    return;
-  }
-  std::string prefix = "Accessibility.ScreenAI.Service.MemoryBefore.";
-  prefix += crashed ? "Crash." : "Shutdown.";
-  if (memory_stats_before_launch_.pressure_available) {
-    base::UmaHistogramEnumeration(prefix + "Pressure",
-                                  memory_stats_before_launch_.pressure_level);
-  }
-  base::UmaHistogramCounts100000(prefix + "Total",
-                                 memory_stats_before_launch_.total_memory);
-  base::UmaHistogramCounts100000(prefix + "Available",
-                                 memory_stats_before_launch_.available_memory);
-}
-
-void ScreenAIServiceRouter::CallPendingStatusRequests(Service service,
-                                                      bool successful) {
-  if (!base::Contains(pending_state_requests_, service)) {
-    return;
-  }
-
-  std::vector<ServiceStateCallback> requests;
-  pending_state_requests_[service].swap(requests);
-  pending_state_requests_.erase(service);
-
-  for (auto& callback : requests) {
-    std::move(callback).Run(successful);
-  }
-}
-
 void ScreenAIServiceRouter::BindScreenAIAnnotator(
     mojo::PendingReceiver<mojom::ScreenAIAnnotator> receiver) {
-  InitializeServiceIfNeeded(Service::kOCR);
-
-  if (ocr_service_.is_bound()) {
-    ocr_service_->BindAnnotator(std::move(receiver));
-  }
+  GetHandler(Service::kOCR)->BindScreenAIAnnotator(std::move(receiver));
 }
 
 void ScreenAIServiceRouter::BindMainContentExtractor(
     mojo::PendingReceiver<mojom::Screen2xMainContentExtractor> receiver) {
-  InitializeServiceIfNeeded(Service::kMainContentExtraction);
-
-  if (main_content_extraction_service_.is_bound()) {
-    main_content_extraction_service_->BindMainContentExtractor(
-        std::move(receiver));
-  }
-}
-
-void ScreenAIServiceRouter::LaunchIfNotRunning() {
-  ScreenAIInstallState::GetInstance()->SetLastUsageTime();
-  if (screen_ai_service_factory_.is_bound()) {
-    return;
-  }
-
-  auto* state_instance = ScreenAIInstallState::GetInstance();
-
-  // To have a smooth user experience, the callers of the service should ensure
-  // that the component is downloaded before promising it to the users and
-  // triggering its launch.
-  // If it is not done, the calling feature will receive no reply when it tries
-  // to use this service. However, they can detect it by using an on-disconnect
-  // handler.
-  if (!state_instance->IsComponentAvailable()) {
-    VLOG(0) << "ScreenAI service launch triggered when component is not "
-               "available.";
-    state_instance->DownloadComponent();
-    return;
-  }
-
-  if (GetAndRecordSuspendedState()) {
-    VLOG(0) << "ScreenAI service triggered while suspended.";
-    return;
-  }
-
-  // Keep memory stats for metrics after shutdown or crash.
-  memory_stats_before_launch_.total_memory =
-      base::SysInfo::AmountOfPhysicalMemoryMB();
-  memory_stats_before_launch_.available_memory = static_cast<int>(
-      base::SysInfo::AmountOfAvailablePhysicalMemory() / (1024 * 1024));
-
-  const auto* const memory_monitor = base::MemoryPressureMonitor::Get();
-  if (memory_monitor) {
-    memory_stats_before_launch_.pressure_available = true;
-    memory_stats_before_launch_.pressure_level =
-        memory_monitor->GetCurrentPressureLevel();
-  } else {
-    memory_stats_before_launch_.pressure_available = false;
-  }
-  ocr_initialized_ = false;
-
-  base::FilePath binary_path = state_instance->get_component_binary_path();
-#if BUILDFLAG(IS_WIN)
-  std::vector<base::FilePath> preload_libraries = {binary_path};
-#elif BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS)
-  std::vector<std::string> extra_switches = {
-      base::StringPrintf("--%s=%s", screen_ai::GetBinaryPathSwitch(),
-                         binary_path.MaybeAsASCII().c_str())};
-#endif  // BUILDFLAG(IS_WIN)
-
-  content::ServiceProcessHost::Launch(
-      screen_ai_service_factory_.BindNewPipeAndPassReceiver(),
-      content::ServiceProcessHost::Options()
-          .WithDisplayName("Screen AI Service")
-#if BUILDFLAG(IS_WIN)
-          .WithPreloadedLibraries(
-              preload_libraries,
-              content::ServiceProcessHostPreloadLibraries::GetPassKey())
-#elif BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS)
-          .WithExtraCommandLineSwitches(extra_switches)
-#endif  // BUILDFLAG(IS_WIN)
-          .Pass());
-
-  shutdown_handler_data_.shutdown_message_received = false;
-  screen_ai_service_factory_->BindShutdownHandler(
-      screen_ai_service_shutdown_handler_.BindNewPipeAndPassRemote());
-
-  screen_ai_service_factory_.set_disconnect_handler(
-      base::BindOnce(&ScreenAIServiceRouter::OnScreenAIServiceDisconnected,
-                     weak_ptr_factory_.GetWeakPtr()));
-}
-
-void ScreenAIServiceRouter::InitializeServiceIfNeeded(Service service) {
-  std::optional<bool> service_state = GetServiceState(service);
-  if (service_state) {
-    // Either service is already initialized or disabled.
-    CallPendingStatusRequests(service, *service_state);
-    return;
-  }
-
-  base::TimeTicks request_start_time = base::TimeTicks::Now();
-  LaunchIfNotRunning();
-
-  if (!screen_ai_service_factory_.is_bound()) {
-    SetLibraryLoadState(service, request_start_time, false);
-    return;
-  }
-
-  switch (service) {
-    case Service::kMainContentExtraction:
-      base::ThreadPool::PostTaskAndReplyWithResult(
-          FROM_HERE,
-          {base::MayBlock(), base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN},
-          base::BindOnce(&ComponentFiles::Load,
-                         kMainContentExtractionFilesList),
-          base::BindOnce(
-              &ScreenAIServiceRouter::InitializeMainContentExtraction,
-              weak_ptr_factory_.GetWeakPtr(), request_start_time,
-              main_content_extraction_service_.BindNewPipeAndPassReceiver()));
-      main_content_extraction_service_.reset_on_disconnect();
-      break;
-
-    case Service::kOCR:
-      base::ThreadPool::PostTaskAndReplyWithResult(
-          FROM_HERE,
-          {base::MayBlock(), base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN},
-          base::BindOnce(&ComponentFiles::Load, kOcrFilesList),
-          base::BindOnce(&ScreenAIServiceRouter::InitializeOCR,
-                         weak_ptr_factory_.GetWeakPtr(), request_start_time,
-                         ocr_service_.BindNewPipeAndPassReceiver()));
-      ocr_service_.reset_on_disconnect();
-      break;
-  }
-}
-
-void ScreenAIServiceRouter::InitializeOCR(
-    base::TimeTicks request_start_time,
-    mojo::PendingReceiver<mojom::OCRService> receiver,
-    std::unique_ptr<ComponentFiles> component_files) {
-  if (component_files->model_files_.empty() ||
-      !screen_ai_service_factory_.is_bound()) {
-    ScreenAIServiceRouter::SetLibraryLoadState(Service::kOCR,
-                                               request_start_time, false);
-    return;
-  }
-
-  CHECK(features::IsScreenAIOCREnabled());
-  screen_ai_service_factory_->InitializeOCR(
-      component_files->library_binary_path_,
-      std::move(component_files->model_files_), std::move(receiver),
-      base::BindOnce(&ScreenAIServiceRouter::SetLibraryLoadState,
-                     weak_ptr_factory_.GetWeakPtr(), Service::kOCR,
-                     request_start_time));
-  ocr_initialized_ = true;
-}
-
-void ScreenAIServiceRouter::InitializeMainContentExtraction(
-    base::TimeTicks request_start_time,
-    mojo::PendingReceiver<mojom::MainContentExtractionService> receiver,
-    std::unique_ptr<ComponentFiles> component_files) {
-  if (component_files->model_files_.empty() ||
-      !screen_ai_service_factory_.is_bound()) {
-    ScreenAIServiceRouter::SetLibraryLoadState(Service::kMainContentExtraction,
-                                               request_start_time, false);
-    return;
-  }
-
-  CHECK(features::IsScreenAIMainContentExtractionEnabled());
-  screen_ai_service_factory_->InitializeMainContentExtraction(
-      component_files->library_binary_path_,
-      std::move(component_files->model_files_), std::move(receiver),
-      base::BindOnce(&ScreenAIServiceRouter::SetLibraryLoadState,
-                     weak_ptr_factory_.GetWeakPtr(),
-                     Service::kMainContentExtraction, request_start_time));
-}
-
-void ScreenAIServiceRouter::SetLibraryLoadState(
-    Service service,
-    base::TimeTicks request_start_time,
-    bool successful) {
-  base::TimeDelta elapsed_time = base::TimeTicks::Now() - request_start_time;
-  base::UmaHistogramBoolean("Accessibility.ScreenAI.Service.Initialization",
-                            successful);
-  base::UmaHistogramTimes(
-      successful ? "Accessibility.ScreenAI.Service.InitializationTime.Success"
-                 : "Accessibility.ScreenAI.Service.InitializationTime.Failure",
-      elapsed_time);
-
-  CallPendingStatusRequests(service, successful);
-
-  if (successful) {
-    return;
-  }
-  switch (service) {
-    case Service::kOCR:
-      ocr_service_.reset();
-      break;
-    case Service::kMainContentExtraction:
-      main_content_extraction_service_.reset();
-      break;
-  }
+  GetHandler(Service::kMainContentExtraction)
+      ->BindMainContentExtractor(std::move(receiver));
 }
 
 bool ScreenAIServiceRouter::IsConnectionBoundForTesting(Service service) {
   switch (service) {
     case Service::kMainContentExtraction:
-      return main_content_extraction_service_.is_bound();
+      return mce_handler_ &&
+             mce_handler_->IsConnectionBoundForTesting();  // IN-TEST
     case Service::kOCR:
-      return ocr_service_.is_bound();
+      return ocr_handler_ &&
+             ocr_handler_->IsConnectionBoundForTesting();  // IN-TEST
   }
 }
 
-bool ScreenAIServiceRouter::IsProcessRunningForTesting() {
-  return screen_ai_service_factory_.is_bound();
+bool ScreenAIServiceRouter::IsProcessRunningForTesting(Service service) {
+  switch (service) {
+    case Service::kMainContentExtraction:
+      return mce_handler_ &&
+             mce_handler_->IsProcessRunningForTesting();  // IN-TEST
+    case Service::kOCR:
+      return ocr_handler_ &&
+             ocr_handler_->IsProcessRunningForTesting();  // IN-TEST
+  }
 }
 
 }  // namespace screen_ai
diff --git a/chrome/browser/screen_ai/screen_ai_service_router.h b/chrome/browser/screen_ai/screen_ai_service_router.h
index 90dd422..0bb0734 100644
--- a/chrome/browser/screen_ai/screen_ai_service_router.h
+++ b/chrome/browser/screen_ai/screen_ai_service_router.h
@@ -6,31 +6,18 @@
 #define CHROME_BROWSER_SCREEN_AI_SCREEN_AI_SERVICE_ROUTER_H_
 
 #include <optional>
-#include <set>
 
-#include "base/memory/memory_pressure_listener.h"
-#include "base/memory/weak_ptr.h"
 #include "base/scoped_observation.h"
 #include "base/time/time.h"
 #include "chrome/browser/screen_ai/screen_ai_install_state.h"
+#include "chrome/browser/screen_ai/screen_ai_service_handler.h"
 #include "components/keyed_service/core/keyed_service.h"
 #include "mojo/public/cpp/bindings/pending_receiver.h"
-#include "mojo/public/cpp/bindings/pending_remote.h"
-#include "mojo/public/cpp/bindings/receiver.h"
-#include "mojo/public/cpp/bindings/remote.h"
-#include "services/screen_ai/public/mojom/screen_ai_factory.mojom.h"
 #include "services/screen_ai/public/mojom/screen_ai_service.mojom.h"
 
-namespace {
-class ComponentFiles;
-}
-
 namespace screen_ai {
 
-using ServiceStateCallback = base::OnceCallback<void(bool)>;
-
 class ScreenAIServiceRouter : public KeyedService,
-                              screen_ai::mojom::ScreenAIServiceShutdownHandler,
                               ScreenAIInstallState::Observer {
  public:
   enum class Service {
@@ -60,91 +47,25 @@
   // ScreenAIInstallState::Observer:
   void StateChanged(ScreenAIInstallState::State state) override;
 
-  // screen_ai::mojom::ScreenAIServiceShutdownHandler::
-  void ShuttingDownOnIdle() override;
-
   // Returns true if the connection for `service` is bound.
   bool IsConnectionBoundForTesting(Service service);
 
   // Returns true if sandboxed process is running.
-  bool IsProcessRunningForTesting();
+  bool IsProcessRunningForTesting(Service service);
 
  private:
   friend class ScreenAIServiceRouterFactory;
-  friend class ScreenAIServiceShutdownHandlerTest;
 
   ScreenAIServiceRouter();
 
-  bool GetAndRecordSuspendedState();
-  void ResetSuspend() { shutdown_handler_data_.suspended = false; }
+  ScreenAIServiceHandler* GetHandler(Service service);
 
-  // Initialzies the `service` if it's not already done.
-  void InitializeServiceIfNeeded(Service service);
-
-  void InitializeOCR(base::TimeTicks request_start_time,
-                     mojo::PendingReceiver<mojom::OCRService> receiver,
-                     std::unique_ptr<ComponentFiles> model_files);
-
-  void InitializeMainContentExtraction(
-      base::TimeTicks request_start_time,
-      mojo::PendingReceiver<mojom::MainContentExtractionService> receiver,
-      std::unique_ptr<ComponentFiles> model_files);
-
-  // Launches the service if it's not already launched.
-  void LaunchIfNotRunning();
-
-  // True if service is already initialized, false if it is disabled, and
-  // nullopt if not known.
-  std::optional<bool> GetServiceState(Service service);
-
-  // Callback from Screen AI service with library load result.
-  void SetLibraryLoadState(Service service,
-                           base::TimeTicks request_start_time,
-                           bool successful);
-
-  // Calls back all pendnding service state requests.
-  void CallPendingStatusRequests(Service service, bool successful);
-
-  // Called when ScreenAI service factory is disconnected.
-  void OnScreenAIServiceDisconnected();
-
-  // Records memory metrics when service shutsdown or crashes.
-  void RecordMemoryMetrics(bool crashed);
-
-  // Returns the list of services that have a pending status request.
-  std::set<Service> GetAllPendingStatusServices();
-
-  // Pending requests to receive service state for each service type.
-  std::map<Service, std::vector<ServiceStateCallback>> pending_state_requests_;
+  std::unique_ptr<ScreenAIServiceHandler> ocr_handler_;
+  std::unique_ptr<ScreenAIServiceHandler> mce_handler_;
 
   // Observes changes in Screen AI component download state.
   base::ScopedObservation<ScreenAIInstallState, ScreenAIInstallState::Observer>
       component_ready_observer_{this};
-
-  struct ShutdownHandlerData {
-    bool shutdown_message_received = false;
-    bool suspended = false;
-    int crash_count = 0;
-  } shutdown_handler_data_;
-
-  struct MemoryStatsBeforeLaunch {
-    int total_memory;      // in MB.
-    int available_memory;  // in MB.
-    bool pressure_available;
-    base::MemoryPressureListener::MemoryPressureLevel pressure_level;
-  } memory_stats_before_launch_;
-
-  bool ocr_initialized_ = false;
-
-  mojo::Receiver<screen_ai::mojom::ScreenAIServiceShutdownHandler>
-      screen_ai_service_shutdown_handler_;
-
-  mojo::Remote<mojom::ScreenAIServiceFactory> screen_ai_service_factory_;
-  mojo::Remote<mojom::OCRService> ocr_service_;
-  mojo::Remote<mojom::MainContentExtractionService>
-      main_content_extraction_service_;
-
-  base::WeakPtrFactory<ScreenAIServiceRouter> weak_ptr_factory_{this};
 };
 
 }  // namespace screen_ai
diff --git a/chrome/browser/screen_ai/screen_ai_service_router_unittest.cc b/chrome/browser/screen_ai/screen_ai_service_router_unittest.cc
index 0f94a80a3..99ecf51 100644
--- a/chrome/browser/screen_ai/screen_ai_service_router_unittest.cc
+++ b/chrome/browser/screen_ai/screen_ai_service_router_unittest.cc
@@ -2,40 +2,41 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#include "chrome/browser/screen_ai/screen_ai_service_router.h"
-
 #include "base/test/metrics/histogram_tester.h"
 #include "base/test/task_environment.h"
+#include "chrome/browser/screen_ai/screen_ai_service_handler.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
 namespace screen_ai {
 
-constexpr char kIsSuspendedMetric[] =
-    "Accessibility.ScreenAI.Service.IsSuspended";
+constexpr char kIsSuspendedMetric[] = "Accessibility.OCR.Service.IsSuspended";
 constexpr char kCrashCountBeforeResumeMetric[] =
-    "Accessibility.ScreenAI.Service.CrashCountBeforeResume";
+    "Accessibility.OCR.Service.CrashCountBeforeResume";
 
+// TODO(crbug.com/408174918): Rename this file as the functionality is now moved
+// `to screen_ai_service_handler.h/cc`.
 class ScreenAIServiceShutdownHandlerTest : public ::testing::Test {
  public:
-  bool IsSuspended() { return router.GetAndRecordSuspendedState(); }
-  void DisconnectService() { router.OnScreenAIServiceDisconnected(); }
-  void SendShuttingdownMessage() { router.ShuttingDownOnIdle(); }
+  ScreenAIServiceShutdownHandlerTest() : handler(true) {}
+
+  bool IsSuspended() { return handler.GetAndRecordSuspendedState(); }
+  void DisconnectService() { handler.OnScreenAIServiceDisconnected(); }
+  void SendShuttingdownMessage() { handler.ShuttingDownOnIdle(); }
   bool IsServiceAvailable() {
-    std::optional<bool> state =
-        router.GetServiceState(ScreenAIServiceRouter::Service::kOCR);
+    std::optional<bool> state = handler.GetServiceState();
     if (state.has_value()) {
       // A true value means that the service is already running which is not
       // possible in unittest.
       EXPECT_FALSE(state.value());
       return false;
     } else {
-      // En empty result means that the service is not banned, and can be used.
+      // An empty result means that the service is not banned, and can be used.
       return true;
     }
   }
 
  protected:
-  ScreenAIServiceRouter router;
+  ScreenAIServiceHandler handler;
   base::HistogramTester histogram_tester_;
   base::test::TaskEnvironment task_environment_{
       base::test::TaskEnvironment::TimeSource::MOCK_TIME};
diff --git a/chrome/browser/serial/BUILD.gn b/chrome/browser/serial/BUILD.gn
new file mode 100644
index 0000000..e122e51
--- /dev/null
+++ b/chrome/browser/serial/BUILD.gn
@@ -0,0 +1,17 @@
+# Copyright 2025 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+source_set("serial") {
+  sources = [ "serial_chooser_context.h" ]
+
+  public_deps = [
+    "//base",
+    "//components/permissions",
+    "//content/public/browser",
+    "//mojo/public/cpp/bindings",
+    "//services/device/public/mojom",
+    "//third_party/blink/public/common",
+    "//url",
+  ]
+}
diff --git a/chrome/browser/signin/chrome_signin_client_unittest.cc b/chrome/browser/signin/chrome_signin_client_unittest.cc
index a2f9b8c4..f24fd7b 100644
--- a/chrome/browser/signin/chrome_signin_client_unittest.cc
+++ b/chrome/browser/signin/chrome_signin_client_unittest.cc
@@ -64,7 +64,6 @@
 
   void TearDown() override {
     BrowserWithTestWindowTest::TearDown();
-    TestingBrowserProcess::GetGlobal()->SetLocalState(nullptr);
   }
 
   void CreateClient(Profile* profile) {
diff --git a/chrome/browser/site_protection/site_protection_metrics_observer_unittest.cc b/chrome/browser/site_protection/site_protection_metrics_observer_unittest.cc
index 77a2381..1bebad2f 100644
--- a/chrome/browser/site_protection/site_protection_metrics_observer_unittest.cc
+++ b/chrome/browser/site_protection/site_protection_metrics_observer_unittest.cc
@@ -26,6 +26,10 @@
 #include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
+#if BUILDFLAG(IS_CHROMEOS)
+#include "chrome/test/base/scoped_testing_local_state.h"
+#endif  // BUILDFLAG(IS_CHROMEOS)
+
 namespace site_protection {
 namespace {
 
@@ -83,23 +87,14 @@
     safe_browsing_factory_->SetTestDatabaseManager(
         safe_browsing_database_manager_.get());
 
-    auto* global_browser_process = TestingBrowserProcess::GetGlobal();
-    global_browser_process->SetSafeBrowsingService(
+    browser_process_->SetSafeBrowsingService(
         safe_browsing_factory_->CreateSafeBrowsingService());
-    global_browser_process->safe_browsing_service()->Initialize();
-
-#if BUILDFLAG(IS_CHROMEOS)
-    // Local state is needed to construct ProxyConfigService, which is a
-    // dependency of PingManager on ChromeOS.
-    global_browser_process->SetLocalState(profile()->GetPrefs());
-#endif
+    browser_process_->safe_browsing_service()->Initialize();
   }
 
   void TearDown() override {
-    auto* global_browser_process = TestingBrowserProcess::GetGlobal();
-    global_browser_process->SetLocalState(nullptr);
-    global_browser_process->safe_browsing_service()->ShutDown();
-    global_browser_process->SetSafeBrowsingService(nullptr);
+    browser_process_->safe_browsing_service()->ShutDown();
+    browser_process_->SetSafeBrowsingService(nullptr);
 
     ChromeRenderViewHostTestHarness::TearDown();
   }
@@ -111,11 +106,6 @@
   }
 
   void SetIncognito() {
-#if BUILDFLAG(IS_CHROMEOS)
-    auto* global_browser_process = TestingBrowserProcess::GetGlobal();
-    global_browser_process->SetLocalState(nullptr);
-#endif
-
     Profile* const otr_profile =
         profile()->GetPrimaryOTRProfile(/*create_if_needed=*/true);
     EXPECT_TRUE(otr_profile->IsIncognitoProfile());
@@ -125,10 +115,6 @@
         otr_profile, std::move(site_instance)));
 
     SetUpForNewWebContents();
-
-#if BUILDFLAG(IS_CHROMEOS)
-    global_browser_process->SetLocalState(profile()->GetPrefs());
-#endif
   }
 
   void SetUpForNewWebContents() {
@@ -218,6 +204,14 @@
 
  protected:
   raw_ptr<TestingBrowserProcess> browser_process_;
+
+#if BUILDFLAG(IS_CHROMEOS)
+  // Local state is needed to construct ProxyConfigService, which is a
+  // dependency of PingManager on ChromeOS.
+  ScopedTestingLocalState scoped_testing_local_state_{
+      TestingBrowserProcess::GetGlobal()};
+#endif  // BUILDFLAG(IS_CHROMEOS)
+
   scoped_refptr<TestSafeBrowsingDatabaseManager>
       safe_browsing_database_manager_;
   std::unique_ptr<safe_browsing::TestSafeBrowsingServiceFactory>
diff --git a/chrome/browser/sync/prefs/chrome_syncable_prefs_database.cc b/chrome/browser/sync/prefs/chrome_syncable_prefs_database.cc
index 2916069..b54303c 100644
--- a/chrome/browser/sync/prefs/chrome_syncable_prefs_database.cc
+++ b/chrome/browser/sync/prefs/chrome_syncable_prefs_database.cc
@@ -1419,7 +1419,7 @@
      {syncable_prefs_ids::kURLsToRestoreOnStartup, syncer::PREFERENCES,
       sync_preferences::PrefSensitivity::kSensitiveRequiresHistory,
       sync_preferences::MergeBehavior::kMergeableListWithRewriteOnUpdate}},
-    {prefs::kUserColorDoNotUse,
+    {prefs::kDeprecatedUserColorDoNotUse,
      {syncable_prefs_ids::kUserColor, syncer::PREFERENCES,
       sync_preferences::PrefSensitivity::kNone,
       sync_preferences::MergeBehavior::kNone}},
@@ -1676,10 +1676,10 @@
       syncer::PREFERENCES, sync_preferences::PrefSensitivity::kNone,
       sync_preferences::MergeBehavior::kNone}},
 #if BUILDFLAG(ENABLE_GLIC)
-      {glic::prefs::kGlicRolloutEligibility,
-       {syncable_prefs_ids::kGlicRolloutEligibility, syncer::PRIORITY_PREFERENCES,
-        sync_preferences::PrefSensitivity::kNone,
-        sync_preferences::MergeBehavior::kNone}},
+    {glic::prefs::kGlicRolloutEligibility,
+     {syncable_prefs_ids::kGlicRolloutEligibility, syncer::PRIORITY_PREFERENCES,
+      sync_preferences::PrefSensitivity::kNone,
+      sync_preferences::MergeBehavior::kNone}},
 #endif  // BUILDFLAG(ENABLE_GLIC)
 });
 
diff --git a/chrome/browser/sync/test/integration/single_client_themes_sync_test.cc b/chrome/browser/sync/test/integration/single_client_themes_sync_test.cc
index c343c70..29adce5 100644
--- a/chrome/browser/sync/test/integration/single_client_themes_sync_test.cc
+++ b/chrome/browser/sync/test/integration/single_client_themes_sync_test.cc
@@ -272,15 +272,15 @@
     sync_pb::EntitySpecifics specifics;
     sync_pb::PreferenceSpecifics* preference_specifics =
         specifics.mutable_preference();
-    preference_specifics->set_name(prefs::kUserColorDoNotUse);
+    preference_specifics->set_name(prefs::kDeprecatedUserColorDoNotUse);
     preference_specifics->set_value(
         preferences_helper::ConvertPrefValueToValueInSpecifics(
             base::Value(static_cast<int>(SK_ColorRED))));
 
     GetFakeServer()->InjectEntity(
         syncer::PersistentUniqueClientEntity::CreateFromSpecificsForTesting(
-            /*non_unique_name=*/prefs::kUserColorDoNotUse,
-            /*client_tag=*/prefs::kUserColorDoNotUse, specifics,
+            /*non_unique_name=*/prefs::kDeprecatedUserColorDoNotUse,
+            /*client_tag=*/prefs::kDeprecatedUserColorDoNotUse, specifics,
             /*creation_time=*/0, /*last_modified_time=*/0));
   }
 
@@ -349,15 +349,15 @@
     sync_pb::EntitySpecifics specifics;
     sync_pb::PreferenceSpecifics* preference_specifics =
         specifics.mutable_preference();
-    preference_specifics->set_name(prefs::kUserColorDoNotUse);
+    preference_specifics->set_name(prefs::kDeprecatedUserColorDoNotUse);
     preference_specifics->set_value(
         preferences_helper::ConvertPrefValueToValueInSpecifics(
             base::Value(static_cast<int>(SK_ColorBLUE))));
 
     GetFakeServer()->InjectEntity(
         syncer::PersistentUniqueClientEntity::CreateFromSpecificsForTesting(
-            /*non_unique_name=*/prefs::kUserColorDoNotUse,
-            /*client_tag=*/prefs::kUserColorDoNotUse, specifics,
+            /*non_unique_name=*/prefs::kDeprecatedUserColorDoNotUse,
+            /*client_tag=*/prefs::kDeprecatedUserColorDoNotUse, specifics,
             /*creation_time=*/0, /*last_modified_time=*/0));
   }
 
@@ -445,15 +445,15 @@
     sync_pb::EntitySpecifics specifics;
     sync_pb::PreferenceSpecifics* preference_specifics =
         specifics.mutable_preference();
-    preference_specifics->set_name(prefs::kUserColorDoNotUse);
+    preference_specifics->set_name(prefs::kDeprecatedUserColorDoNotUse);
     preference_specifics->set_value(
         preferences_helper::ConvertPrefValueToValueInSpecifics(
             base::Value(static_cast<int>(SK_ColorBLUE))));
 
     GetFakeServer()->InjectEntity(
         syncer::PersistentUniqueClientEntity::CreateFromSpecificsForTesting(
-            /*non_unique_name=*/prefs::kUserColorDoNotUse,
-            /*client_tag=*/prefs::kUserColorDoNotUse, specifics,
+            /*non_unique_name=*/prefs::kDeprecatedUserColorDoNotUse,
+            /*client_tag=*/prefs::kDeprecatedUserColorDoNotUse, specifics,
             /*creation_time=*/0, /*last_modified_time=*/0));
   }
   {
diff --git a/chrome/browser/themes/theme_service.cc b/chrome/browser/themes/theme_service.cc
index be7ae5e7..0ad8f1b 100644
--- a/chrome/browser/themes/theme_service.cc
+++ b/chrome/browser/themes/theme_service.cc
@@ -315,9 +315,8 @@
       base::BindRepeating(&ThemeService::NotifyThemeChanged,
                           base::Unretained(this)));
   pref_change_registrar_.Add(
-      GetThemePrefNameInMigration(ThemePrefInMigration::kUserColor),
-      base::BindRepeating(&ThemeService::NotifyThemeChanged,
-                          base::Unretained(this)));
+      prefs::kUserColor, base::BindRepeating(&ThemeService::NotifyThemeChanged,
+                                             base::Unretained(this)));
 }
 
 void ThemeService::Shutdown() {
@@ -584,9 +583,8 @@
   {
     base::AutoReset<bool> resetter(&should_suppress_theme_updates_, true);
     ClearThemeData(/*clear_ntp_background=*/false);
-    profile_->GetPrefs()->SetInteger(
-        GetThemePrefNameInMigration(ThemePrefInMigration::kUserColor),
-        user_color.value_or(SK_ColorTRANSPARENT));
+    profile_->GetPrefs()->SetInteger(prefs::kUserColor,
+                                     user_color.value_or(SK_ColorTRANSPARENT));
     profile_->GetPrefs()->SetString(prefs::kCurrentThemeID, kUserColorThemeID);
   }
   NotifyThemeChanged();
@@ -619,9 +617,7 @@
   {
     base::AutoReset<bool> resetter(&should_suppress_theme_updates_, true);
     ClearThemeData(/*clear_ntp_background=*/false);
-    profile_->GetPrefs()->SetInteger(
-        GetThemePrefNameInMigration(ThemePrefInMigration::kUserColor),
-        user_color);
+    profile_->GetPrefs()->SetInteger(prefs::kUserColor, user_color);
     profile_->GetPrefs()->SetString(prefs::kCurrentThemeID, kUserColorThemeID);
     profile_->GetPrefs()->SetInteger(
         GetThemePrefNameInMigration(ThemePrefInMigration::kBrowserColorVariant),
@@ -972,8 +968,7 @@
 void ThemeService::ClearThemePrefs() {
   profile_->GetPrefs()->ClearPref(prefs::kCurrentThemePackFilename);
   profile_->GetPrefs()->ClearPref(prefs::kAutogeneratedThemeColor);
-  profile_->GetPrefs()->ClearPref(
-      GetThemePrefNameInMigration(ThemePrefInMigration::kUserColor));
+  profile_->GetPrefs()->ClearPref(prefs::kUserColor);
   profile_->GetPrefs()->ClearPref(GetThemePrefNameInMigration(
       ThemePrefInMigration::kGrayscaleThemeEnabled));
   profile_->GetPrefs()->ClearPref(
diff --git a/chrome/browser/themes/theme_service_factory.cc b/chrome/browser/themes/theme_service_factory.cc
index 4419110..4c9c455 100644
--- a/chrome/browser/themes/theme_service_factory.cc
+++ b/chrome/browser/themes/theme_service_factory.cc
@@ -135,10 +135,9 @@
       prefs::kNonSyncingBrowserColorSchemeDoNotUse,
       static_cast<int>(ThemeService::BrowserColorScheme::kSystem));
   registry->RegisterIntegerPref(
-      prefs::kUserColorDoNotUse, SK_ColorTRANSPARENT,
+      prefs::kDeprecatedUserColorDoNotUse, SK_ColorTRANSPARENT,
       user_prefs::PrefRegistrySyncable::SYNCABLE_PREF);
-  registry->RegisterIntegerPref(prefs::kNonSyncingUserColorDoNotUse,
-                                SK_ColorTRANSPARENT);
+  registry->RegisterIntegerPref(prefs::kUserColor, SK_ColorTRANSPARENT);
   registry->RegisterIntegerPref(
       prefs::kBrowserColorVariantDoNotUse,
       static_cast<int>(ui::mojom::BrowserColorVariant::kSystem),
diff --git a/chrome/browser/themes/theme_syncable_service.cc b/chrome/browser/themes/theme_syncable_service.cc
index 358b5cc..0add73f 100644
--- a/chrome/browser/themes/theme_syncable_service.cc
+++ b/chrome/browser/themes/theme_syncable_service.cc
@@ -57,7 +57,7 @@
          {prefs::kBrowserColorSchemeDoNotUse,
           prefs::kNonSyncingBrowserColorSchemeDoNotUse}},
         {ThemePrefInMigration::kUserColor,
-         {prefs::kUserColorDoNotUse, prefs::kNonSyncingUserColorDoNotUse}},
+         {prefs::kDeprecatedUserColorDoNotUse, prefs::kUserColor}},
         {ThemePrefInMigration::kBrowserColorVariant,
          {prefs::kBrowserColorVariantDoNotUse,
           prefs::kNonSyncingBrowserColorVariantDoNotUse}},
diff --git a/chrome/browser/themes/theme_syncable_service_unittest.cc b/chrome/browser/themes/theme_syncable_service_unittest.cc
index 1ce12ad..f072f7f 100644
--- a/chrome/browser/themes/theme_syncable_service_unittest.cc
+++ b/chrome/browser/themes/theme_syncable_service_unittest.cc
@@ -1072,9 +1072,8 @@
             ThemeService::BrowserColorScheme::kSystem);
 
   // Verify that the new prefs are used.
-  EXPECT_EQ(
-      profile()->GetPrefs()->GetInteger(prefs::kNonSyncingUserColorDoNotUse),
-      static_cast<int>(SK_ColorRED));
+  EXPECT_EQ(profile()->GetPrefs()->GetInteger(prefs::kUserColor),
+            static_cast<int>(SK_ColorRED));
   EXPECT_EQ(profile()->GetPrefs()->GetInteger(
                 prefs::kNonSyncingBrowserColorVariantDoNotUse),
             static_cast<int>(ui::mojom::BrowserColorVariant::kTonalSpot));
@@ -1108,8 +1107,9 @@
       ProtoEnumToBrowserColorScheme(change_specifics.browser_color_scheme()));
 
   // Verify that the old prefs are updated.
-  EXPECT_EQ(profile()->GetPrefs()->GetInteger(prefs::kUserColorDoNotUse),
-            static_cast<int>(SK_ColorRED));
+  EXPECT_EQ(
+      profile()->GetPrefs()->GetInteger(prefs::kDeprecatedUserColorDoNotUse),
+      static_cast<int>(SK_ColorRED));
   EXPECT_EQ(
       profile()->GetPrefs()->GetInteger(prefs::kBrowserColorVariantDoNotUse),
       static_cast<int>(ui::mojom::BrowserColorVariant::kTonalSpot));
@@ -1988,8 +1988,8 @@
       std::make_unique<syncer::SyncChangeProcessorWrapperForTest>(
           fake_change_processor())));
 
-  ASSERT_FALSE(
-      profile()->GetPrefs()->GetUserPrefValue(prefs::kUserColorDoNotUse));
+  ASSERT_FALSE(profile()->GetPrefs()->GetUserPrefValue(
+      prefs::kDeprecatedUserColorDoNotUse));
   ASSERT_FALSE(profile()->GetPrefs()->GetUserPrefValue(
       prefs::kBrowserColorVariantDoNotUse));
   ASSERT_FALSE(profile()->GetPrefs()->GetUserPrefValue(
@@ -2001,12 +2001,13 @@
   theme_service()->SetUserColorAndBrowserColorVariant(
       SK_ColorRED, ui::mojom::BrowserColorVariant::kTonalSpot);
 
-  ASSERT_TRUE(
-      profile()->GetPrefs()->GetUserPrefValue(prefs::kUserColorDoNotUse));
+  ASSERT_TRUE(profile()->GetPrefs()->GetUserPrefValue(
+      prefs::kDeprecatedUserColorDoNotUse));
   ASSERT_TRUE(profile()->GetPrefs()->GetUserPrefValue(
       prefs::kBrowserColorVariantDoNotUse));
-  EXPECT_EQ(profile()->GetPrefs()->GetInteger(prefs::kUserColorDoNotUse),
-            static_cast<int>(SK_ColorRED));
+  EXPECT_EQ(
+      profile()->GetPrefs()->GetInteger(prefs::kDeprecatedUserColorDoNotUse),
+      static_cast<int>(SK_ColorRED));
   EXPECT_EQ(
       profile()->GetPrefs()->GetInteger(prefs::kBrowserColorVariantDoNotUse),
       static_cast<int>(ui::mojom::BrowserColorVariant::kTonalSpot));
@@ -2026,8 +2027,8 @@
       profile()->GetPrefs()->GetBoolean(prefs::kGrayscaleThemeEnabledDoNotUse));
 
   // Other prefs are cleared.
-  EXPECT_FALSE(
-      profile()->GetPrefs()->GetUserPrefValue(prefs::kUserColorDoNotUse));
+  EXPECT_FALSE(profile()->GetPrefs()->GetUserPrefValue(
+      prefs::kDeprecatedUserColorDoNotUse));
   EXPECT_FALSE(profile()->GetPrefs()->GetUserPrefValue(
       prefs::kBrowserColorVariantDoNotUse));
   EXPECT_FALSE(profile()->GetPrefs()->GetUserPrefValue(
@@ -2047,8 +2048,8 @@
   // Other prefs are left as-is.
   EXPECT_TRUE(profile()->GetPrefs()->GetUserPrefValue(
       prefs::kGrayscaleThemeEnabledDoNotUse));
-  EXPECT_FALSE(
-      profile()->GetPrefs()->GetUserPrefValue(prefs::kUserColorDoNotUse));
+  EXPECT_FALSE(profile()->GetPrefs()->GetUserPrefValue(
+      prefs::kDeprecatedUserColorDoNotUse));
   EXPECT_FALSE(profile()->GetPrefs()->GetUserPrefValue(
       prefs::kBrowserColorVariantDoNotUse));
 
@@ -2056,8 +2057,8 @@
   theme_service()->UseDefaultTheme();
 
   // All prefs are cleared.
-  EXPECT_FALSE(
-      profile()->GetPrefs()->GetUserPrefValue(prefs::kUserColorDoNotUse));
+  EXPECT_FALSE(profile()->GetPrefs()->GetUserPrefValue(
+      prefs::kDeprecatedUserColorDoNotUse));
   EXPECT_FALSE(profile()->GetPrefs()->GetUserPrefValue(
       prefs::kBrowserColorVariantDoNotUse));
   EXPECT_FALSE(profile()->GetPrefs()->GetUserPrefValue(
@@ -3603,10 +3604,9 @@
         prefs::kNonSyncingBrowserColorSchemeDoNotUse,
         static_cast<int>(ThemeService::BrowserColorScheme::kSystem));
     registry->RegisterIntegerPref(
-        prefs::kUserColorDoNotUse, SK_ColorTRANSPARENT,
+        prefs::kDeprecatedUserColorDoNotUse, SK_ColorTRANSPARENT,
         user_prefs::PrefRegistrySyncable::SYNCABLE_PREF);
-    registry->RegisterIntegerPref(prefs::kNonSyncingUserColorDoNotUse,
-                                  SK_ColorTRANSPARENT);
+    registry->RegisterIntegerPref(prefs::kUserColor, SK_ColorTRANSPARENT);
     registry->RegisterIntegerPref(
         prefs::kBrowserColorVariantDoNotUse,
         static_cast<int>(ui::mojom::BrowserColorVariant::kSystem),
@@ -3632,14 +3632,14 @@
   ASSERT_FALSE(
       pref_service_.GetBoolean(prefs::kSyncingThemePrefsMigratedToNonSyncing));
 
-  pref_service_.SetInteger(prefs::kUserColorDoNotUse, SK_ColorBLUE);
-  EXPECT_FALSE(pref_service_.HasPrefPath(prefs::kNonSyncingUserColorDoNotUse));
+  pref_service_.SetInteger(prefs::kDeprecatedUserColorDoNotUse, SK_ColorBLUE);
+  EXPECT_FALSE(pref_service_.HasPrefPath(prefs::kUserColor));
 
   base::HistogramTester histogram_tester;
   MigrateSyncingThemePrefsToNonSyncingIfNeeded(&pref_service_);
   EXPECT_TRUE(
       pref_service_.GetBoolean(prefs::kSyncingThemePrefsMigratedToNonSyncing));
-  EXPECT_EQ(pref_service_.GetInteger(prefs::kNonSyncingUserColorDoNotUse),
+  EXPECT_EQ(pref_service_.GetInteger(prefs::kUserColor),
             static_cast<int>(SK_ColorBLUE));
   histogram_tester.ExpectUniqueSample(
       kThemePrefMigrationAlreadyMigratedHistogram, false, 1);
@@ -3651,8 +3651,8 @@
   ASSERT_FALSE(
       pref_service_.GetBoolean(prefs::kSyncingThemePrefsMigratedToNonSyncing));
 
-  pref_service_.SetInteger(prefs::kUserColorDoNotUse, SK_ColorBLUE);
-  EXPECT_FALSE(pref_service_.HasPrefPath(prefs::kNonSyncingUserColorDoNotUse));
+  pref_service_.SetInteger(prefs::kDeprecatedUserColorDoNotUse, SK_ColorBLUE);
+  EXPECT_FALSE(pref_service_.HasPrefPath(prefs::kUserColor));
 
   pref_service_.SetDict(
       prefs::kNtpCustomBackgroundDictDoNotUse,
@@ -3672,7 +3672,7 @@
   MigrateSyncingThemePrefsToNonSyncingIfNeeded(&pref_service_);
   EXPECT_TRUE(
       pref_service_.GetBoolean(prefs::kSyncingThemePrefsMigratedToNonSyncing));
-  EXPECT_EQ(pref_service_.GetInteger(prefs::kNonSyncingUserColorDoNotUse),
+  EXPECT_EQ(pref_service_.GetInteger(prefs::kUserColor),
             static_cast<int>(SK_ColorBLUE));
   histogram_tester.ExpectUniqueSample(
       kThemePrefMigrationAlreadyMigratedHistogram, false, 1);
@@ -3686,11 +3686,11 @@
 TEST_F(ThemePrefsMigrationTest,
        DoNotMigrateSyncingThemePrefsToNonSyncingIfAlreadyDone) {
   pref_service_.SetBoolean(prefs::kSyncingThemePrefsMigratedToNonSyncing, true);
-  pref_service_.SetInteger(prefs::kUserColorDoNotUse, SK_ColorBLUE);
+  pref_service_.SetInteger(prefs::kDeprecatedUserColorDoNotUse, SK_ColorBLUE);
 
   base::HistogramTester histogram_tester;
   MigrateSyncingThemePrefsToNonSyncingIfNeeded(&pref_service_);
-  EXPECT_FALSE(pref_service_.HasPrefPath(prefs::kNonSyncingUserColorDoNotUse));
+  EXPECT_FALSE(pref_service_.HasPrefPath(prefs::kUserColor));
   histogram_tester.ExpectUniqueSample(
       kThemePrefMigrationAlreadyMigratedHistogram, true, 1);
   histogram_tester.ExpectTotalCount(kThemePrefMigrationMigratedPrefHistogram,
@@ -3715,8 +3715,9 @@
         prefs::kBrowserColorSchemeDoNotUse,
         base::Value(
             static_cast<int>(ThemeService::BrowserColorScheme::kLight))));
-    initial_data.push_back(CreateRemotePrefsSyncData(
-        prefs::kUserColorDoNotUse, base::Value(static_cast<int>(SK_ColorRED))));
+    initial_data.push_back(
+        CreateRemotePrefsSyncData(prefs::kDeprecatedUserColorDoNotUse,
+                                  base::Value(static_cast<int>(SK_ColorRED))));
     initial_data.push_back(CreateRemotePrefsSyncData(
         prefs::kBrowserColorVariantDoNotUse,
         base::Value(
@@ -3779,7 +3780,7 @@
       prefs()->GetBoolean(prefs::kShouldReadIncomingSyncingThemePrefs));
   EXPECT_EQ(prefs()->GetInteger(prefs::kNonSyncingBrowserColorSchemeDoNotUse),
             static_cast<int>(ThemeService::BrowserColorScheme::kLight));
-  EXPECT_EQ(prefs()->GetInteger(prefs::kNonSyncingUserColorDoNotUse),
+  EXPECT_EQ(prefs()->GetInteger(prefs::kUserColor),
             static_cast<int>(SK_ColorRED));
   EXPECT_EQ(prefs()->GetInteger(prefs::kNonSyncingBrowserColorVariantDoNotUse),
             static_cast<int>(ui::mojom::BrowserColorVariant::kTonalSpot));
@@ -3823,7 +3824,7 @@
       prefs()->GetBoolean(prefs::kShouldReadIncomingSyncingThemePrefs));
   EXPECT_EQ(prefs()->GetInteger(prefs::kNonSyncingBrowserColorSchemeDoNotUse),
             static_cast<int>(ThemeService::BrowserColorScheme::kLight));
-  EXPECT_EQ(prefs()->GetInteger(prefs::kNonSyncingUserColorDoNotUse),
+  EXPECT_EQ(prefs()->GetInteger(prefs::kUserColor),
             static_cast<int>(SK_ColorRED));
   EXPECT_EQ(prefs()->GetInteger(prefs::kNonSyncingBrowserColorVariantDoNotUse),
             static_cast<int>(ui::mojom::BrowserColorVariant::kTonalSpot));
@@ -3864,7 +3865,7 @@
       prefs()->GetBoolean(prefs::kShouldReadIncomingSyncingThemePrefs));
   EXPECT_NE(prefs()->GetInteger(prefs::kNonSyncingBrowserColorSchemeDoNotUse),
             static_cast<int>(ThemeService::BrowserColorScheme::kLight));
-  EXPECT_NE(prefs()->GetInteger(prefs::kNonSyncingUserColorDoNotUse),
+  EXPECT_NE(prefs()->GetInteger(prefs::kUserColor),
             static_cast<int>(SK_ColorRED));
   EXPECT_NE(prefs()->GetInteger(prefs::kNonSyncingBrowserColorVariantDoNotUse),
             static_cast<int>(ui::mojom::BrowserColorVariant::kTonalSpot));
@@ -3914,7 +3915,7 @@
       prefs()->GetBoolean(prefs::kShouldReadIncomingSyncingThemePrefs));
   EXPECT_NE(prefs()->GetInteger(prefs::kNonSyncingBrowserColorSchemeDoNotUse),
             static_cast<int>(ThemeService::BrowserColorScheme::kLight));
-  EXPECT_NE(prefs()->GetInteger(prefs::kNonSyncingUserColorDoNotUse),
+  EXPECT_NE(prefs()->GetInteger(prefs::kUserColor),
             static_cast<int>(SK_ColorRED));
   EXPECT_NE(prefs()->GetInteger(prefs::kNonSyncingBrowserColorVariantDoNotUse),
             static_cast<int>(ui::mojom::BrowserColorVariant::kTonalSpot));
@@ -3953,7 +3954,7 @@
       prefs()->GetBoolean(prefs::kShouldReadIncomingSyncingThemePrefs));
   EXPECT_EQ(prefs()->GetInteger(prefs::kNonSyncingBrowserColorSchemeDoNotUse),
             static_cast<int>(ThemeService::BrowserColorScheme::kLight));
-  EXPECT_EQ(prefs()->GetInteger(prefs::kNonSyncingUserColorDoNotUse),
+  EXPECT_EQ(prefs()->GetInteger(prefs::kUserColor),
             static_cast<int>(SK_ColorRED));
   EXPECT_EQ(prefs()->GetInteger(prefs::kNonSyncingBrowserColorVariantDoNotUse),
             static_cast<int>(ui::mojom::BrowserColorVariant::kTonalSpot));
@@ -4020,7 +4021,7 @@
                 : 1u);
   fake_change_processor_->changes().clear();
 
-  ASSERT_EQ(prefs()->GetInteger(prefs::kNonSyncingUserColorDoNotUse),
+  ASSERT_EQ(prefs()->GetInteger(prefs::kUserColor),
             static_cast<int>(SK_ColorBLUE));
   ASSERT_EQ(prefs()->GetInteger(prefs::kNonSyncingBrowserColorVariantDoNotUse),
             static_cast<int>(ui::mojom::BrowserColorVariant::kNeutral));
@@ -4038,7 +4039,7 @@
                    fake_change_processor_->changes(), [](const auto& e) {
                      return e.sync_data().GetSpecifics().has_theme();
                    }));
-  EXPECT_EQ(prefs()->GetInteger(prefs::kNonSyncingUserColorDoNotUse),
+  EXPECT_EQ(prefs()->GetInteger(prefs::kUserColor),
             static_cast<int>(SK_ColorRED));
   EXPECT_EQ(prefs()->GetInteger(prefs::kNonSyncingBrowserColorVariantDoNotUse),
             static_cast<int>(ui::mojom::BrowserColorVariant::kTonalSpot));
diff --git a/chrome/browser/ui/BUILD.gn b/chrome/browser/ui/BUILD.gn
index 4ddf2cc..2bd4a83 100644
--- a/chrome/browser/ui/BUILD.gn
+++ b/chrome/browser/ui/BUILD.gn
@@ -86,8 +86,6 @@
     "recently_audible_helper.cc",
     "recently_audible_helper.h",
     "screen_capture_notification_ui.h",
-    "serial/serial_chooser_controller.cc",
-    "serial/serial_chooser_controller.h",
     "session_crashed_bubble.h",
     "simple_message_box.h",
     "simple_message_box_internal.cc",
@@ -414,6 +412,8 @@
     "//chrome/browser/ui/prefs:impl",
     "//chrome/browser/ui/safety_hub",
     "//chrome/browser/ui/search_engines",
+    "//chrome/browser/ui/serial",
+    "//chrome/browser/ui/serial:impl",
     "//chrome/browser/ui/tab_contents",
     "//chrome/browser/ui/tab_contents:impl",
     "//chrome/browser/ui/webui",
@@ -636,7 +636,6 @@
     "//components/webrtc_logging/browser",
     "//components/webui/about",
     "//components/webui/chrome_urls",
-    "//components/webui/chrome_urls/mojom:mojo_bindings",
     "//components/webui/flags",
     "//components/webui/version",
     "//components/zoom",
@@ -732,6 +731,7 @@
     "//chrome/browser/picture_in_picture:impl",
     "//chrome/browser/ui/autofill:impl",
     "//chrome/browser/ui/bluetooth:impl",
+    "//chrome/browser/ui/serial:impl",
     "//chrome/browser/ui/tab_contents:impl",
     "//chrome/browser/search_engine_choice:impl",
     "//chrome/browser/profiles:profile_util_impl",
@@ -5697,6 +5697,8 @@
       "webui/tab_strip/thumbnail_tracker.cc",
       "webui/tab_strip/thumbnail_tracker.h",
     ]
+
+    deps += [ "//chrome/browser/ui/tabs/tab_strip_api:tab_strip_api" ]
   }
 
   if (enable_compose) {
diff --git a/chrome/browser/ui/android/omnibox/BUILD.gn b/chrome/browser/ui/android/omnibox/BUILD.gn
index 245e534e..623352a 100644
--- a/chrome/browser/ui/android/omnibox/BUILD.gn
+++ b/chrome/browser/ui/android/omnibox/BUILD.gn
@@ -427,6 +427,7 @@
     "java/src/org/chromium/chrome/browser/omnibox/suggestions/OmniboxSuggestionsDropdownUnitTest.java",
     "java/src/org/chromium/chrome/browser/omnibox/suggestions/PreWarmingRecycledViewPoolTest.java",
     "java/src/org/chromium/chrome/browser/omnibox/suggestions/RecyclerViewSelectionControllerUnitTest.java",
+    "java/src/org/chromium/chrome/browser/omnibox/suggestions/SelectionControllerUnitTest.java",
     "java/src/org/chromium/chrome/browser/omnibox/suggestions/SimpleSelectionControllerUnitTest.java",
     "java/src/org/chromium/chrome/browser/omnibox/suggestions/SuggestionHorizontalDividerTest.java",
     "java/src/org/chromium/chrome/browser/omnibox/suggestions/SuggestionListViewBinderUnitTest.java",
diff --git a/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/LocationBar.java b/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/LocationBar.java
index 3fe38e0..61017b34 100644
--- a/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/LocationBar.java
+++ b/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/LocationBar.java
@@ -112,6 +112,13 @@
     /** Toggle the url bar's text size to be small or normal sized. */
     default void setUrlBarUsesSmallText(boolean useSmallText) {}
 
+    /**
+     * Toggle whether the status icon should be hidden for secure origins. This should only be used
+     * in minimized/reduced presentations of the LocationBar since the status icon has affordances
+     * for page-specific permissions, privacy, etc.
+     */
+    default void setHideStatusIconForSecureOrigins(boolean hideStatusIconForSecureOrigins) {}
+
     /** Destroys the LocationBar. */
     void destroy();
 }
diff --git a/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/LocationBarCoordinator.java b/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/LocationBarCoordinator.java
index be6aa53..18af96ce 100644
--- a/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/LocationBarCoordinator.java
+++ b/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/LocationBarCoordinator.java
@@ -541,6 +541,11 @@
         mUrlCoordinator.setUseSmallText(useSmallText);
     }
 
+    @Override
+    public void setHideStatusIconForSecureOrigins(boolean hideStatusIconForSecureOrigins) {
+        mStatusCoordinator.setHideStatusIconForSecureOrigins(hideStatusIconForSecureOrigins);
+    }
+
     // AutocompleteDelegate implementation.
     @Override
     public void onUrlTextChanged() {
diff --git a/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/status/StatusCoordinator.java b/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/status/StatusCoordinator.java
index 0081e8d83..66c24bd 100644
--- a/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/status/StatusCoordinator.java
+++ b/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/status/StatusCoordinator.java
@@ -48,6 +48,7 @@
  */
 @NullMarked
 public class StatusCoordinator implements View.OnClickListener, LocationBarDataProvider.Observer {
+
     /** Interface for displaying page info popup on omnibox. */
     public interface PageInfoAction {
         /**
@@ -184,11 +185,9 @@
         mMediator.setStatusClickListener(listener != null ? listener : this);
     }
 
-    /**
-     * @param show Whether the status icon should be VISIBLE, otherwise GONE.
-     */
-    public void setStatusIconShown(boolean show) {
-        mMediator.setStatusIconShown(show);
+    /** Toggle whether the status icon should be hidden for secure origins. */
+    public void setHideStatusIconForSecureOrigins(boolean hideStatusIconForSecureOrigins) {
+        mMediator.setHideStatusIconForSecureOrigins(hideStatusIconForSecureOrigins);
     }
 
     /**
@@ -311,13 +310,6 @@
                 : mModel.get(StatusProperties.STATUS_ICON_RESOURCE).getIconResForTesting();
     }
 
-    /** Returns the icon identifier used for custom resources. */
-    public @Nullable String getSecurityIconIdentifierForTesting() {
-        return mModel.get(StatusProperties.STATUS_ICON_RESOURCE) == null
-                ? null
-                : mModel.get(StatusProperties.STATUS_ICON_RESOURCE).getIconIdentifierForTesting();
-    }
-
     /**
      * Update visibility of the verbose status based on the button type and focus state of the
      * omnibox.
@@ -413,12 +405,4 @@
             animators.add(animator);
         }
     }
-
-    /**
-     * Set whether the status view should be shown. If the view is not shown, the status view will
-     * be permanently gone until it is updated through this method during the current lifecycle.
-     */
-    public void setShowStatusView(boolean show) {
-        mMediator.setShowStatusView(show);
-    }
 }
diff --git a/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/status/StatusMediator.java b/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/status/StatusMediator.java
index 2632a9b..76765754 100644
--- a/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/status/StatusMediator.java
+++ b/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/status/StatusMediator.java
@@ -124,6 +124,7 @@
     private Drawable mDefaultStatusBackgroundIncognito;
     private Drawable mVerboseStatusBackground;
     private Drawable mVerboseStatusBackgroundIncognito;
+    private boolean mHideStatusIconForSecureOrigins;
 
     /**
      * @param model The {@link PropertyModel} for this mediator.
@@ -245,9 +246,15 @@
             updateVerboseStatusTextVisibility();
             updateLocationBarIcon(IconTransitionType.CROSSFADE);
             updateColorTheme();
+            updateVisibilityForOriginSecurity();
         }
     }
 
+    void setHideStatusIconForSecureOrigins(boolean hideStatusIconForSecureOrigins) {
+        mHideStatusIconForSecureOrigins = hideStatusIconForSecureOrigins;
+        updateVisibilityForOriginSecurity();
+    }
+
     /** Specify minimum width of the separator field. */
     void setSeparatorFieldMinWidth(int width) {
         mSeparatorMinWidth = width;
@@ -951,4 +958,10 @@
         mDefaultStatusBackgroundIncognito =
                 DrawableUtils.getIconBackground(context, /* isIncognito= */ true, size, size);
     }
+
+    private void updateVisibilityForOriginSecurity() {
+        setShowStatusView(
+                !mHideStatusIconForSecureOrigins
+                        || mPageSecurityLevel != ConnectionSecurityLevel.SECURE);
+    }
 }
diff --git a/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/status/StatusMediatorUnitTest.java b/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/status/StatusMediatorUnitTest.java
index 79ddec3..924bc361 100644
--- a/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/status/StatusMediatorUnitTest.java
+++ b/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/status/StatusMediatorUnitTest.java
@@ -835,6 +835,25 @@
         Assert.assertTrue(mModel.get(StatusProperties.SHOW_STATUS_VIEW));
     }
 
+    @Test
+    @SmallTest
+    public void hideViewForSecureOrigins() {
+        mMediator.updateVerboseStatus(ConnectionSecurityLevel.SECURE, false, false);
+        Assert.assertTrue(mModel.get(StatusProperties.SHOW_STATUS_VIEW));
+
+        mMediator.setHideStatusIconForSecureOrigins(true);
+        Assert.assertFalse(mModel.get(StatusProperties.SHOW_STATUS_VIEW));
+
+        mMediator.updateVerboseStatus(ConnectionSecurityLevel.WARNING, false, false);
+        Assert.assertTrue(mModel.get(StatusProperties.SHOW_STATUS_VIEW));
+
+        mMediator.updateVerboseStatus(ConnectionSecurityLevel.SECURE, false, false);
+        Assert.assertFalse(mModel.get(StatusProperties.SHOW_STATUS_VIEW));
+
+        mMediator.setHideStatusIconForSecureOrigins(false);
+        Assert.assertTrue(mModel.get(StatusProperties.SHOW_STATUS_VIEW));
+    }
+
     private String getIconIdentifierForTesting() {
         return mModel.get(StatusProperties.STATUS_ICON_RESOURCE).getIconIdentifierForTesting();
     }
diff --git a/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/suggestions/AutocompleteCoordinator.java b/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/suggestions/AutocompleteCoordinator.java
index 4c96be8..c456a04 100644
--- a/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/suggestions/AutocompleteCoordinator.java
+++ b/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/suggestions/AutocompleteCoordinator.java
@@ -4,6 +4,8 @@
 
 package org.chromium.chrome.browser.omnibox.suggestions;
 
+import static org.chromium.build.NullUtil.assumeNonNull;
+
 import android.content.Context;
 import android.os.Handler;
 import android.view.KeyEvent;
@@ -339,12 +341,6 @@
 
         boolean isShowingList = mDropdown != null && mDropdown.getViewGroup().isShown();
 
-        // List of keys used to navigate the suggestions list.
-        boolean isSelectionKey =
-                (keyCode == KeyEvent.KEYCODE_DPAD_UP)
-                        || (keyCode == KeyEvent.KEYCODE_DPAD_DOWN)
-                        || (keyCode == KeyEvent.KEYCODE_TAB);
-
         if (event.getKeyCode() == KeyEvent.KEYCODE_ESCAPE) {
             if (isShowingList) {
                 mMediator.stopAutocomplete(true);
@@ -353,18 +349,41 @@
             }
             return true;
         }
-        if (isShowingList && isSelectionKey) {
+
+        // Always handle <ENTER> key, even if the suggestions list is not showing.
+        // This allows users to navigate to the typed url or query.
+        // Try to dispatch to suggestions list, if one is showing, otherwise invoke navigation.
+        if (KeyNavigationUtil.isEnter(event)) {
+            if (isShowingList
+                    && assumeNonNull(mDropdown).getViewGroup().onKeyDown(keyCode, event)) {
+                return true;
+            }
+
+            if (mParent.getVisibility() == View.VISIBLE) {
+                mMediator.loadTypedOmniboxText(
+                        event.getEventTime(), /* openInNewTab= */ event.isAltPressed());
+                return true;
+            }
+
+            return false;
+        }
+
+        // Do not attempt to interpret any navigation keys when the suggestions list is not showing.
+        if (!isShowingList) {
+            return false;
+        }
+
+        // Do not attempt to interpret non-navigaton keys.
+        // There are cases where the SPACE key may gen inappropriately routed to the
+        // Suggestion, simulating press/long press of the UI element.
+        if ((keyCode == KeyEvent.KEYCODE_DPAD_UP)
+                || (keyCode == KeyEvent.KEYCODE_DPAD_DOWN)
+                || (keyCode == KeyEvent.KEYCODE_TAB)) {
             mMediator.allowPendingItemSelection();
-        }
-        if (isShowingList
-                && mDropdown != null
-                && mDropdown.getViewGroup().onKeyDown(keyCode, event)) {
+            assumeNonNull(mDropdown).getViewGroup().onKeyDown(keyCode, event);
             return true;
         }
-        if (KeyNavigationUtil.isEnter(event) && mParent.getVisibility() == View.VISIBLE) {
-            mMediator.loadTypedOmniboxText(event.getEventTime(), event.isAltPressed());
-            return true;
-        }
+
         return false;
     }
 
diff --git a/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/suggestions/SelectionController.java b/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/suggestions/SelectionController.java
index 3a4e769..0568db3 100644
--- a/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/suggestions/SelectionController.java
+++ b/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/suggestions/SelectionController.java
@@ -84,9 +84,24 @@
      * @return whether selection was applied to the new element.
      */
     public boolean advanceForward() {
+        // If parked at upper sentinel, bail.
         if (mPosition == Integer.MAX_VALUE) return false;
-        if (mPosition == Integer.MIN_VALUE) return setPosition(0);
-        return setPosition(mPosition + 1);
+
+        // If parked at lower sentinel, resume from 0.
+        int newPosition = getPosition().orElse(-1) + 1;
+        int itemCount = getItemCount();
+        while (newPosition < itemCount) {
+            if (isSelectableItem(newPosition)) {
+                return setPosition(newPosition);
+            }
+            newPosition++;
+        }
+
+        // Don't touch selection if we can't advance. Otherwise, park at sentinel.
+        if (mMode == Mode.SATURATING_WITH_SENTINEL) {
+            setPosition(Integer.MAX_VALUE);
+        }
+        return false;
     }
 
     /**
@@ -96,14 +111,33 @@
      * @return whether selection was applied to the new element.
      */
     public boolean advanceBack() {
+        // If parked at lower sentinel, bail.
         if (mPosition == Integer.MIN_VALUE) return false;
-        if (mPosition == Integer.MAX_VALUE) return setPosition(getItemCount());
-        return setPosition(mPosition - 1);
+
+        // If parked at upper sentinel, resume from getItemCount() - 1.
+        int newPosition = getPosition().orElse(getItemCount()) - 1;
+        while (newPosition >= 0) {
+            if (isSelectableItem(newPosition)) {
+                return setPosition(newPosition);
+            }
+            newPosition--;
+        }
+
+        // Don't touch selection if we can't advance. Otherwise, park at sentinel.
+        if (mMode == Mode.SATURATING_WITH_SENTINEL) {
+            setPosition(Integer.MIN_VALUE);
+        }
+        return false;
+    }
+
+    /** Returns whether specific position is a sentinel. */
+    private static boolean isSentinel(int position) {
+        return position == Integer.MIN_VALUE || position == Integer.MAX_VALUE;
     }
 
     /** Returns true if selection controller is currently parked outside the valid range. */
     public boolean isParkedAtSentinel() {
-        return mPosition == Integer.MIN_VALUE || mPosition == Integer.MAX_VALUE;
+        return isSentinel(mPosition);
     }
 
     /** Returns current counter value (unless saturated). */
@@ -120,42 +154,48 @@
      */
     @VisibleForTesting
     boolean setPosition(int newPosition) {
-        if (!isParkedAtSentinel()) {
-            setItemState(mPosition, false);
-        }
-
-        int oldPosition = mPosition;
+        // Compute new position.
         int itemCount = getItemCount();
-        mPosition = newPosition;
         switch (mMode) {
             case Mode.SATURATING:
                 if (itemCount == 0) {
-                    mPosition = Integer.MIN_VALUE;
+                    newPosition = Integer.MIN_VALUE;
                 } else {
-                    mPosition = MathUtils.clamp(mPosition, 0, itemCount - 1);
+                    newPosition = MathUtils.clamp(newPosition, 0, itemCount - 1);
                 }
                 break;
 
             case Mode.SATURATING_WITH_SENTINEL:
                 // Park outside the valid range, keeping the information which edge we hit.
-                if (mPosition < 0) { // Underflow
-                    mPosition = Integer.MIN_VALUE;
-                } else if (mPosition >= itemCount) {
-                    mPosition = Integer.MAX_VALUE;
+                if (newPosition < 0) { // Underflow
+                    newPosition = Integer.MIN_VALUE;
+                } else if (newPosition >= itemCount) {
+                    newPosition = Integer.MAX_VALUE;
                 }
                 break;
         }
 
-        if (isParkedAtSentinel()) return false;
-
-        // Select new item, fall back to old position if not possible.
-        if (!setItemState(mPosition, true)) {
-            mPosition = oldPosition;
-            setItemState(mPosition, true);
-            // We failed to select the requested entry.
+        // Do not attempt to move selection if the next item is not selectable.
+        if (!isSentinel(newPosition) && !isSelectableItem(newPosition)) {
             return false;
         }
 
+        if (!isParkedAtSentinel()) {
+            setItemState(mPosition, false);
+        }
+
+        mPosition = newPosition;
+
+        if (!isParkedAtSentinel()) {
+            setItemState(mPosition, true);
+            return true;
+        }
+
+        return false;
+    }
+
+    /** Returns whether view at specific position is focusable. */
+    protected boolean isSelectableItem(int position) {
         return true;
     }
 
@@ -164,7 +204,6 @@
      *
      * @param position the index of an element to change the state of
      * @param state the desired new state
-     * @return the applied state of the item at specified position.
      */
-    protected abstract boolean setItemState(int position, boolean isSelected);
+    protected abstract void setItemState(int position, boolean isSelected);
 }
diff --git a/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/suggestions/SelectionControllerUnitTest.java b/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/suggestions/SelectionControllerUnitTest.java
new file mode 100644
index 0000000..6d1ff55
--- /dev/null
+++ b/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/suggestions/SelectionControllerUnitTest.java
@@ -0,0 +1,318 @@
+// Copyright 2025 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.omnibox.suggestions;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.anyBoolean;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import org.chromium.base.test.BaseRobolectricTestRunner;
+import org.chromium.chrome.browser.omnibox.suggestions.SelectionController.Mode;
+
+import java.util.OptionalInt;
+
+/** Robolectric unit tests for {@link SelectionController}. */
+@RunWith(BaseRobolectricTestRunner.class)
+public class SelectionControllerUnitTest {
+    private static final int DEFAULT_NUM_ITEMS = 3;
+
+    public @Rule MockitoRule mMockitoRule = MockitoJUnit.rule();
+
+    private SelectionController createTestController(@Mode int mode) {
+        return spy(
+                new SelectionController(mode) {
+                    @Override
+                    protected void setItemState(int position, boolean isSelected) {}
+
+                    @Override
+                    protected int getItemCount() {
+                        return DEFAULT_NUM_ITEMS;
+                    }
+                });
+    }
+
+    private void verifyPositionReset(SelectionController c, int position) {
+        verify(c).setItemState(position, false);
+        assertEquals(OptionalInt.empty(), c.getPosition());
+        assertTrue(c.isParkedAtSentinel());
+        clearInvocations(c);
+    }
+
+    private void verifyPositionSet(SelectionController c, int position) {
+        verify(c).setItemState(position, true);
+        assertEquals(OptionalInt.of(position), c.getPosition());
+        assertFalse(c.isParkedAtSentinel());
+        clearInvocations(c);
+    }
+
+    private void verifyPositionChanged(SelectionController c, int from, int to) {
+        verify(c).setItemState(from, false);
+        verifyPositionSet(c, to);
+    }
+
+    @Test
+    public void advanceForward_saturating() {
+        var c = createTestController(Mode.SATURATING);
+        c.reset();
+
+        verifyPositionSet(c, 0);
+
+        assertTrue(c.advanceForward());
+        verifyPositionChanged(c, 0, 1);
+
+        assertTrue(c.advanceForward());
+        verifyPositionChanged(c, 1, 2);
+
+        // Cannot move any further. We've reached the limit.
+        assertFalse(c.advanceForward());
+        assertEquals(OptionalInt.of(2), c.getPosition());
+
+        assertFalse(c.advanceForward());
+        assertEquals(OptionalInt.of(2), c.getPosition());
+    }
+
+    @Test
+    public void advanceForward_saturatingWithSentinel() {
+        var c = createTestController(Mode.SATURATING_WITH_SENTINEL);
+        c.reset();
+
+        assertTrue(c.isParkedAtSentinel());
+
+        assertTrue(c.advanceForward());
+        verifyPositionSet(c, 0);
+
+        assertTrue(c.advanceForward());
+        verifyPositionChanged(c, 0, 1);
+
+        assertTrue(c.advanceForward());
+        verifyPositionChanged(c, 1, 2);
+
+        assertFalse(c.advanceForward());
+        verifyPositionReset(c, 2);
+
+        assertFalse(c.advanceForward());
+        assertFalse(c.advanceForward());
+    }
+
+    @Test
+    public void advanceBack_saturating() {
+        var c = createTestController(Mode.SATURATING);
+        c.reset();
+
+        c.setPosition(DEFAULT_NUM_ITEMS);
+        verifyPositionChanged(c, 0, 2);
+
+        assertTrue(c.advanceBack());
+        verifyPositionChanged(c, 2, 1);
+
+        assertTrue(c.advanceBack());
+        verifyPositionChanged(c, 1, 0);
+
+        // Cannot move any further. We've reached the limit.
+        assertFalse(c.advanceBack());
+        assertEquals(OptionalInt.of(0), c.getPosition());
+
+        assertFalse(c.advanceBack());
+        assertEquals(OptionalInt.of(0), c.getPosition());
+    }
+
+    @Test
+    public void advanceBack_saturatingWithSentinel() {
+        var c = createTestController(Mode.SATURATING_WITH_SENTINEL);
+        c.reset();
+
+        c.setPosition(DEFAULT_NUM_ITEMS - 1);
+        verifyPositionSet(c, 2);
+
+        assertTrue(c.advanceBack());
+        verifyPositionChanged(c, 2, 1);
+
+        assertTrue(c.advanceBack());
+        verifyPositionChanged(c, 1, 0);
+
+        assertFalse(c.advanceBack());
+        verifyPositionReset(c, 0);
+
+        assertFalse(c.advanceBack());
+        assertFalse(c.advanceBack());
+    }
+
+    @Test
+    public void advanceForward_skipMiddleItems_saturating() {
+        var c = createTestController(Mode.SATURATING);
+        when(c.isSelectableItem(1)).thenReturn(false);
+        c.reset();
+
+        verifyPositionSet(c, 0);
+
+        assertTrue(c.advanceForward());
+
+        verify(c, times(1)).setItemState(0, false);
+        verify(c, times(1)).setItemState(2, true);
+        verify(c, times(2)).setItemState(anyInt(), anyBoolean());
+
+        assertEquals(OptionalInt.of(2), c.getPosition());
+    }
+
+    @Test
+    public void advanceBack_skipMiddleItems_saturating() {
+        var c = createTestController(Mode.SATURATING);
+        when(c.isSelectableItem(1)).thenReturn(false);
+        c.reset();
+
+        c.setPosition(2);
+        verifyPositionChanged(c, 0, 2);
+        assertTrue(c.advanceBack());
+
+        // This will try to move away from position 0 twice
+        // - to advance to position 1, which will fail
+        // - then, to advance to position 0, which should work.
+        verify(c, times(1)).setItemState(2, false);
+        verify(c, times(1)).setItemState(0, true);
+        verify(c, times(2)).setItemState(anyInt(), anyBoolean());
+        assertEquals(OptionalInt.of(0), c.getPosition());
+    }
+
+    @Test
+    public void advanceForward_skipTailItems_saturating() {
+        var c = createTestController(Mode.SATURATING);
+        when(c.isSelectableItem(1)).thenReturn(false);
+        when(c.isSelectableItem(2)).thenReturn(false);
+        c.reset();
+
+        verifyPositionSet(c, 0);
+
+        assertFalse(c.advanceForward());
+
+        // Selection never moved.
+        verify(c, times(0)).setItemState(anyInt(), anyBoolean());
+
+        // We shouldn't move the selection.
+        assertEquals(OptionalInt.of(0), c.getPosition());
+    }
+
+    @Test
+    public void advanceBack_skipTailItems_saturating() {
+        var c = createTestController(Mode.SATURATING);
+        when(c.isSelectableItem(1)).thenReturn(false);
+        when(c.isSelectableItem(0)).thenReturn(false);
+
+        c.setPosition(2);
+        verifyPositionSet(c, 2);
+        assertFalse(c.advanceBack());
+
+        // Selection never moved.
+        verify(c, times(0)).setItemState(anyInt(), anyBoolean());
+
+        // We shouldn't move the selection.
+        assertEquals(OptionalInt.of(2), c.getPosition());
+    }
+
+    @Test
+    public void advanceForward_skipTailItems_saturatingWithSentinel() {
+        var c = createTestController(Mode.SATURATING_WITH_SENTINEL);
+        when(c.isSelectableItem(1)).thenReturn(false);
+        when(c.isSelectableItem(2)).thenReturn(false);
+        c.reset();
+
+        // Sentinel -> position 0:
+        assertTrue(c.advanceForward());
+        verifyPositionSet(c, 0);
+
+        // Position 0 -> (skipping 1 & 2) -> Sentinel
+        assertFalse(c.advanceForward());
+        verifyPositionReset(c, 0);
+        assertEquals(OptionalInt.empty(), c.getPosition());
+    }
+
+    @Test
+    public void advanceBack_skipTailItems_saturatingWithSentinel() {
+        var c = createTestController(Mode.SATURATING_WITH_SENTINEL);
+        when(c.isSelectableItem(1)).thenReturn(false);
+        when(c.isSelectableItem(0)).thenReturn(false);
+
+        c.setPosition(2);
+        verifyPositionSet(c, 2);
+        assertFalse(c.advanceBack());
+
+        // Selection reset.
+        verifyPositionReset(c, 2);
+        assertEquals(OptionalInt.empty(), c.getPosition());
+    }
+
+    @Test
+    public void advanceForward_noSelectableItems_saturating() {
+        var c = createTestController(Mode.SATURATING);
+        when(c.isSelectableItem(0)).thenReturn(false);
+        when(c.isSelectableItem(1)).thenReturn(false);
+        when(c.isSelectableItem(2)).thenReturn(false);
+        c.reset();
+
+        assertTrue(c.isParkedAtSentinel());
+        assertFalse(c.advanceForward());
+    }
+
+    @Test
+    public void advanceBack_noSelectableItems_saturating() {
+        var c = createTestController(Mode.SATURATING);
+        when(c.isSelectableItem(0)).thenReturn(false);
+        when(c.isSelectableItem(1)).thenReturn(false);
+        when(c.isSelectableItem(2)).thenReturn(false);
+        c.reset();
+
+        assertTrue(c.isParkedAtSentinel());
+        assertFalse(c.advanceForward());
+    }
+
+    @Test
+    public void selectionControllerWithNoItems() {
+        var c = createTestController(Mode.SATURATING);
+        when(c.getItemCount()).thenReturn(0);
+        c.reset();
+
+        // Normally, saturating controller should start at valid range, but this is an edge case.
+        assertTrue(c.isParkedAtSentinel());
+        assertEquals(OptionalInt.empty(), c.getPosition());
+
+        // Simulate we now have an item. This should make the saturating controller immediately jump
+        // to the first valid item.
+        when(c.getItemCount()).thenReturn(1);
+        c.reset();
+        assertFalse(c.isParkedAtSentinel());
+        assertEquals(OptionalInt.of(0), c.getPosition());
+
+        // Simulate we lost all items. This should make the saturating controller revert to sentnel.
+        when(c.getItemCount()).thenReturn(0);
+        c.reset();
+        assertTrue(c.isParkedAtSentinel());
+        assertEquals(OptionalInt.empty(), c.getPosition());
+    }
+
+    @Test
+    public void reset_saturating() {
+        var c = createTestController(Mode.SATURATING);
+        c.reset();
+
+        verifyPositionSet(c, 0);
+
+        c.advanceForward(); // 1
+        verifyPositionChanged(c, 0, 1);
+        c.reset(); // back to default (0)
+        verifyPositionChanged(c, 1, 0);
+    }
+}
diff --git a/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/suggestions/SimpleSelectionController.java b/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/suggestions/SimpleSelectionController.java
index efd4f1d..1f3cf714 100644
--- a/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/suggestions/SimpleSelectionController.java
+++ b/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/suggestions/SimpleSelectionController.java
@@ -21,9 +21,8 @@
          *
          * @param position the position to apply selection change to
          * @param isSelected whether that position should be selected
-         * @return whether selection was applied at requested position
          */
-        boolean onSelectionChanged(int position, boolean isSelected);
+        void onSelectionChanged(int position, boolean isSelected);
     }
 
     /**
@@ -57,7 +56,7 @@
     }
 
     @Override
-    protected boolean setItemState(int position, boolean isSelected) {
-        return mListener.onSelectionChanged(position, isSelected);
+    protected void setItemState(int position, boolean isSelected) {
+        mListener.onSelectionChanged(position, isSelected);
     }
 }
diff --git a/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/suggestions/SimpleSelectionControllerUnitTest.java b/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/suggestions/SimpleSelectionControllerUnitTest.java
index d30f68a..9f16494 100644
--- a/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/suggestions/SimpleSelectionControllerUnitTest.java
+++ b/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/suggestions/SimpleSelectionControllerUnitTest.java
@@ -7,14 +7,9 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
-import static org.mockito.Mockito.anyInt;
 import static org.mockito.Mockito.clearInvocations;
-import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoMoreInteractions;
-import static org.mockito.Mockito.when;
 
-import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -35,20 +30,6 @@
     public @Rule MockitoRule mMockitoRule = MockitoJUnit.rule();
     private @Mock SimpleSelectionController.OnSelectionChangedListener mListener;
 
-    @Before
-    public void setUp() {
-        // Allow selection changes to succeed unless explicitly overridden.
-        when(mListener.onSelectionChanged(anyInt(), eq(true))).thenReturn(true);
-        when(mListener.onSelectionChanged(anyInt(), eq(false))).thenReturn(true);
-    }
-
-    private void verifyPositionReset(SimpleSelectionController c, int position) {
-        verify(mListener).onSelectionChanged(position, false);
-        assertEquals(OptionalInt.empty(), c.getPosition());
-        assertTrue(c.isParkedAtSentinel());
-        clearInvocations(mListener);
-    }
-
     private void verifyPositionSet(SelectionController c, int position) {
         verify(mListener).onSelectionChanged(position, true);
         assertEquals(OptionalInt.of(position), c.getPosition());
@@ -62,100 +43,6 @@
     }
 
     @Test
-    public void advanceForward_saturating() {
-        SimpleSelectionController c =
-                new SimpleSelectionController(mListener, MAX_POSITION, Mode.SATURATING);
-        verifyPositionSet(c, 0);
-
-        assertTrue(c.advanceForward());
-        verifyPositionChanged(c, 0, 1);
-
-        assertTrue(c.advanceForward());
-        verifyPositionChanged(c, 1, 2);
-
-        assertTrue(c.advanceForward());
-        verifyPositionChanged(c, 2, 2);
-
-        assertTrue(c.advanceForward());
-        verifyPositionChanged(c, 2, 2);
-    }
-
-    @Test
-    public void advanceForward_saturatingWithSentinel() {
-        SimpleSelectionController c =
-                new SimpleSelectionController(
-                        mListener, MAX_POSITION, Mode.SATURATING_WITH_SENTINEL);
-        assertTrue(c.isParkedAtSentinel());
-
-        assertTrue(c.advanceForward());
-        verifyPositionSet(c, 0);
-
-        assertTrue(c.advanceForward());
-        verifyPositionChanged(c, 0, 1);
-
-        assertTrue(c.advanceForward());
-        verifyPositionChanged(c, 1, 2);
-
-        assertFalse(c.advanceForward());
-        verifyPositionReset(c, 2);
-
-        assertFalse(c.advanceForward());
-        verifyNoMoreInteractions(mListener);
-    }
-
-    @Test
-    public void advanceBack_saturating() {
-        SimpleSelectionController c =
-                new SimpleSelectionController(mListener, MAX_POSITION, Mode.SATURATING);
-        c.setPosition(MAX_POSITION);
-        verifyPositionChanged(c, 0, 2);
-
-        assertTrue(c.advanceBack());
-        verifyPositionChanged(c, 2, 1);
-
-        assertTrue(c.advanceBack());
-        verifyPositionChanged(c, 1, 0);
-
-        assertTrue(c.advanceBack());
-        verifyPositionChanged(c, 0, 0);
-
-        assertTrue(c.advanceBack());
-        verifyPositionChanged(c, 0, 0);
-    }
-
-    @Test
-    public void advanceBack_saturatingWithSentinel() {
-        SimpleSelectionController c =
-                new SimpleSelectionController(
-                        mListener, MAX_POSITION, Mode.SATURATING_WITH_SENTINEL);
-        c.setPosition(MAX_POSITION - 1);
-        verifyPositionSet(c, 2);
-
-        assertTrue(c.advanceBack());
-        verifyPositionChanged(c, 2, 1);
-
-        assertTrue(c.advanceBack());
-        verifyPositionChanged(c, 1, 0);
-
-        assertFalse(c.advanceBack());
-        verifyPositionReset(c, 0);
-
-        assertFalse(c.advanceBack());
-        verifyNoMoreInteractions(mListener);
-    }
-
-    @Test
-    public void advanceForward_saturating_listenerReturnsFalse() {
-        when(mListener.onSelectionChanged(1, true)).thenReturn(false);
-
-        SimpleSelectionController c =
-                new SimpleSelectionController(mListener, MAX_POSITION, Mode.SATURATING);
-        verifyPositionSet(c, 0);
-        assertFalse(c.advanceForward());
-        verifyPositionChanged(c, 0, 0);
-    }
-
-    @Test
     public void setItemCount() {
         SimpleSelectionController c =
                 new SimpleSelectionController(mListener, MAX_POSITION, Mode.SATURATING);
@@ -178,37 +65,4 @@
         c.setItemCount(2);
         verifyPositionSet(c, 1);
     }
-
-    @Test
-    public void selectionControllerWithNoItems() {
-        SimpleSelectionController c = new SimpleSelectionController(mListener, 0, Mode.SATURATING);
-        // Normally, saturating controller should start at valid range, but this is an edge case.
-        assertTrue(c.isParkedAtSentinel());
-        assertEquals(OptionalInt.empty(), c.getPosition());
-
-        // Simulate we now have an item. This should make the saturating controller immediately jump
-        // to the first valid item.
-        c.setItemCount(1);
-        assertFalse(c.isParkedAtSentinel());
-        assertEquals(OptionalInt.of(0), c.getPosition());
-
-        // Simulate we lost all items. This should make the saturating controller revert to sentnel.
-        c.setItemCount(0);
-        assertTrue(c.isParkedAtSentinel());
-        assertEquals(OptionalInt.empty(), c.getPosition());
-    }
-
-    @Test
-    public void reset_saturating() {
-        SimpleSelectionController c =
-                new SimpleSelectionController(mListener, MAX_POSITION, Mode.SATURATING);
-        verifyPositionSet(c, 0);
-
-        c.advanceForward(); // 1
-        verifyPositionChanged(c, 0, 1);
-        c.advanceForward(); // 2
-        verifyPositionChanged(c, 1, 2);
-        c.reset(); // back to default (0)
-        verifyPositionChanged(c, 2, 0);
-    }
 }
diff --git a/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/suggestions/base/BaseSuggestionView.java b/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/suggestions/base/BaseSuggestionView.java
index 50f6167..54925aa7 100644
--- a/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/suggestions/base/BaseSuggestionView.java
+++ b/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/suggestions/base/BaseSuggestionView.java
@@ -122,9 +122,8 @@
      *
      * @param buttonIndex the index of an action button
      * @param isSelected whether to apply hairline
-     * @return the highlight state of the specified action button.
      */
-    private boolean highlightActionButton(int buttonIndex, boolean isHighlighted) {
+    private void highlightActionButton(int buttonIndex, boolean isHighlighted) {
         mActionButtons
                 .get(buttonIndex)
                 .setForeground(
@@ -132,7 +131,6 @@
                                 ? AppCompatResources.getDrawable(
                                         getContext(), R.drawable.hairline_circle)
                                 : null);
-        return isHighlighted;
     }
 
     /**
diff --git a/chrome/browser/ui/android/strings/android_chrome_strings.grd b/chrome/browser/ui/android/strings/android_chrome_strings.grd
index 71e7f31..7f15f0a 100644
--- a/chrome/browser/ui/android/strings/android_chrome_strings.grd
+++ b/chrome/browser/ui/android/strings/android_chrome_strings.grd
@@ -4777,7 +4777,7 @@
       </message>
 
       <!-- Account bookmarks strings -->
-      <message name="IDS_LOCAL_BOOKMARKS_SECTION_HEADER" desc="The section header of local top-level bookmark folders.">
+      <message name="IDS_LOCAL_BOOKMARKS_SECTION_HEADER" desc="This string appears on a page of saved bookmarks. It is the heading for a category. The categories indicate where a bookmark is saved: in this string, the bookmarks are saved locally on the user's device. This is a shortened version of the noun phrase: 'Bookmarks that are only on your device'. If your language permits it, you can omit 'Bookmarks' because it is already at the top of the page." meaning="Bookmarks that are only on your device">
         Only on this device
       </message>
       <message name="IDS_ACCOUNT_BOOKMARKS_SECTION_HEADER" desc="The section header of account top-level bookmark folders.">
diff --git a/chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_LOCAL_BOOKMARKS_SECTION_HEADER.png.sha1 b/chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_LOCAL_BOOKMARKS_SECTION_HEADER.png.sha1
index 86934ec8..16bda55 100644
--- a/chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_LOCAL_BOOKMARKS_SECTION_HEADER.png.sha1
+++ b/chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_LOCAL_BOOKMARKS_SECTION_HEADER.png.sha1
@@ -1 +1 @@
-c871fa2ddbea29b9b87cc290ea6cd8092d04706a
\ No newline at end of file
+702dccc1af99b967c6c889d7c51ca0be18c133a4
\ No newline at end of file
diff --git a/chrome/browser/ui/android/toolbar/java/src/org/chromium/chrome/browser/toolbar/MiniOriginBarController.java b/chrome/browser/ui/android/toolbar/java/src/org/chromium/chrome/browser/toolbar/MiniOriginBarController.java
index 5da568d..1c60031 100644
--- a/chrome/browser/ui/android/toolbar/java/src/org/chromium/chrome/browser/toolbar/MiniOriginBarController.java
+++ b/chrome/browser/ui/android/toolbar/java/src/org/chromium/chrome/browser/toolbar/MiniOriginBarController.java
@@ -108,6 +108,7 @@
         mShowMiniOriginBar = showMiniOriginBar;
         mLocationBar.setShowOriginOnly(mShowMiniOriginBar);
         mLocationBar.setUrlBarUsesSmallText(mShowMiniOriginBar);
+        mLocationBar.setHideStatusIconForSecureOrigins(mShowMiniOriginBar);
         mSuppressToolbarSceneLayerSupplier.set(mShowMiniOriginBar);
         mControlContainer.toggleLocationBarOnlyMode(mShowMiniOriginBar);
 
diff --git a/chrome/browser/ui/ash/quick_answers/quick_answers_controller_unittest.cc b/chrome/browser/ui/ash/quick_answers/quick_answers_controller_unittest.cc
index 351ee59..50ed5ee0 100644
--- a/chrome/browser/ui/ash/quick_answers/quick_answers_controller_unittest.cc
+++ b/chrome/browser/ui/ash/quick_answers/quick_answers_controller_unittest.cc
@@ -237,6 +237,7 @@
       {});
 
   chromeos::test::FakeMagicBoostState fake_magic_boost_state;
+  fake_magic_boost_state.SetAvailability(true);
   fake_magic_boost_state.AsyncWriteConsentStatus(
       chromeos::HMRConsentStatus::kUnset);
 
diff --git a/chrome/browser/ui/ash/read_write_cards/DEPS b/chrome/browser/ui/ash/read_write_cards/DEPS
index b704ca9..75d9b433 100644
--- a/chrome/browser/ui/ash/read_write_cards/DEPS
+++ b/chrome/browser/ui/ash/read_write_cards/DEPS
@@ -15,6 +15,7 @@
     "+chrome/browser/ash/crosapi",
     "+chrome/browser/ash/magic_boost",
     "+chrome/browser/global_features.h",
+    "+chrome/browser/profiles/profile.h",
     "+chrome/common/chrome_constants.h",
   ],
 }
diff --git a/chrome/browser/ui/ash/read_write_cards/read_write_cards_manager_impl_unittest.cc b/chrome/browser/ui/ash/read_write_cards/read_write_cards_manager_impl_unittest.cc
index afe36868..3c8a7862 100644
--- a/chrome/browser/ui/ash/read_write_cards/read_write_cards_manager_impl_unittest.cc
+++ b/chrome/browser/ui/ash/read_write_cards/read_write_cards_manager_impl_unittest.cc
@@ -14,6 +14,7 @@
 #include "chrome/browser/ash/magic_boost/magic_boost_controller_ash.h"
 #include "chrome/browser/ash/magic_boost/magic_boost_state_ash.h"
 #include "chrome/browser/global_features.h"
+#include "chrome/browser/profiles/profile.h"
 #include "chrome/browser/ui/ash/editor_menu/editor_menu_card_context.h"
 #include "chrome/browser/ui/ash/editor_menu/editor_menu_controller_impl.h"
 #include "chrome/browser/ui/ash/magic_boost/magic_boost_card_controller.h"
@@ -81,7 +82,8 @@
 
     // `ReadWriteCardsManagerImpl` will initialize `QuickAnswersState`
     // indirectly. `QuickAnswersState` depends on `MagicBoostState`.
-    magic_boost_state_ = std::make_unique<ash::MagicBoostStateAsh>();
+    magic_boost_state_ = std::make_unique<ash::MagicBoostStateAsh>(
+        base::BindRepeating([]() { return static_cast<Profile*>(nullptr); }));
     manager_ = std::make_unique<ReadWriteCardsManagerImpl>(
         TestingBrowserProcess::GetGlobal()
             ->GetFeatures()
diff --git a/chrome/browser/ui/browser.cc b/chrome/browser/ui/browser.cc
index 75c32a1..1bea3ad 100644
--- a/chrome/browser/ui/browser.cc
+++ b/chrome/browser/ui/browser.cc
@@ -1831,18 +1831,11 @@
   instant_controller_.reset();
 }
 
-void Browser::OnSplitTabCreated(
-    std::vector<std::pair<tabs::TabInterface*, int>> tabs,
-    split_tabs::SplitTabId split_id,
-    SplitTabAddReason reason,
-    split_tabs::SplitTabVisualData visual_data) {
-  UpdateBookmarkBarState(BOOKMARK_BAR_STATE_CHANGE_SPLIT_TAB_CHANGE);
-}
-void Browser::OnSplitTabRemoved(
-    std::vector<std::pair<tabs::TabInterface*, int>> tabs,
-    split_tabs::SplitTabId split_id,
-    SplitTabRemoveReason reason) {
-  UpdateBookmarkBarState(BOOKMARK_BAR_STATE_CHANGE_SPLIT_TAB_CHANGE);
+void Browser::OnSplitTabChanged(const SplitTabChange& change) {
+  if (change.type == SplitTabChange::Type::kAdded ||
+      change.type == SplitTabChange::Type::kRemoved) {
+    UpdateBookmarkBarState(BOOKMARK_BAR_STATE_CHANGE_SPLIT_TAB_CHANGE);
+  }
 }
 
 void Browser::SetTopControlsShownRatio(content::WebContents* web_contents,
diff --git a/chrome/browser/ui/browser.h b/chrome/browser/ui/browser.h
index 8cb8eb63..5d3ab97 100644
--- a/chrome/browser/ui/browser.h
+++ b/chrome/browser/ui/browser.h
@@ -754,13 +754,7 @@
                               tabs::TabInterface* tab,
                               int index) override;
   void TabStripEmpty() override;
-  void OnSplitTabCreated(std::vector<std::pair<tabs::TabInterface*, int>> tabs,
-                         split_tabs::SplitTabId split_id,
-                         SplitTabAddReason reason,
-                         split_tabs::SplitTabVisualData visual_data) override;
-  void OnSplitTabRemoved(std::vector<std::pair<tabs::TabInterface*, int>> tabs,
-                         split_tabs::SplitTabId split_id,
-                         SplitTabRemoveReason reason) override;
+  void OnSplitTabChanged(const SplitTabChange& change) override;
 
   // Overridden from content::WebContentsDelegate:
   void ActivateContents(content::WebContents* contents) override;
diff --git a/chrome/browser/ui/browser_ui_prefs.cc b/chrome/browser/ui/browser_ui_prefs.cc
index 40a441bb..73c4f25 100644
--- a/chrome/browser/ui/browser_ui_prefs.cc
+++ b/chrome/browser/ui/browser_ui_prefs.cc
@@ -201,6 +201,7 @@
                                std::string());
   registry->RegisterIntegerPref(prefs::kEnterpriseProfileBadgeToolbarSettings,
                                 0);
-  registry->RegisterBooleanPref(prefs::kNTPFooterThemeAttributionEnabled, true);
+  registry->RegisterBooleanPref(prefs::kNTPFooterExtensionAttributionEnabled,
+                                true);
 #endif  // BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_WIN)
 }
diff --git a/chrome/browser/ui/browser_window/public/browser_window_features.h b/chrome/browser/ui/browser_window/public/browser_window_features.h
index 6fa6100..62aca57 100644
--- a/chrome/browser/ui/browser_window/public/browser_window_features.h
+++ b/chrome/browser/ui/browser_window/public/browser_window_features.h
@@ -33,6 +33,7 @@
 class ToastController;
 class ToastService;
 class DownloadToolbarUIController;
+class TabStripServiceRegister;
 
 #if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC)
 class PdfInfoBarController;
@@ -78,10 +79,6 @@
 class SendTabToSelfToolbarBubbleController;
 }  // namespace send_tab_to_self
 
-namespace tabs_api::mojom {
-class TabStripService;
-}
-
 // This class owns the core controllers for features that are scoped to a given
 // browser window on desktop. It can be subclassed by tests to perform
 // dependency injection.
@@ -226,6 +223,11 @@
     return cookie_controls_bubble_coordinator_.get();
   }
 
+  // Only fetch the tab_strip_service to register a pending receiver.
+  TabStripServiceRegister* tab_strip_service() {
+    return tab_strip_service_.get();
+  }
+
  protected:
   BrowserWindowFeatures();
 
@@ -310,7 +312,7 @@
       cookie_controls_bubble_coordinator_;
 
   // This is an experimental API that interacts with the TabStripModel.
-  std::unique_ptr<tabs_api::mojom::TabStripService> tab_strip_service_;
+  std::unique_ptr<TabStripServiceRegister> tab_strip_service_;
 };
 
 #endif  // CHROME_BROWSER_UI_BROWSER_WINDOW_PUBLIC_BROWSER_WINDOW_FEATURES_H_
diff --git a/chrome/browser/ui/find_bar/find_bar.h b/chrome/browser/ui/find_bar/find_bar.h
index d41922c5..bb4fc8a 100644
--- a/chrome/browser/ui/find_bar/find_bar.h
+++ b/chrome/browser/ui/find_bar/find_bar.h
@@ -40,8 +40,9 @@
   virtual void SetFindBarController(FindBarController* find_bar_controller) = 0;
 
   // Shows the find bar. Any previous search string will again be visible.
-  // If |animate| is true, we try to slide the find bar in.
-  virtual void Show(bool animate) = 0;
+  // If `animate` is true, we try to slide the find bar in.
+  // If `focus` is true, the find bar takes focus and accepts keyboard input.
+  virtual void Show(bool animate, bool focus) = 0;
 
   // Hide the find bar.  If |animate| is true, we try to slide the find bar
   // away.
diff --git a/chrome/browser/ui/find_bar/find_bar_controller.cc b/chrome/browser/ui/find_bar/find_bar_controller.cc
index 6c4f213..7f9cc18 100644
--- a/chrome/browser/ui/find_bar/find_bar_controller.cc
+++ b/chrome/browser/ui/find_bar/find_bar_controller.cc
@@ -35,15 +35,17 @@
   find_in_page::FindTabHelper* find_tab_helper =
       find_in_page::FindTabHelper::FromWebContents(web_contents());
 
-  // Only show the animation if we're not already showing a find bar for the
-  // selected WebContents.
-  if (!find_tab_helper->find_ui_active()) {
+  const bool new_session = !find_tab_helper->find_ui_active();
+  if (new_session) {
     has_user_modified_text_ = false;
     MaybeSetPrepopulateText();
 
     find_tab_helper->set_find_ui_active(true);
-    find_bar_->Show(true);
   }
+
+  // FindBarController::Show() is triggered by users (e.g. Ctrl+F, or F3) so
+  // the find bar should always take focus.
+  find_bar_->Show(/*animate=*/true, /*focus=*/true);
   find_bar_->SetFocusAndSelection();
 
   if (find_next) {
@@ -88,6 +90,7 @@
     // When we hide the window, we need to notify the renderer that we are done
     // for now, so that we can abort the scoping effort and clear all the
     // tickmarks and highlighting.
+    find_tab_helper->set_find_ui_focused(false);
     find_tab_helper->StopFinding(selection_action);
 
     if (result_action == find_in_page::ResultAction::kClear) {
@@ -141,7 +144,8 @@
     // visible state. We also want to reset the window location so that
     // we don't surprise the user by popping up to the left for no apparent
     // reason.
-    find_bar_->Show(false);
+    find_bar_->Show(/*animate=*/false,
+                    /*focus=*/find_tab_helper->find_ui_focused());
     // The condition below can be true on macOS if the global pasteboard changed
     // while this tab was inactive (the find result will have been reset by
     // FindBarPlatformHelperMac). In that case, we need to find the new text to
diff --git a/chrome/browser/ui/lens/BUILD.gn b/chrome/browser/ui/lens/BUILD.gn
index fea398e..ce696ec 100644
--- a/chrome/browser/ui/lens/BUILD.gn
+++ b/chrome/browser/ui/lens/BUILD.gn
@@ -15,10 +15,10 @@
     "lens_overlay_entry_point_controller.h",
     "lens_overlay_gen204_controller.h",
     "lens_overlay_image_helper.h",
-    "lens_searchbox_controller.h",
     "lens_overlay_side_panel_navigation_throttle.h",
     "lens_overlay_untrusted_ui.h",
     "lens_search_controller.h",
+    "lens_searchbox_controller.h",
     "lens_side_panel_untrusted_ui.h",
   ]
   friend = [
@@ -75,7 +75,10 @@
   ]
 
   if (enable_pdf) {
-    public_deps += [ "//pdf/mojom" ]
+    public_deps += [
+      "//components/pdf/browser",
+      "//pdf/mojom",
+    ]
   }
 }
 
@@ -232,12 +235,14 @@
     "//chrome/browser/themes",
     "//chrome/browser/ui",
     "//chrome/browser/ui:browser_element_identifiers",
+    "//chrome/browser/ui:ui_features",
     "//chrome/browser/ui/browser_window",
     "//chrome/browser/ui/exclusive_access",
     "//chrome/browser/ui/find_bar",
     "//chrome/browser/ui/hats",
     "//chrome/browser/ui/hats:test_support",
     "//chrome/browser/ui/tabs:tabs_public",
+    "//chrome/browser/ui/views/page_action",
     "//chrome/browser/ui/views/side_panel",
     "//chrome/test:test_support",
     "//chrome/test:test_support_ui",
diff --git a/chrome/browser/ui/lens/lens_overlay_controller.cc b/chrome/browser/ui/lens/lens_overlay_controller.cc
index f1a7e9f..b61fbd1c 100644
--- a/chrome/browser/ui/lens/lens_overlay_controller.cc
+++ b/chrome/browser/ui/lens/lens_overlay_controller.cc
@@ -50,8 +50,8 @@
 #include "chrome/browser/ui/lens/lens_overlay_theme_utils.h"
 #include "chrome/browser/ui/lens/lens_overlay_untrusted_ui.h"
 #include "chrome/browser/ui/lens/lens_overlay_url_builder.h"
-#include "chrome/browser/ui/lens/lens_permission_bubble_controller.h"
 #include "chrome/browser/ui/lens/lens_preselection_bubble.h"
+#include "chrome/browser/ui/lens/lens_search_contextualization_controller.h"
 #include "chrome/browser/ui/lens/lens_search_controller.h"
 #include "chrome/browser/ui/lens/lens_searchbox_controller.h"
 #include "chrome/browser/ui/lens/page_content_type_conversions.h"
@@ -330,16 +330,6 @@
       theme_service_(theme_service),
       gen204_controller_(
           std::make_unique<lens::LensOverlayGen204Controller>()) {
-  tab_subscriptions_.push_back(tab_->RegisterDidActivate(base::BindRepeating(
-      &LensOverlayController::TabForegrounded, weak_factory_.GetWeakPtr())));
-  tab_subscriptions_.push_back(tab_->RegisterWillDeactivate(
-      base::BindRepeating(&LensOverlayController::TabWillEnterBackground,
-                          weak_factory_.GetWeakPtr())));
-  tab_subscriptions_.push_back(tab_->RegisterWillDiscardContents(
-      base::BindRepeating(&LensOverlayController::WillDiscardContents,
-                          weak_factory_.GetWeakPtr())));
-  tab_subscriptions_.push_back(tab_->RegisterWillDetach(base::BindRepeating(
-      &LensOverlayController::WillDetach, weak_factory_.GetWeakPtr())));
   lens_overlay_event_handler_ =
       std::make_unique<lens::LensOverlayEventHandler>(this);
 
@@ -517,13 +507,13 @@
   page_->OnCopyCommand();
 }
 
-bool LensOverlayController::IsOverlayShowing() {
+bool LensOverlayController::IsOverlayShowing() const {
   return state_ == State::kStartingWebUI || state_ == State::kOverlay ||
          state_ == State::kOverlayAndResults ||
          state_ == State::kClosingSidePanel;
 }
 
-bool LensOverlayController::IsOverlayActive() {
+bool LensOverlayController::IsOverlayActive() const {
   return IsOverlayShowing() || state_ == State::kLivePageAndResults;
 }
 
@@ -903,27 +893,6 @@
     return;
   }
 
-  // Request user permission before grabbing a screenshot.
-  CHECK(pref_service_);
-  // If contextual serachbox is enabled, show permission bubble again informing
-  // users of other information that will be shared. The contextual searchbox
-  // pref is a different pref.
-  if (!lens::CanSharePageScreenshotWithLensOverlay(pref_service_) ||
-      (lens::features::IsLensOverlayContextualSearchboxEnabled() &&
-       !lens::CanSharePageContentWithLensOverlay(pref_service_))) {
-    if (!permission_bubble_controller_) {
-      permission_bubble_controller_ =
-          std::make_unique<lens::LensPermissionBubbleController>(
-              *tab_, pref_service_, invocation_source);
-    }
-    permission_bubble_controller_->RequestPermission(
-        tab_->GetContents(),
-        base::BindRepeating(&LensOverlayController::ShowUI,
-                            weak_factory_.GetWeakPtr(), invocation_source,
-                            lens_overlay_query_controller));
-    return;
-  }
-
   // Increment the counter for the number of times the Lens Overlay has been
   // started.
   int lens_overlay_start_count =
@@ -1262,7 +1231,7 @@
 
   // If contextual searchbox is enabled, make sure the page bytes are current
   // prior to issuing the search box request.
-  GetPageContextualization(
+  GetContextualizationController()->GetPageContextualization(
       base::BindOnce(&LensOverlayController::UpdatePageContextualization,
                      weak_factory_.GetWeakPtr())
           .Then(base::BindOnce(
@@ -1573,7 +1542,7 @@
       ConvertSignificantRegionBoxes(all_bounds);
   initialization_data->last_retrieved_most_visible_page_ = pdf_current_page;
 
-  GetPageContextualization(base::BindOnce(
+  GetContextualizationController()->GetPageContextualization(base::BindOnce(
       &LensOverlayController::StorePageContentAndContinueInitialization,
       weak_factory_.GetWeakPtr(), std::move(initialization_data)));
 }
@@ -1591,67 +1560,7 @@
   RecordDocumentMetrics(page_count);
 }
 
-void LensOverlayController::GetPageContextualization(
-    PageContentRetrievedCallback callback) {
-  // If the contextual searchbox is disabled, exit early.
-  if (!lens::features::IsLensOverlayContextualSearchboxEnabled()) {
-    std::move(callback).Run(/*page_contents=*/{}, lens::MimeType::kUnknown,
-                            std::nullopt);
-    return;
-  }
-
-  is_page_context_eligible_ = true;
-  results_side_panel_coordinator_->SetShowProtectedErrorPage(false);
-
 #if BUILDFLAG(ENABLE_PDF)
-  // Try and fetch the PDF bytes if enabled.
-  pdf::PDFDocumentHelper* pdf_helper =
-      lens::features::UsePdfsAsContext()
-          ? pdf::PDFDocumentHelper::MaybeGetForWebContents(tab_->GetContents())
-          : nullptr;
-  if (pdf_helper) {
-    // Fetch the PDF bytes then initialize the overlay.
-    pdf_helper->GetPdfBytes(
-        /*size_limit=*/lens::features::GetLensOverlayFileUploadLimitBytes(),
-        base::BindOnce(&LensOverlayController::OnPdfBytesReceived,
-                       weak_factory_.GetWeakPtr(), std::move(callback)));
-    return;
-  }
-#endif  // BUILDFLAG(ENABLE_PDF)
-
-  std::vector<lens::PageContent> page_contents;
-  auto* render_frame_host = tab_->GetContents()->GetPrimaryMainFrame();
-  if (!render_frame_host || (!lens::features::UseInnerHtmlAsContext() &&
-                             !lens::features::UseInnerTextAsContext() &&
-                             !lens::features::UseApcAsContext())) {
-    std::move(callback).Run(page_contents, lens::MimeType::kUnknown,
-                            std::nullopt);
-    return;
-  }
-  // TODO(crbug.com/399610478): The fetches for innerHTML, innerText, and APC
-  // should be parallelized to fetch all data at once. Currently fetches are
-  // sequential to prevent getting stuck in a race condition.
-  MaybeGetInnerHtml(page_contents, render_frame_host, std::move(callback));
-}
-
-#if BUILDFLAG(ENABLE_PDF)
-void LensOverlayController::OnPdfBytesReceived(
-    PageContentRetrievedCallback callback,
-    pdf::mojom::PdfListener::GetPdfBytesStatus status,
-    const std::vector<uint8_t>& bytes,
-    uint32_t page_count) {
-  // TODO(b/370530197): Show user error message if status is not success.
-  if (status != pdf::mojom::PdfListener::GetPdfBytesStatus::kSuccess ||
-      page_count == 0) {
-    std::move(callback).Run(
-        {lens::PageContent(/*bytes=*/{}, lens::MimeType::kPdf)},
-        lens::MimeType::kPdf, page_count);
-    return;
-  }
-  std::move(callback).Run({lens::PageContent(bytes, lens::MimeType::kPdf)},
-                          lens::MimeType::kPdf, page_count);
-}
-
 void LensOverlayController::FetchVisiblePageIndexAndGetPartialPdfText(
     uint32_t page_count) {
   pdf::PDFDocumentHelper* pdf_helper =
@@ -1718,138 +1627,6 @@
 }
 #endif  // BUILDFLAG(ENABLE_PDF)
 
-void LensOverlayController::MaybeGetInnerHtml(
-    std::vector<lens::PageContent> page_contents,
-    content::RenderFrameHost* render_frame_host,
-    PageContentRetrievedCallback callback) {
-  if (!lens::features::UseInnerHtmlAsContext()) {
-    MaybeGetInnerText(page_contents, render_frame_host, std::move(callback));
-    return;
-  }
-  content_extraction::GetInnerHtml(
-      *render_frame_host,
-      base::BindOnce(&LensOverlayController::OnInnerHtmlReceived,
-                     weak_factory_.GetWeakPtr(), page_contents,
-                     render_frame_host, std::move(callback)));
-}
-
-void LensOverlayController::OnInnerHtmlReceived(
-    std::vector<lens::PageContent> page_contents,
-    content::RenderFrameHost* render_frame_host,
-    PageContentRetrievedCallback callback,
-    const std::optional<std::string>& result) {
-  const bool was_successful =
-      result.has_value() &&
-      result->size() <= lens::features::GetLensOverlayFileUploadLimitBytes();
-  // Add the innerHTML to the page contents if successful, or empty bytes if
-  // not.
-  page_contents.emplace_back(
-      /*bytes=*/was_successful
-          ? std::vector<uint8_t>(result->begin(), result->end())
-          : std::vector<uint8_t>{},
-      lens::MimeType::kHtml);
-  MaybeGetInnerText(page_contents, render_frame_host, std::move(callback));
-}
-
-void LensOverlayController::MaybeGetInnerText(
-    std::vector<lens::PageContent> page_contents,
-    content::RenderFrameHost* render_frame_host,
-    PageContentRetrievedCallback callback) {
-  if (!lens::features::UseInnerTextAsContext()) {
-    MaybeGetAnnotatedPageContent(page_contents, render_frame_host,
-                                 std::move(callback));
-    return;
-  }
-  content_extraction::GetInnerText(
-      *render_frame_host, /*node_id=*/std::nullopt,
-      base::BindOnce(&LensOverlayController::OnInnerTextReceived,
-                     weak_factory_.GetWeakPtr(), page_contents,
-                     render_frame_host, std::move(callback)));
-}
-
-void LensOverlayController::OnInnerTextReceived(
-    std::vector<lens::PageContent> page_contents,
-    content::RenderFrameHost* render_frame_host,
-    PageContentRetrievedCallback callback,
-    std::unique_ptr<content_extraction::InnerTextResult> result) {
-  const bool was_successful =
-      result && result->inner_text.size() <=
-                    lens::features::GetLensOverlayFileUploadLimitBytes();
-  // Add the innerText to the page_contents if successful, or empty bytes if
-  // not.
-  page_contents.emplace_back(
-      /*bytes=*/was_successful
-          ? std::vector<uint8_t>(result->inner_text.begin(),
-                                 result->inner_text.end())
-          : std::vector<uint8_t>{},
-      lens::MimeType::kPlainText);
-  MaybeGetAnnotatedPageContent(page_contents, render_frame_host,
-                               std::move(callback));
-}
-
-void LensOverlayController::MaybeGetAnnotatedPageContent(
-    std::vector<lens::PageContent> page_contents,
-    content::RenderFrameHost* render_frame_host,
-    PageContentRetrievedCallback callback) {
-  if (!lens::features::UseApcAsContext()) {
-    // Done fetching page contents.
-    // Keep legacy behavior consistent by setting the primary content type to
-    // plain text if that is the only content type enabled.
-    // TODO(crbug.com/401614601): Set primary content type to kHtml in all
-    // cases.
-    auto primary_content_type = lens::features::UseInnerTextAsContext() &&
-                                        !lens::features::UseInnerHtmlAsContext()
-                                    ? lens::MimeType::kPlainText
-                                    : lens::MimeType::kHtml;
-    std::move(callback).Run(page_contents, primary_content_type, std::nullopt);
-    return;
-  }
-
-  blink::mojom::AIPageContentOptionsPtr ai_page_content_options =
-      optimization_guide::DefaultAIPageContentOptions();
-  ai_page_content_options->on_critical_path = true;
-  ai_page_content_options->max_meta_elements = 20;
-  optimization_guide::GetAIPageContent(
-      tab_->GetContents(), std::move(ai_page_content_options),
-      base::BindOnce(&LensOverlayController::OnAnnotatedPageContentReceived,
-                     weak_factory_.GetWeakPtr(), page_contents,
-                     std::move(callback)));
-}
-
-void LensOverlayController::OnAnnotatedPageContentReceived(
-    std::vector<lens::PageContent> page_contents,
-    PageContentRetrievedCallback callback,
-    std::optional<optimization_guide::AIPageContentResult> result) {
-  // Add the apc proto the page_contents if it exists.
-  if (result) {
-    // Convert the page metadata to a C struct defined in the optimization_guide
-    // component so it can be passed to the shared library.
-    std::vector<optimization_guide::FrameMetadata> frame_metadata_structs =
-        lens::ConvertFrameMetadataFromProto(result.value());
-
-    // If the page is protected, do not send the latest page content to the
-    // server.
-    const auto& tab_url = tab_->GetContents()->GetLastCommittedURL();
-    if (!IsPageContextEligible(
-            tab_url, std::move(frame_metadata_structs),
-            lens_search_controller_->page_context_eligibility())) {
-      is_page_context_eligible_ = false;
-      results_side_panel_coordinator_->SetShowProtectedErrorPage(true);
-      // Clear all previous page contents.
-      page_contents.clear();
-    } else {
-      std::string serialized_apc;
-      result->proto.SerializeToString(&serialized_apc);
-      page_contents.emplace_back(
-          std::vector<uint8_t>(serialized_apc.begin(), serialized_apc.end()),
-          lens::MimeType::kAnnotatedPageContent);
-    }
-  }
-  // Done fetching page contents.
-  std::move(callback).Run(page_contents, lens::MimeType::kAnnotatedPageContent,
-                          std::nullopt);
-}
-
 std::vector<lens::mojom::CenterRotatedBoxPtr>
 LensOverlayController::ConvertSignificantRegionBoxes(
     const std::vector<gfx::Rect>& all_bounds) {
@@ -1901,7 +1678,7 @@
     return;
   }
 
-  GetPageContextualization(
+  GetContextualizationController()->GetPageContextualization(
       base::BindOnce(&LensOverlayController::UpdatePageContextualization,
                      weak_factory_.GetWeakPtr()));
 }
@@ -2236,7 +2013,6 @@
     permission_request_manager->RestorePrompt();
   }
 
-  permission_bubble_controller_.reset();
   results_side_panel_coordinator_ = nullptr;
   side_panel_in_use_.reset();
   pre_initialization_suggest_inputs_.reset();
@@ -2788,6 +2564,13 @@
 void LensOverlayController::TabForegrounded(tabs::TabInterface* tab) {
   // Ignore the event if the overlay is not backgrounded.
   if (state_ != State::kBackground) {
+    // TODO(crbug.com/404941800): This is a temporary DCHECK. This should be a
+    // CHECK and the if statement above should be removed once the root cause
+    // causing the CHECK(state_ == State::kBackground) to fail is found and
+    // fixed.
+    DCHECK(state_ == State::kBackground)
+        << "State should be kBackground but is instead "
+        << static_cast<int>(state_);
     return;
   }
 
@@ -2810,6 +2593,7 @@
 void LensOverlayController::TabWillEnterBackground(tabs::TabInterface* tab) {
   // If the current tab was already backgrounded, do nothing.
   if (state_ == State::kBackground) {
+    DCHECK(state_ != State::kBackground) << "State should not be kBackground.";
     return;
   }
 
@@ -2835,33 +2619,6 @@
       lens::LensOverlayDismissalSource::kTabBackgroundedWhileScreenshotting);
 }
 
-void LensOverlayController::WillDiscardContents(
-    tabs::TabInterface* tab,
-    content::WebContents* old_contents,
-    content::WebContents* new_contents) {
-  // Background tab contents discarded.
-  lens_search_controller_->CloseLensSync(
-      lens::LensOverlayDismissalSource::kTabContentsDiscarded);
-}
-
-void LensOverlayController::WillDetach(
-    tabs::TabInterface* tab,
-    tabs::TabInterface::DetachReason reason) {
-  // When dragging a tab into a new window, all window-specific state must be
-  // reset. As this flow is not fully functional, close the overlay regardless
-  // of `reason`. https://crbug.com/342921671.
-  switch (reason) {
-    case tabs::TabInterface::DetachReason::kDelete:
-      lens_search_controller_->CloseLensSync(
-          lens::LensOverlayDismissalSource::kTabClosed);
-      return;
-    case tabs::TabInterface::DetachReason::kInsertIntoOtherWindow:
-      lens_search_controller_->CloseLensSync(
-          lens::LensOverlayDismissalSource::kTabDragNewWindow);
-      return;
-  }
-}
-
 void LensOverlayController::ActivityRequestedByOverlay(
     ui::mojom::ClickModifiersPtr click_modifiers) {
   // The tab is expected to be in the foreground.
@@ -3745,3 +3502,8 @@
 LensOverlayController::GetLensSearchboxController() {
   return lens_search_controller_->lens_searchbox_controller();
 }
+
+lens::LensSearchContextualizationController*
+LensOverlayController::GetContextualizationController() {
+  return lens_search_controller_->lens_search_contextualization_controller();
+}
diff --git a/chrome/browser/ui/lens/lens_overlay_controller.h b/chrome/browser/ui/lens/lens_overlay_controller.h
index 0df117a..ff7c2bef 100644
--- a/chrome/browser/ui/lens/lens_overlay_controller.h
+++ b/chrome/browser/ui/lens/lens_overlay_controller.h
@@ -79,19 +79,16 @@
 }  // namespace content
 
 namespace lens {
+class LensOverlayEventHandler;
 class LensOverlayQueryController;
 class LensOverlaySidePanelCoordinator;
 class LensPermissionBubbleController;
 class LensSearchboxController;
-class LensOverlayEventHandler;
+class LensSearchContextualizationController;
 struct SearchQuery;
 class SidePanelInUse;
 }  // namespace lens
 
-namespace optimization_guide {
-struct AIPageContentResult;
-}  // namespace optimization_guide
-
 namespace signin {
 class IdentityManager;
 }  // namespace signin
@@ -120,19 +117,6 @@
 
 extern void* kLensOverlayPreselectionWidgetIdentifier;
 
-// Callback type alias for page content bytes retrieved. Multiple pieces and
-// types of content may be retrieved and returned in `page_contents`.
-// `primary_content_type` is the main type used in the request flow and used to
-// determine request params and whether updated requests need to be sent.
-// `pdf_page_count` is the number of pages in the document being retrieved, not
-// necessarily the number of pages in `bytes`. For example, if the document is a
-// PDF, `pdf_page_count` is the number of pages in the PDF, while `bytes` could
-// be empty because the PDF is too large.
-using PageContentRetrievedCallback =
-    base::OnceCallback<void(std::vector<lens::PageContent> page_contents,
-                            lens::MimeType primary_content_type,
-                            std::optional<uint32_t> pdf_page_count)>;
-
 // Manages all state associated with the lens overlay.
 // This class is not thread safe. It should only be used from the browser
 // thread.
@@ -310,10 +294,10 @@
   void TriggerCopy();
 
   // Returns true if the overlay is open and covering the current active tab.
-  bool IsOverlayShowing();
+  bool IsOverlayShowing() const;
 
   // Returns true if the overlay is showing or is in live page mode.
-  bool IsOverlayActive();
+  bool IsOverlayActive() const;
 
   // Returns true if the overlay is in the process of initializing.
   bool IsOverlayInitializing();
@@ -476,11 +460,6 @@
     return initialization_data_->significant_region_boxes_;
   }
 
-  lens::LensPermissionBubbleController*
-  get_lens_permission_bubble_controller_for_testing() {
-    return permission_bubble_controller_.get();
-  }
-
   views::Widget* get_preselection_widget_for_testing() {
     return preselection_widget_.get();
   }
@@ -797,20 +776,7 @@
       lens::MimeType primary_content_type,
       std::optional<uint32_t> page_count);
 
-  // Tries to fetch the underlying page content bytes to use for
-  // contextualization. If page content can not be retrieved, the callback will
-  // be run with no bytes.
-  void GetPageContextualization(PageContentRetrievedCallback callback);
-
 #if BUILDFLAG(ENABLE_PDF)
-  // Receives the PDF bytes from the IPC call to the PDF renderer and stores
-  // them in initialization data. `pdf_page_count` is passed to the partial PDF
-  // text fetch to be used to determine when to stop fetching.
-  void OnPdfBytesReceived(PageContentRetrievedCallback callback,
-                          pdf::mojom::PdfListener::GetPdfBytesStatus status,
-                          const std::vector<uint8_t>& bytes,
-                          uint32_t pdf_page_count);
-
   // Fetches the visible page index from the PDF renderer and then starts the
   // process of fetching the text from the PDF to be used for suggest signals.
   void FetchVisiblePageIndexAndGetPartialPdfText(uint32_t page_count);
@@ -825,47 +791,6 @@
                                  const std::u16string& page_text);
 #endif  // BUILDFLAG(ENABLE_PDF)
 
-  // Gets the inner HTML for contextualization if flag enabled. Otherwise skip
-  // to MaybeGetInnerText().
-  void MaybeGetInnerHtml(std::vector<lens::PageContent> page_contents,
-                         content::RenderFrameHost* render_frame_host,
-                         PageContentRetrievedCallback callback);
-
-  // Callback for when the inner HTML is retrieved from the underlying page.
-  // Calls MaybeGetInnerText().
-  void OnInnerHtmlReceived(std::vector<lens::PageContent> page_contents,
-                           content::RenderFrameHost* render_frame_host,
-                           PageContentRetrievedCallback callback,
-                           const std::optional<std::string>& result);
-
-  // Gets the inner text for contextualization if flag enabled. Otherwise skip
-  // to MaybeGetAnnotatedPageContent().
-  void MaybeGetInnerText(std::vector<lens::PageContent> page_contents,
-                         content::RenderFrameHost* render_frame_host,
-                         PageContentRetrievedCallback callback);
-
-  // Callback for when the inner text is retrieved from the underlying page.
-  // Calls MaybeGetAnnotatedPageContent().
-  void OnInnerTextReceived(
-      std::vector<lens::PageContent> page_contents,
-      content::RenderFrameHost* render_frame_host,
-      PageContentRetrievedCallback callback,
-      std::unique_ptr<content_extraction::InnerTextResult> result);
-
-  // Gets the annotated page content for contextualization if flag enabled.
-  // Otherwise run the callback with the HTML and/or innerText.
-  void MaybeGetAnnotatedPageContent(
-      std::vector<lens::PageContent> page_contents,
-      content::RenderFrameHost* render_frame_host,
-      PageContentRetrievedCallback callback);
-
-  // Callback for when the annotated page content is retrieved. Runs the
-  // callback with the HTML, innerText, and/or annotated page content.
-  void OnAnnotatedPageContentReceived(
-      std::vector<lens::PageContent> page_contents,
-      PageContentRetrievedCallback callback,
-      std::optional<optimization_guide::AIPageContentResult> apc);
-
   // Creates the mojo bounding boxes for the significant regions.
   std::vector<lens::mojom::CenterRotatedBoxPtr> ConvertSignificantRegionBoxes(
       const std::vector<gfx::Rect>& all_bounds);
@@ -1021,15 +946,6 @@
   // Called when the associated tab will enter the background.
   void TabWillEnterBackground(tabs::TabInterface* tab);
 
-  // Called when the tab's WebContents is discarded.
-  void WillDiscardContents(tabs::TabInterface* tab,
-                           content::WebContents* old_contents,
-                           content::WebContents* new_contents);
-
-  // Called when the tab will be removed from the window.
-  void WillDetach(tabs::TabInterface* tab,
-                  tabs::TabInterface::DetachReason reason);
-
   // Suggest a name for the save as image feature incorporating the hostname of
   // the page. Protocol, TLD, etc are not taken into consideration. Duplicate
   // names get automatic suffixes.
@@ -1162,6 +1078,10 @@
   // Shorthand to grab the LensSearchboxController for this instance of Lens.
   lens::LensSearchboxController* GetLensSearchboxController();
 
+  // Shorthand to grab the LensSearchContextualizationController for this
+  // instance of Lens.
+  lens::LensSearchContextualizationController* GetContextualizationController();
+
   // Owns the LensSearchController which owns this class
   raw_ptr<tabs::TabInterface> tab_;
 
@@ -1179,10 +1099,6 @@
   // that the overlay will return to when the tab is foregrounded.
   State backgrounded_state_ = State::kOff;
 
-  // Controller for showing the page screenshot permission bubble.
-  std::unique_ptr<lens::LensPermissionBubbleController>
-      permission_bubble_controller_;
-
   // The assembly data needed for the overlay to be created and shown.
   std::unique_ptr<OverlayInitializationData> initialization_data_;
 
@@ -1241,9 +1157,6 @@
   // until the overlay is closed.
   raw_ptr<lens::LensOverlayQueryController> lens_overlay_query_controller_;
 
-  // Holds subscriptions for TabInterface callbacks.
-  std::vector<base::CallbackListSubscription> tab_subscriptions_;
-
   // The callbacks pending the handshake to complete so the Lens suggest inputs
   // can be retrieved.
   base::OnceCallbackList<void(
diff --git a/chrome/browser/ui/lens/lens_overlay_controller_browsertest.cc b/chrome/browser/ui/lens/lens_overlay_controller_browsertest.cc
index de9c8208..a50c5d3 100644
--- a/chrome/browser/ui/lens/lens_overlay_controller_browsertest.cc
+++ b/chrome/browser/ui/lens/lens_overlay_controller_browsertest.cc
@@ -77,10 +77,13 @@
 #include "chrome/browser/ui/location_bar/location_bar.h"
 #include "chrome/browser/ui/tabs/public/tab_features.h"
 #include "chrome/browser/ui/tabs/tab_strip_model.h"
+#include "chrome/browser/ui/ui_features.h"
 #include "chrome/browser/ui/views/frame/browser_view.h"
 #include "chrome/browser/ui/views/location_bar/location_bar_view.h"
 #include "chrome/browser/ui/views/omnibox/omnibox_view_views.h"
+#include "chrome/browser/ui/views/page_action/page_action_container_view.h"
 #include "chrome/browser/ui/views/page_action/page_action_icon_controller.h"
+#include "chrome/browser/ui/views/page_action/page_action_view.h"
 #include "chrome/browser/ui/views/side_panel/side_panel.h"
 #include "chrome/browser/ui/views/side_panel/side_panel_coordinator.h"
 #include "chrome/browser/ui/views/side_panel/side_panel_entry_id.h"
@@ -957,38 +960,6 @@
                                              web_contents->GetURL());
   }
 
-  void VerifyEntrypoints(bool expected_visible) {
-    // Verify context menu entrypoint matches expected visibility.
-    EXPECT_EQ(expected_visible, GetContextMenu()->IsItemPresent(
-                                    IDC_CONTENT_CONTEXT_LENS_REGION_SEARCH));
-
-    // Verify omnibox (location bar) icon matches expected visibility.
-    auto* location_bar =
-        BrowserView::GetBrowserViewForBrowser(browser())->GetLocationBarView();
-    location_bar->omnibox_view()->RequestFocus();
-    location_bar->page_action_icon_controller()->UpdateAll();
-
-    auto* omnibox_entrypoint =
-        location_bar->page_action_icon_controller()->GetIconView(
-            PageActionIconType::kLensOverlay);
-    ASSERT_TRUE(base::test::RunUntil([&]() {
-      return omnibox_entrypoint->GetVisible() == expected_visible;
-    }));
-
-    // Verify three dot menu entrypoint matches expected visibility.
-    EXPECT_EQ(expected_visible,
-              browser()->command_controller()->IsCommandEnabled(
-                  IDC_CONTENT_CONTEXT_LENS_OVERLAY));
-
-    // Verify toolbar entrypoint is always enabled and visible.
-    actions::ActionItem* toolbar_entry_point =
-        actions::ActionManager::Get().FindAction(
-            kActionSidePanelShowLensOverlayResults,
-            browser()->browser_actions()->root_action_item());
-    EXPECT_TRUE(toolbar_entry_point->GetVisible());
-    EXPECT_TRUE(toolbar_entry_point->GetEnabled());
-  }
-
  protected:
   base::test::ScopedFeatureList feature_list_;
   raw_ptr<MockHatsService> mock_hats_service_ = nullptr;
@@ -1023,24 +994,29 @@
   // Wait for the bubble to become visible.
   views::test::WidgetVisibleWaiter(bubble_widget).Wait();
   ASSERT_TRUE(bubble_widget->IsVisible());
-  ASSERT_TRUE(controller->get_lens_permission_bubble_controller_for_testing()
-                  ->HasOpenDialogWidget());
+
+  auto* search_controller = GetLensSearchController();
+  ASSERT_TRUE(
+      search_controller->get_lens_permission_bubble_controller_for_testing()
+          ->HasOpenDialogWidget());
 
   // Verify attempting to show the UI again does not close the bubble widget.
   OpenLensOverlay(LensOverlayInvocationSource::kAppMenu);
   // State should remain off.
   ASSERT_EQ(controller->state(), State::kOff);
   ASSERT_TRUE(bubble_widget->IsVisible());
-  ASSERT_TRUE(controller->get_lens_permission_bubble_controller_for_testing()
-                  ->HasOpenDialogWidget());
+  ASSERT_TRUE(
+      search_controller->get_lens_permission_bubble_controller_for_testing()
+          ->HasOpenDialogWidget());
 
   // Simulate click on the accept button.
   auto* bubble_widget_delegate =
       bubble_widget->widget_delegate()->AsBubbleDialogDelegate();
   ClickBubbleDialogButton(bubble_widget_delegate,
                           bubble_widget_delegate->GetOkButton());
-  ASSERT_FALSE(controller->get_lens_permission_bubble_controller_for_testing()
-                   ->HasOpenDialogWidget());
+  ASSERT_FALSE(
+      search_controller->get_lens_permission_bubble_controller_for_testing()
+          ->HasOpenDialogWidget());
 
   // Verify sharing the page content and screenshot are now permitted.
   ASSERT_TRUE(lens::CanSharePageContentWithLensOverlay(prefs));
@@ -1084,24 +1060,29 @@
   // Wait for the bubble to become visible.
   views::test::WidgetVisibleWaiter(bubble_widget).Wait();
   ASSERT_TRUE(bubble_widget->IsVisible());
-  ASSERT_TRUE(controller->get_lens_permission_bubble_controller_for_testing()
-                  ->HasOpenDialogWidget());
+
+  auto* search_controller = GetLensSearchController();
+  ASSERT_TRUE(
+      search_controller->get_lens_permission_bubble_controller_for_testing()
+          ->HasOpenDialogWidget());
 
   // Verify attempting to show the UI again does not close the bubble widget.
   OpenLensOverlay(LensOverlayInvocationSource::kAppMenu);
   // State should remain off.
   ASSERT_EQ(controller->state(), State::kOff);
   ASSERT_TRUE(bubble_widget->IsVisible());
-  ASSERT_TRUE(controller->get_lens_permission_bubble_controller_for_testing()
-                  ->HasOpenDialogWidget());
+  ASSERT_TRUE(
+      search_controller->get_lens_permission_bubble_controller_for_testing()
+          ->HasOpenDialogWidget());
 
   // Simulate click on the accept button.
   auto* bubble_widget_delegate =
       bubble_widget->widget_delegate()->AsBubbleDialogDelegate();
   ClickBubbleDialogButton(bubble_widget_delegate,
                           bubble_widget_delegate->GetOkButton());
-  ASSERT_FALSE(controller->get_lens_permission_bubble_controller_for_testing()
-                   ->HasOpenDialogWidget());
+  ASSERT_FALSE(
+      search_controller->get_lens_permission_bubble_controller_for_testing()
+          ->HasOpenDialogWidget());
 
   // Verify sharing the page content and screenshot are now permitted.
   ASSERT_TRUE(lens::CanSharePageContentWithLensOverlay(prefs));
@@ -1143,24 +1124,29 @@
   // Wait for the bubble to become visible.
   views::test::WidgetVisibleWaiter(bubble_widget).Wait();
   ASSERT_TRUE(bubble_widget->IsVisible());
-  ASSERT_TRUE(controller->get_lens_permission_bubble_controller_for_testing()
-                  ->HasOpenDialogWidget());
+
+  auto* search_controller = GetLensSearchController();
+  ASSERT_TRUE(
+      search_controller->get_lens_permission_bubble_controller_for_testing()
+          ->HasOpenDialogWidget());
 
   // Verify attempting to show the UI again does not close the bubble widget.
   OpenLensOverlay(LensOverlayInvocationSource::kAppMenu);
   // State should remain off.
   ASSERT_EQ(controller->state(), State::kOff);
   ASSERT_TRUE(bubble_widget->IsVisible());
-  ASSERT_TRUE(controller->get_lens_permission_bubble_controller_for_testing()
-                  ->HasOpenDialogWidget());
+  ASSERT_TRUE(
+      search_controller->get_lens_permission_bubble_controller_for_testing()
+          ->HasOpenDialogWidget());
 
   // Simulate click on the reject button.
   auto* bubble_widget_delegate =
       bubble_widget->widget_delegate()->AsBubbleDialogDelegate();
   ClickBubbleDialogButton(bubble_widget_delegate,
                           bubble_widget_delegate->GetCancelButton());
-  ASSERT_FALSE(controller->get_lens_permission_bubble_controller_for_testing()
-                   ->HasOpenDialogWidget());
+  ASSERT_FALSE(
+      search_controller->get_lens_permission_bubble_controller_for_testing()
+          ->HasOpenDialogWidget());
 
   // Verify sharing the page screenshot is still permitted.
   ASSERT_TRUE(lens::CanSharePageScreenshotWithLensOverlay(prefs));
@@ -4730,7 +4716,86 @@
                   ->IsEnabled());
 }
 
-IN_PROC_BROWSER_TEST_F(LensOverlayControllerBrowserTest,
+class LensOverlayControllerEntrypointsBrowserTest
+    : public LensOverlayControllerBrowserTest,
+      public ::testing::WithParamInterface<bool> {
+ public:
+  LensOverlayControllerEntrypointsBrowserTest() = default;
+  ~LensOverlayControllerEntrypointsBrowserTest() override = default;
+
+  void SetupFeatureList() override {
+    std::vector<base::test::FeatureRefAndParams> enabled_features = {
+        {lens::features::kLensOverlay, {}},
+        {lens::features::kLensOverlayContextualSearchbox,
+         {
+
+         }},
+        {lens::features::kLensOverlaySurvey, {}},
+        {lens::features::kLensOverlaySidePanelOpenInNewTab, {}}};
+    if (IsPageActionsMigrationEnabled()) {
+      enabled_features.push_back(
+          {::features::kPageActionsMigration,
+           {
+               {::features::kPageActionsMigrationLensOverlay.name, "true"},
+           }});
+    }
+    feature_list_.InitWithFeaturesAndParameters(
+        enabled_features,
+        /*disabled_features=*/{
+            lens::features::kLensOverlaySimplifiedSelection});
+  }
+
+  void VerifyEntrypoints(bool expected_visible) {
+    // Verify context menu entrypoint matches expected visibility.
+    EXPECT_EQ(expected_visible, GetContextMenu()->IsItemPresent(
+                                    IDC_CONTENT_CONTEXT_LENS_REGION_SEARCH));
+
+    // Verify omnibox (location bar) icon matches expected visibility.
+    auto* location_bar =
+        BrowserView::GetBrowserViewForBrowser(browser())->GetLocationBarView();
+    location_bar->omnibox_view()->RequestFocus();
+    views::View* omnibox_entrypoint;
+    if (IsPageActionMigrated(PageActionIconType::kLensOverlay)) {
+      omnibox_entrypoint =
+          location_bar->page_action_container()->GetPageActionView(
+              kActionSidePanelShowLensOverlayResults);
+    } else {
+      location_bar->page_action_icon_controller()->UpdateAll();
+      omnibox_entrypoint =
+          location_bar->page_action_icon_controller()->GetIconView(
+              PageActionIconType::kLensOverlay);
+    }
+    ASSERT_TRUE(base::test::RunUntil([&]() {
+      return omnibox_entrypoint->GetVisible() == expected_visible;
+    }));
+
+    // Verify three dot menu entrypoint matches expected visibility.
+    EXPECT_EQ(expected_visible,
+              browser()->command_controller()->IsCommandEnabled(
+                  IDC_CONTENT_CONTEXT_LENS_OVERLAY));
+
+    // Verify toolbar entrypoint is always enabled and visible.
+    actions::ActionItem* toolbar_entry_point =
+        actions::ActionManager::Get().FindAction(
+            kActionSidePanelShowLensOverlayResults,
+            browser()->browser_actions()->root_action_item());
+    EXPECT_TRUE(toolbar_entry_point->GetVisible());
+    EXPECT_TRUE(toolbar_entry_point->GetEnabled());
+  }
+
+ private:
+  bool IsPageActionsMigrationEnabled() const { return GetParam(); }
+};
+
+INSTANTIATE_TEST_SUITE_P(All,
+                         LensOverlayControllerEntrypointsBrowserTest,
+                         ::testing::Values(false, true),
+                         [](const ::testing::TestParamInfo<bool>& info) {
+                           return info.param ? "PageActionsMigrationEnabled"
+                                             : "PageActionsMigrationDisabled";
+                         });
+
+IN_PROC_BROWSER_TEST_P(LensOverlayControllerEntrypointsBrowserTest,
                        OverlayHidesEntrypoints) {
   WaitForPaint();
 
@@ -8504,24 +8569,29 @@
   // Wait for the bubble to become visible.
   views::test::WidgetVisibleWaiter(bubble_widget).Wait();
   ASSERT_TRUE(bubble_widget->IsVisible());
-  ASSERT_TRUE(controller->get_lens_permission_bubble_controller_for_testing()
-                  ->HasOpenDialogWidget());
+
+  auto* search_controller = GetLensSearchController();
+  ASSERT_TRUE(
+      search_controller->get_lens_permission_bubble_controller_for_testing()
+          ->HasOpenDialogWidget());
 
   // Verify attempting to show the UI again does not close the bubble widget.
   OpenLensOverlay(LensOverlayInvocationSource::kAppMenu);
   // State should remain off.
   ASSERT_EQ(controller->state(), State::kOff);
   ASSERT_TRUE(bubble_widget->IsVisible());
-  ASSERT_TRUE(controller->get_lens_permission_bubble_controller_for_testing()
-                  ->HasOpenDialogWidget());
+  ASSERT_TRUE(
+      search_controller->get_lens_permission_bubble_controller_for_testing()
+          ->HasOpenDialogWidget());
 
   // Simulate click on the accept button.
   auto* bubble_widget_delegate =
       bubble_widget->widget_delegate()->AsBubbleDialogDelegate();
   ClickBubbleDialogButton(bubble_widget_delegate,
                           bubble_widget_delegate->GetOkButton());
-  ASSERT_FALSE(controller->get_lens_permission_bubble_controller_for_testing()
-                   ->HasOpenDialogWidget());
+  ASSERT_FALSE(
+      search_controller->get_lens_permission_bubble_controller_for_testing()
+          ->HasOpenDialogWidget());
 
   // Verify sharing the page screenshot is now permitted.
   ASSERT_TRUE(lens::CanSharePageScreenshotWithLensOverlay(prefs));
@@ -8562,24 +8632,29 @@
   // Wait for the bubble to become visible.
   views::test::WidgetVisibleWaiter(bubble_widget).Wait();
   ASSERT_TRUE(bubble_widget->IsVisible());
-  ASSERT_TRUE(controller->get_lens_permission_bubble_controller_for_testing()
-                  ->HasOpenDialogWidget());
+
+  auto* search_controller = GetLensSearchController();
+  ASSERT_TRUE(
+      search_controller->get_lens_permission_bubble_controller_for_testing()
+          ->HasOpenDialogWidget());
 
   // Verify attempting to show the UI again does not close the bubble widget.
   OpenLensOverlay(LensOverlayInvocationSource::kAppMenu);
   // State should remain off.
   ASSERT_EQ(controller->state(), State::kOff);
   ASSERT_TRUE(bubble_widget->IsVisible());
-  ASSERT_TRUE(controller->get_lens_permission_bubble_controller_for_testing()
-                  ->HasOpenDialogWidget());
+  ASSERT_TRUE(
+      search_controller->get_lens_permission_bubble_controller_for_testing()
+          ->HasOpenDialogWidget());
 
   // Simulate click on the reject button.
   auto* bubble_widget_delegate =
       bubble_widget->widget_delegate()->AsBubbleDialogDelegate();
   ClickBubbleDialogButton(bubble_widget_delegate,
                           bubble_widget_delegate->GetCancelButton());
-  ASSERT_FALSE(controller->get_lens_permission_bubble_controller_for_testing()
-                   ->HasOpenDialogWidget());
+  ASSERT_FALSE(
+      search_controller->get_lens_permission_bubble_controller_for_testing()
+          ->HasOpenDialogWidget());
 
   // Verify sharing the page screenshot is still not permitted.
   ASSERT_FALSE(lens::CanSharePageScreenshotWithLensOverlay(prefs));
diff --git a/chrome/browser/ui/lens/lens_overlay_entry_point_controller.cc b/chrome/browser/ui/lens/lens_overlay_entry_point_controller.cc
index 9238e84..eae642c 100644
--- a/chrome/browser/ui/lens/lens_overlay_entry_point_controller.cc
+++ b/chrome/browser/ui/lens/lens_overlay_entry_point_controller.cc
@@ -156,7 +156,7 @@
   return phys_mem_mb > lens::features::GetLensOverlayMinRamMb();
 }
 
-bool LensOverlayEntryPointController::AreVisible() {
+bool LensOverlayEntryPointController::AreVisible() const {
   return IsEnabled() && !IsOverlayActive();
 }
 
@@ -177,6 +177,7 @@
       toolbar_entry_point->SetVisible(enabled);
     }
   }
+  UpdatePageActionState();
 }
 
 // static
@@ -311,18 +312,19 @@
                                              });
 }
 
-bool LensOverlayEntryPointController::IsOverlayActive() {
-  auto* active_tab = browser_window_interface_->GetActiveTabInterface();
+bool LensOverlayEntryPointController::IsOverlayActive() const {
+  const auto* active_tab = browser_window_interface_->GetActiveTabInterface();
   if (!active_tab) {
     return false;
   }
-  auto* controller = active_tab->GetTabFeatures()->lens_overlay_controller();
+  const auto* controller =
+      active_tab->GetTabFeatures()->lens_overlay_controller();
   return controller && controller->IsOverlayActive();
 }
 
 bool LensOverlayEntryPointController::ShouldShowPageAction(
     tabs::TabInterface* active_tab) const {
-  if (!IsEnabled()) {
+  if (!AreVisible()) {
     return false;
   }
 
diff --git a/chrome/browser/ui/lens/lens_overlay_entry_point_controller.h b/chrome/browser/ui/lens/lens_overlay_entry_point_controller.h
index cbd3815..a86f07c 100644
--- a/chrome/browser/ui/lens/lens_overlay_entry_point_controller.h
+++ b/chrome/browser/ui/lens/lens_overlay_entry_point_controller.h
@@ -56,7 +56,7 @@
   // this current moment in time. Sometimes, entrypoints are hidden ephermally,
   // such as when the Lens Overlay is currently active, so entrypoints do
   // nothing.
-  bool AreVisible();
+  bool AreVisible() const;
 
   // Updates the enable/disable and visibility state of entry points. If
   // hide_toolbar_entrypoint is true, instead of just disabling the toolbar
@@ -91,7 +91,7 @@
   actions::ActionItem* GetToolbarEntrypoint();
 
   // Return true if the Lens Overlay is active on the current tab.
-  bool IsOverlayActive();
+  bool IsOverlayActive() const;
 
   // Observer to check for focus changes.
   base::ScopedObservation<views::FocusManager, views::FocusChangeListener>
diff --git a/chrome/browser/ui/lens/lens_search_contextualization_controller.cc b/chrome/browser/ui/lens/lens_search_contextualization_controller.cc
index 52f4c52..24943644 100644
--- a/chrome/browser/ui/lens/lens_search_contextualization_controller.cc
+++ b/chrome/browser/ui/lens/lens_search_contextualization_controller.cc
@@ -4,7 +4,269 @@
 
 #include "chrome/browser/ui/lens/lens_search_contextualization_controller.h"
 
-LensSearchContextualizationController::LensSearchContextualizationController() =
-    default;
+#include "base/functional/bind.h"
+#include "chrome/browser/content_extraction/inner_html.h"
+#include "chrome/browser/content_extraction/inner_text.h"
+#include "chrome/browser/ui/lens/lens_overlay_proto_converter.h"
+#include "chrome/browser/ui/lens/lens_overlay_side_panel_coordinator.h"
+#include "chrome/browser/ui/lens/lens_search_controller.h"
+#include "components/lens/lens_features.h"
+#include "components/tabs/public/tab_interface.h"
+#include "pdf/buildflags.h"
+
+#if BUILDFLAG(ENABLE_PDF)
+#include "components/pdf/browser/pdf_document_helper.h"
+#include "pdf/mojom/pdf.mojom.h"
+#endif  // BUILDFLAG(ENABLE_PDF)
+
+namespace {
+
+bool IsPageContextEligible(
+    const GURL& main_frame_url,
+    std::vector<optimization_guide::FrameMetadata> frame_metadata,
+    optimization_guide::PageContextEligibility* page_context_eligibility) {
+  if (!page_context_eligibility ||
+      !lens::features::IsLensSearchProtectedPageEnabled() ||
+      !lens::features::IsLensOverlayContextualSearchboxEnabled() ||
+      !lens::features::UseApcAsContext()) {
+    return true;
+  }
+  return page_context_eligibility->api().IsPageContextEligible(
+      main_frame_url.host(), main_frame_url.path(), std::move(frame_metadata));
+}
+
+}  // namespace
+
+namespace lens {
+
+LensSearchContextualizationController::LensSearchContextualizationController(
+    LensSearchController* lens_search_controller)
+    : lens_search_controller_(lens_search_controller) {}
 LensSearchContextualizationController::
     ~LensSearchContextualizationController() = default;
+
+void LensSearchContextualizationController::StartContextualization(
+    lens::LensOverlayInvocationSource invocation_source,
+    lens::LensOverlayQueryController* lens_overlay_query_controller) {
+  // TODO(crbug.com/403573362): Implement starting the query flow from here if
+  // needed.
+  return;
+}
+
+void LensSearchContextualizationController::GetPageContextualization(
+    PageContentRetrievedCallback callback) {
+  // If the contextual searchbox is disabled, exit early.
+  if (!lens::features::IsLensOverlayContextualSearchboxEnabled()) {
+    std::move(callback).Run(/*page_contents=*/{}, lens::MimeType::kUnknown,
+                            std::nullopt);
+    return;
+  }
+
+  is_page_context_eligible_ = true;
+  lens_search_controller_->lens_overlay_side_panel_coordinator()
+      ->SetShowProtectedErrorPage(false);
+
+#if BUILDFLAG(ENABLE_PDF)
+  // The overlay controller needs to check if the PDF helper exists before
+  // calling MaybeGetPdfBytes or else the `callback` will have been moved but
+  // not called.
+  pdf::PDFDocumentHelper* pdf_helper =
+      lens::features::UsePdfsAsContext()
+          ? pdf::PDFDocumentHelper::MaybeGetForWebContents(
+                lens_search_controller_->GetTabInterface()->GetContents())
+          : nullptr;
+  if (lens::features::UsePdfsAsContext() && pdf_helper) {
+    // Fetch the PDF bytes then run the callback.
+    MaybeGetPdfBytes(pdf_helper, std::move(callback));
+    return;
+  }
+#endif  // BUILDFLAG(ENABLE_PDF)
+
+  std::vector<lens::PageContent> page_contents;
+  auto* render_frame_host = lens_search_controller_->GetTabInterface()
+                                ->GetContents()
+                                ->GetPrimaryMainFrame();
+  if (!render_frame_host || (!lens::features::UseInnerHtmlAsContext() &&
+                             !lens::features::UseInnerTextAsContext() &&
+                             !lens::features::UseApcAsContext())) {
+    std::move(callback).Run(page_contents, lens::MimeType::kUnknown,
+                            std::nullopt);
+    return;
+  }
+  // TODO(crbug.com/399610478): The fetches for innerHTML, innerText, and APC
+  // should be parallelized to fetch all data at once. Currently fetches are
+  // sequential to prevent getting stuck in a race condition.
+  MaybeGetInnerHtml(page_contents, render_frame_host, std::move(callback));
+}
+
+void LensSearchContextualizationController::MaybeGetInnerHtml(
+    std::vector<lens::PageContent> page_contents,
+    content::RenderFrameHost* render_frame_host,
+    PageContentRetrievedCallback callback) {
+  if (!lens::features::UseInnerHtmlAsContext()) {
+    MaybeGetInnerText(page_contents, render_frame_host, std::move(callback));
+    return;
+  }
+  content_extraction::GetInnerHtml(
+      *render_frame_host,
+      base::BindOnce(
+          &LensSearchContextualizationController::OnInnerHtmlReceived,
+          weak_ptr_factory_.GetWeakPtr(), page_contents, render_frame_host,
+          std::move(callback)));
+}
+
+void LensSearchContextualizationController::OnInnerHtmlReceived(
+    std::vector<lens::PageContent> page_contents,
+    content::RenderFrameHost* render_frame_host,
+    PageContentRetrievedCallback callback,
+    const std::optional<std::string>& result) {
+  const bool was_successful =
+      result.has_value() &&
+      result->size() <= lens::features::GetLensOverlayFileUploadLimitBytes();
+  // Add the innerHTML to the page contents if successful, or empty bytes if
+  // not.
+  page_contents.emplace_back(
+      /*bytes=*/was_successful
+          ? std::vector<uint8_t>(result->begin(), result->end())
+          : std::vector<uint8_t>{},
+      lens::MimeType::kHtml);
+  MaybeGetInnerText(page_contents, render_frame_host, std::move(callback));
+}
+
+void LensSearchContextualizationController::MaybeGetInnerText(
+    std::vector<lens::PageContent> page_contents,
+    content::RenderFrameHost* render_frame_host,
+    PageContentRetrievedCallback callback) {
+  if (!lens::features::UseInnerTextAsContext()) {
+    MaybeGetAnnotatedPageContent(page_contents, render_frame_host,
+                                 std::move(callback));
+    return;
+  }
+  content_extraction::GetInnerText(
+      *render_frame_host, /*node_id=*/std::nullopt,
+      base::BindOnce(
+          &LensSearchContextualizationController::OnInnerTextReceived,
+          weak_ptr_factory_.GetWeakPtr(), page_contents, render_frame_host,
+          std::move(callback)));
+}
+
+void LensSearchContextualizationController::OnInnerTextReceived(
+    std::vector<lens::PageContent> page_contents,
+    content::RenderFrameHost* render_frame_host,
+    PageContentRetrievedCallback callback,
+    std::unique_ptr<content_extraction::InnerTextResult> result) {
+  const bool was_successful =
+      result && result->inner_text.size() <=
+                    lens::features::GetLensOverlayFileUploadLimitBytes();
+  // Add the innerText to the page_contents if successful, or empty bytes if
+  // not.
+  page_contents.emplace_back(
+      /*bytes=*/was_successful
+          ? std::vector<uint8_t>(result->inner_text.begin(),
+                                 result->inner_text.end())
+          : std::vector<uint8_t>{},
+      lens::MimeType::kPlainText);
+  MaybeGetAnnotatedPageContent(page_contents, render_frame_host,
+                               std::move(callback));
+}
+
+void LensSearchContextualizationController::MaybeGetAnnotatedPageContent(
+    std::vector<lens::PageContent> page_contents,
+    content::RenderFrameHost* render_frame_host,
+    PageContentRetrievedCallback callback) {
+  if (!lens::features::UseApcAsContext()) {
+    // Done fetching page contents.
+    // Keep legacy behavior consistent by setting the primary content type to
+    // plain text if that is the only content type enabled.
+    // TODO(crbug.com/401614601): Set primary content type to kHtml in all
+    // cases.
+    auto primary_content_type = lens::features::UseInnerTextAsContext() &&
+                                        !lens::features::UseInnerHtmlAsContext()
+                                    ? lens::MimeType::kPlainText
+                                    : lens::MimeType::kHtml;
+    std::move(callback).Run(page_contents, primary_content_type, std::nullopt);
+    return;
+  }
+
+  blink::mojom::AIPageContentOptionsPtr ai_page_content_options =
+      optimization_guide::DefaultAIPageContentOptions();
+  ai_page_content_options->on_critical_path = true;
+  ai_page_content_options->max_meta_elements = 20;
+  optimization_guide::GetAIPageContent(
+      lens_search_controller_->GetTabInterface()->GetContents(),
+      std::move(ai_page_content_options),
+      base::BindOnce(&LensSearchContextualizationController::
+                         OnAnnotatedPageContentReceived,
+                     weak_ptr_factory_.GetWeakPtr(), page_contents,
+                     std::move(callback)));
+}
+
+void LensSearchContextualizationController::OnAnnotatedPageContentReceived(
+    std::vector<lens::PageContent> page_contents,
+    PageContentRetrievedCallback callback,
+    std::optional<optimization_guide::AIPageContentResult> result) {
+  // Add the apc proto the page_contents if it exists.
+  if (result) {
+    // Convert the page metadata to a C struct defined in the optimization_guide
+    // component so it can be passed to the shared library.
+    std::vector<optimization_guide::FrameMetadata> frame_metadata_structs =
+        lens::ConvertFrameMetadataFromProto(result.value());
+
+    // If the page is protected, do not send the latest page content to the
+    // server.
+    const auto& tab_url = lens_search_controller_->GetTabInterface()
+                              ->GetContents()
+                              ->GetLastCommittedURL();
+    if (!IsPageContextEligible(
+            tab_url, std::move(frame_metadata_structs),
+            lens_search_controller_->page_context_eligibility())) {
+      is_page_context_eligible_ = false;
+      lens_search_controller_->lens_overlay_side_panel_coordinator()
+          ->SetShowProtectedErrorPage(true);
+      // Clear all previous page contents.
+      page_contents.clear();
+    } else {
+      std::string serialized_apc;
+      result->proto.SerializeToString(&serialized_apc);
+      page_contents.emplace_back(
+          std::vector<uint8_t>(serialized_apc.begin(), serialized_apc.end()),
+          lens::MimeType::kAnnotatedPageContent);
+    }
+  }
+  // Done fetching page contents.
+  std::move(callback).Run(page_contents, lens::MimeType::kAnnotatedPageContent,
+                          std::nullopt);
+}
+
+#if BUILDFLAG(ENABLE_PDF)
+void LensSearchContextualizationController::MaybeGetPdfBytes(
+    pdf::PDFDocumentHelper* pdf_helper,
+    PageContentRetrievedCallback callback) {
+  // Try and fetch the PDF bytes if enabled.
+  CHECK(pdf_helper);
+  pdf_helper->GetPdfBytes(
+      /*size_limit=*/lens::features::GetLensOverlayFileUploadLimitBytes(),
+      base::BindOnce(&LensSearchContextualizationController::OnPdfBytesReceived,
+                     weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
+}
+
+void LensSearchContextualizationController::OnPdfBytesReceived(
+    PageContentRetrievedCallback callback,
+    pdf::mojom::PdfListener::GetPdfBytesStatus status,
+    const std::vector<uint8_t>& bytes,
+    uint32_t page_count) {
+  // TODO(crbug.com/370530197): Show user error message if status is not
+  // success.
+  if (status != pdf::mojom::PdfListener::GetPdfBytesStatus::kSuccess ||
+      page_count == 0) {
+    std::move(callback).Run(
+        {lens::PageContent(/*bytes=*/{}, lens::MimeType::kPdf)},
+        lens::MimeType::kPdf, page_count);
+    return;
+  }
+  std::move(callback).Run({lens::PageContent(bytes, lens::MimeType::kPdf)},
+                          lens::MimeType::kPdf, page_count);
+}
+#endif  // BUILDFLAG(ENABLE_PDF)
+
+}  // namespace lens
diff --git a/chrome/browser/ui/lens/lens_search_contextualization_controller.h b/chrome/browser/ui/lens/lens_search_contextualization_controller.h
index bd67ae3..14ceed442 100644
--- a/chrome/browser/ui/lens/lens_search_contextualization_controller.h
+++ b/chrome/browser/ui/lens/lens_search_contextualization_controller.h
@@ -5,13 +5,58 @@
 #ifndef CHROME_BROWSER_UI_LENS_LENS_SEARCH_CONTEXTUALIZATION_CONTROLLER_H_
 #define CHROME_BROWSER_UI_LENS_LENS_SEARCH_CONTEXTUALIZATION_CONTROLLER_H_
 
+#include "chrome/browser/lens/core/mojom/lens_side_panel.mojom.h"
+#include "chrome/browser/ui/lens/lens_overlay_query_controller.h"
+#include "components/lens/lens_overlay_invocation_source.h"
+#include "components/omnibox/browser/autocomplete_match_type.h"
+#include "components/tabs/public/tab_interface.h"
+#include "pdf/buildflags.h"
+
+#if BUILDFLAG(ENABLE_PDF)
+#include "components/pdf/browser/pdf_document_helper.h"
+#include "pdf/mojom/pdf.mojom.h"
+#endif  // BUILDFLAG(ENABLE_PDF)
+
+class LensSearchController;
+
+namespace content {
+class RenderFrameHost;
+}  // namespace content
+
+namespace content_extraction {
+struct InnerTextResult;
+}  // namespace content_extraction
+
+namespace optimization_guide {
+struct AIPageContentResult;
+}  // namespace optimization_guide
+
+using GetIsContextualSearchboxCallback =
+    lens::mojom::LensSidePanelPageHandler::GetIsContextualSearchboxCallback;
+
+namespace lens {
+
+// Callback type alias for page content bytes retrieved. Multiple pieces and
+// types of content may be retrieved and returned in `page_contents`.
+// `primary_content_type` is the main type used in the request flow and used to
+// determine request params and whether updated requests need to be sent.
+// `pdf_page_count` is the number of pages in the document being retrieved, not
+// necessarily the number of pages in `bytes`. For example, if the document is a
+// PDF, `pdf_page_count` is the number of pages in the PDF, while `bytes` could
+// be empty because the PDF is too large.
+using PageContentRetrievedCallback =
+    base::OnceCallback<void(std::vector<lens::PageContent> page_contents,
+                            lens::MimeType primary_content_type,
+                            std::optional<uint32_t> pdf_page_count)>;
+
 // Controller responsible for handling contextualization logic for Lens flows.
 // This includes grabbing content related to the page and issuing Lens requests
 // so searchbox requests are contextualized.
 class LensSearchContextualizationController {
  public:
-  LensSearchContextualizationController();
-  ~LensSearchContextualizationController();
+  explicit LensSearchContextualizationController(
+      LensSearchController* lens_search_controller);
+  virtual ~LensSearchContextualizationController();
 
   // Internal state machine. States are mutually exclusive. Exposed for testing.
   enum class State {
@@ -24,8 +69,88 @@
   };
   State state() { return state_; }
 
+  // Starts the contextualization flow without the overlay being shown to the
+  // user. Virtual for testing.
+  virtual void StartContextualization(
+      lens::LensOverlayInvocationSource invocation_source,
+      lens::LensOverlayQueryController* lens_overlay_query_controller);
+
+  // Tries to fetch the underlying page content bytes to use for
+  // contextualization. If page content can not be retrieved, the callback will
+  // be run with no bytes.
+  void GetPageContextualization(PageContentRetrievedCallback callback);
+
  private:
+  // Gets the inner HTML for contextualization if flag enabled. Otherwise skip
+  // to MaybeGetInnerText().
+  void MaybeGetInnerHtml(std::vector<lens::PageContent> page_contents,
+                         content::RenderFrameHost* render_frame_host,
+                         PageContentRetrievedCallback callback);
+
+  // Callback for when the inner HTML is retrieved from the underlying page.
+  // Calls MaybeGetInnerText().
+  void OnInnerHtmlReceived(std::vector<lens::PageContent> page_contents,
+                           content::RenderFrameHost* render_frame_host,
+                           PageContentRetrievedCallback callback,
+                           const std::optional<std::string>& result);
+
+  // Gets the inner text for contextualization if flag enabled. Otherwise skip
+  // to MaybeGetAnnotatedPageContent().
+  void MaybeGetInnerText(std::vector<lens::PageContent> page_contents,
+                         content::RenderFrameHost* render_frame_host,
+                         PageContentRetrievedCallback callback);
+
+  // Callback for when the inner text is retrieved from the underlying page.
+  // Calls MaybeGetAnnotatedPageContent().
+  void OnInnerTextReceived(
+      std::vector<lens::PageContent> page_contents,
+      content::RenderFrameHost* render_frame_host,
+      PageContentRetrievedCallback callback,
+      std::unique_ptr<content_extraction::InnerTextResult> result);
+
+  // Gets the annotated page content for contextualization if flag enabled.
+  // Otherwise run the callback with the HTML and/or innerText.
+  void MaybeGetAnnotatedPageContent(
+      std::vector<lens::PageContent> page_contents,
+      content::RenderFrameHost* render_frame_host,
+      PageContentRetrievedCallback callback);
+
+  // Callback for when the annotated page content is retrieved. Runs the
+  // callback with the HTML, innerText, and/or annotated page content.
+  void OnAnnotatedPageContentReceived(
+      std::vector<lens::PageContent> page_contents,
+      PageContentRetrievedCallback callback,
+      std::optional<optimization_guide::AIPageContentResult> apc);
+
+#if BUILDFLAG(ENABLE_PDF)
+  // Gets the PDF bytes from the IPC call to the PDF renderer if the PDF
+  // feature is enabled. Otherwise run the callback with no bytes.
+  void MaybeGetPdfBytes(pdf::PDFDocumentHelper* pdf_helper,
+                        PageContentRetrievedCallback callback);
+
+  // Receives the PDF bytes from the IPC call to the PDF renderer and stores
+  // them in initialization data. `pdf_page_count` is passed to the partial PDF
+  // text fetch to be used to determine when to stop fetching.
+  void OnPdfBytesReceived(PageContentRetrievedCallback callback,
+                          pdf::mojom::PdfListener::GetPdfBytesStatus status,
+                          const std::vector<uint8_t>& bytes,
+                          uint32_t pdf_page_count);
+#endif  // BUILDFLAG(ENABLE_PDF)
+
   // The current state of the contextualization flow.
   State state_ = State::kOff;
+
+  // Indicates whether the user is currently on a context eligible page.
+  bool is_page_context_eligible_ = true;
+
+  // Owns this.
+  const raw_ptr<LensSearchController> lens_search_controller_;
+
+  // Must be the last member.
+  base::WeakPtrFactory<LensSearchContextualizationController> weak_ptr_factory_{
+      this};
 };
+
+}  // namespace lens
+
 #endif  // CHROME_BROWSER_UI_LENS_LENS_SEARCH_CONTEXTUALIZATION_CONTROLLER_H_
diff --git a/chrome/browser/ui/lens/lens_search_controller.cc b/chrome/browser/ui/lens/lens_search_controller.cc
index b6270577..4e6d10ff 100644
--- a/chrome/browser/ui/lens/lens_search_controller.cc
+++ b/chrome/browser/ui/lens/lens_search_controller.cc
@@ -14,9 +14,12 @@
 #include "chrome/browser/ui/lens/lens_overlay_query_controller.h"
 #include "chrome/browser/ui/lens/lens_overlay_side_panel_coordinator.h"
 #include "chrome/browser/ui/lens/lens_overlay_theme_utils.h"
+#include "chrome/browser/ui/lens/lens_permission_bubble_controller.h"
+#include "chrome/browser/ui/lens/lens_search_contextualization_controller.h"
 #include "chrome/browser/ui/lens/lens_searchbox_controller.h"
 #include "chrome/browser/ui/tabs/public/tab_features.h"
 #include "chrome/browser/ui/webui/webui_embedding_context.h"
+#include "components/lens/lens_features.h"
 #include "components/lens/lens_overlay_permission_utils.h"
 #include "components/omnibox/browser/autocomplete_match_type.h"
 #include "components/optimization_guide/content/browser/page_context_eligibility.h"
@@ -40,7 +43,18 @@
 }  // namespace
 
 LensSearchController::LensSearchController(tabs::TabInterface* tab)
-    : tab_(tab) {}
+    : tab_(tab) {
+  tab_subscriptions_.push_back(tab_->RegisterDidActivate(base::BindRepeating(
+      &LensSearchController::TabForegrounded, weak_ptr_factory_.GetWeakPtr())));
+  tab_subscriptions_.push_back(tab_->RegisterWillDeactivate(
+      base::BindRepeating(&LensSearchController::TabWillEnterBackground,
+                          weak_ptr_factory_.GetWeakPtr())));
+  tab_subscriptions_.push_back(tab_->RegisterWillDiscardContents(
+      base::BindRepeating(&LensSearchController::WillDiscardContents,
+                          weak_ptr_factory_.GetWeakPtr())));
+  tab_subscriptions_.push_back(tab_->RegisterWillDetach(base::BindRepeating(
+      &LensSearchController::WillDetach, weak_ptr_factory_.GetWeakPtr())));
+}
 LensSearchController::~LensSearchController() = default;
 
 // TODO(crbug.com/404941800): Reconsider which of these controllers should be
@@ -70,6 +84,9 @@
 
   lens_searchbox_controller_ = CreateLensSearchboxController();
 
+  lens_contextualization_controller_ =
+      CreateLensSearchContextualizationController();
+
   CreatePageContextEligibilityAPI();
 }
 
@@ -91,29 +108,26 @@
     lens::LensOverlayInvocationSource invocation_source) {
   CheckInitialized(initialized_);
 
-  // The UI should only show if the tab is in the foreground or if the tab web
-  // contents is not in a crash state.
-  if (!tab_->IsActivated() || tab_->GetContents()->IsCrashed()) {
+  // If the eligibility checks fail, do not procced with opening any UI.
+  if (!RunLensEligibilityChecks(
+          invocation_source,
+          /*permission_granted_callback=*/base::BindRepeating(
+              &LensSearchController::OpenLensOverlay,
+              weak_ptr_factory_.GetWeakPtr(), invocation_source))) {
     return;
   }
 
-  // Exit early if the Lens feature is already active.
-  if (state() != State::kOff) {
-    return;
-  }
   state_ = State::kInitializing;
 
   // Create the query controller to be used for the current invocation.
   CHECK(!lens_overlay_query_controller_);
   lens_overlay_query_controller_ = CreateLensQueryController(invocation_source);
 
-  // TODO(crbug.com/404941800): Add logic based on this classes state once the
-  // state machine is available.
   lens_overlay_controller_->ShowUI(invocation_source,
                                    lens_overlay_query_controller_.get());
 }
 
-void LensSearchController::OpenLensOverlayWithPendingRegion(
+void LensSearchController::OpenLensOverlayWithPendingRegionFromBounds(
     lens::LensOverlayInvocationSource invocation_source,
     const gfx::Rect& tab_bounds,
     const gfx::Rect& view_bounds,
@@ -130,14 +144,13 @@
     lens::LensOverlayInvocationSource invocation_source,
     lens::mojom::CenterRotatedBoxPtr region,
     const SkBitmap& region_bitmap) {
-  // The UI should only show if the tab is in the foreground or if the tab web
-  // contents is not in a crash state.
-  if (!tab_->IsActivated() || tab_->GetContents()->IsCrashed()) {
-    return;
-  }
-
-  // Exit early if the Lens feature is already active.
-  if (state() != State::kOff) {
+  // If the eligibility checks fail, do not procced with opening any UI.
+  if (!RunLensEligibilityChecks(
+          invocation_source,
+          /*permission_granted_callback=*/base::BindRepeating(
+              &LensSearchController::OpenLensOverlayWithPendingRegion,
+              weak_ptr_factory_.GetWeakPtr(), invocation_source,
+              base::Passed(region.Clone()), region_bitmap))) {
     return;
   }
   state_ = State::kInitializing;
@@ -146,8 +159,6 @@
   CHECK(!lens_overlay_query_controller_);
   lens_overlay_query_controller_ = CreateLensQueryController(invocation_source);
 
-  // TODO(crbug.com/404941800): Add logic based on this classes state once the
-  // state machine is available.
   lens_overlay_controller_->ShowUIWithPendingRegion(
       lens_overlay_query_controller_.get(), invocation_source,
       std::move(region), region_bitmap);
@@ -155,24 +166,20 @@
 
 void LensSearchController::StartContextualization(
     lens::LensOverlayInvocationSource invocation_source) {
-  // The UI should only show if the tab is in the foreground or if the tab web
-  // contents is not in a crash state.
-  if (!tab_->IsActivated() || tab_->GetContents()->IsCrashed()) {
+  // If the eligibility checks fail, do not procced with opening any UI.
+  if (!RunLensEligibilityChecks(
+          invocation_source,
+          /*permission_granted_callback=*/base::BindRepeating(
+              &LensSearchController::StartContextualization,
+              weak_ptr_factory_.GetWeakPtr(), invocation_source))) {
     return;
   }
 
-  // Exit early if the Lens feature is already active.
-  if (state() != State::kOff) {
-    return;
-  }
   state_ = State::kInitializing;
 
   // Create the query controller to be used for the current invocation.
   CHECK(!lens_overlay_query_controller_);
   lens_overlay_query_controller_ = CreateLensQueryController(invocation_source);
-
-  // TODO(crbug.com/404941800): Add logic based on this classes state once the
-  // state machine is available.
   // TODO(crbug.com/404941800): This flow should not start the overlay once
   // contextualization is separated from the overlay.
   lens_overlay_controller_->StartContextualizationWithoutOverlay(
@@ -183,25 +190,23 @@
     const GURL& destination_url,
     AutocompleteMatchType::Type match_type,
     bool is_zero_prefix_suggestion) {
-  // The UI should only show if the tab is in the foreground or if the tab web
-  // contents is not in a crash state.
-  if (!tab_->IsActivated() || tab_->GetContents()->IsCrashed()) {
-    return;
-  }
-
-  // Exit early if the Lens feature is already active.
-  if (state() != State::kOff) {
-    return;
-  }
-  state_ = State::kInitializing;
-
   // TODO(crbug.com/402497756): For prototyping, reusing the existing
   // omnibox entry point. However, for production, create a new invocation
   // source for this new entry point.
   lens::LensOverlayInvocationSource invocation_source =
       lens::LensOverlayInvocationSource::kOmnibox;
 
-  // Create the query controller to be used for the current invocation.
+  // If the eligibility checks fail, do not procced with opening any UI.
+  if (!RunLensEligibilityChecks(
+          invocation_source,
+          /*permission_granted_callback=*/base::BindRepeating(
+              &LensSearchController::IssueContextualSearchRequest,
+              weak_ptr_factory_.GetWeakPtr(), destination_url, match_type,
+              is_zero_prefix_suggestion))) {
+    return;
+  }
+  state_ = State::kInitializing;
+
   CHECK(!lens_overlay_query_controller_);
   lens_overlay_query_controller_ = CreateLensQueryController(invocation_source);
 
@@ -269,6 +274,12 @@
   return lens_overlay_controller_.get();
 }
 
+const LensOverlayController* LensSearchController::lens_overlay_controller()
+    const {
+  CheckInitialized(initialized_);
+  return lens_overlay_controller_.get();
+}
+
 lens::LensOverlaySidePanelCoordinator*
 LensSearchController::lens_overlay_side_panel_coordinator() {
   CheckInitialized(initialized_);
@@ -292,6 +303,12 @@
   return nullptr;
 }
 
+lens::LensSearchContextualizationController*
+LensSearchController::lens_search_contextualization_controller() {
+  CheckInitialized(initialized_);
+  return lens_contextualization_controller_.get();
+}
+
 std::unique_ptr<LensOverlayController>
 LensSearchController::CreateLensOverlayController(
     tabs::TabInterface* tab,
@@ -338,6 +355,11 @@
   return std::make_unique<lens::LensSearchboxController>(this);
 }
 
+std::unique_ptr<lens::LensSearchContextualizationController>
+LensSearchController::CreateLensSearchContextualizationController() {
+  return std::make_unique<lens::LensSearchContextualizationController>(this);
+}
+
 void LensSearchController::CreatePageContextEligibilityAPI() {
   // Post to a background thread to avoid blocking the set up of the overlay.
   base::ThreadPool::PostTaskAndReplyWithResult(
@@ -377,6 +399,38 @@
       gen204_controller_.get());
 }
 
+bool LensSearchController::RunLensEligibilityChecks(
+    lens::LensOverlayInvocationSource invocation_source,
+    base::RepeatingClosure permission_granted_callback) {
+  // The UI should only show if the tab is in the foreground or if the tab web
+  // contents is not in a crash state.
+  if (!tab_->IsActivated() || tab_->GetContents()->IsCrashed()) {
+    return false;
+  }
+
+  // Exit early if the Lens feature is already active.
+  if (state() != State::kOff) {
+    return false;
+  }
+
+  // If the user hasn't granted permission, request user permission before
+  // showing the UI.
+  if (!lens::CanSharePageScreenshotWithLensOverlay(pref_service_) ||
+      (lens::features::IsLensOverlayContextualSearchboxEnabled() &&
+       !lens::CanSharePageContentWithLensOverlay(pref_service_))) {
+    if (!lens_permission_bubble_controller_) {
+      lens_permission_bubble_controller_ =
+          std::make_unique<lens::LensPermissionBubbleController>(
+              *tab_, pref_service_, invocation_source);
+    }
+    lens_permission_bubble_controller_->RequestPermission(
+        tab_->GetContents(), std::move(permission_granted_callback));
+    return false;
+  }
+
+  return true;
+}
+
 void LensSearchController::NotifyOverlayOpened() {
   CHECK(state() == State::kInitializing);
   state_ = State::kActive;
@@ -385,6 +439,7 @@
 void LensSearchController::CloseLensPart2() {
   // Cleanup the query controller.
   lens_overlay_query_controller_.reset();
+  lens_permission_bubble_controller_.reset();
   // Let the controllers know to cleanup.
   lens_searchbox_controller_->CloseUI();
   state_ = State::kOff;
@@ -422,3 +477,56 @@
     const std::string& thumbnail_bytes) {
   lens_searchbox_controller_->HandleThumbnailCreated(thumbnail_bytes);
 }
+
+void LensSearchController::TabForegrounded(tabs::TabInterface* tab) {
+  // Ignore the event if the overlay is not backgrounded.
+  if (state_ != State::kBackground) {
+    return;
+  }
+
+  // Notify the overlay controller of the tab foregrounded event so it can
+  // restore to the previous state.
+  lens_overlay_controller_->TabForegrounded(tab);
+
+  state_ = State::kActive;
+}
+
+void LensSearchController::TabWillEnterBackground(tabs::TabInterface* tab) {
+  if (state_ == State::kOff) {
+    return;
+  }
+
+  // Ignore the event if the overlay is already backgrounded.
+  if (state_ == State::kBackground) {
+    return;
+  }
+
+  // Notify the overlay controller of the tab will enter background event so
+  // it can hide the overlay.
+  lens_overlay_controller_->TabWillEnterBackground(tab);
+
+  state_ = State::kBackground;
+}
+
+void LensSearchController::WillDiscardContents(
+    tabs::TabInterface* tab,
+    content::WebContents* old_contents,
+    content::WebContents* new_contents) {
+  // Background tab contents discarded.
+  CloseLensSync(lens::LensOverlayDismissalSource::kTabContentsDiscarded);
+}
+
+void LensSearchController::WillDetach(tabs::TabInterface* tab,
+                                      tabs::TabInterface::DetachReason reason) {
+  // When dragging a tab into a new window, all window-specific state must be
+  // reset. As this flow is not fully functional, close the overlay regardless
+  // of `reason`. https://crbug.com/342921671.
+  switch (reason) {
+    case tabs::TabInterface::DetachReason::kDelete:
+      CloseLensSync(lens::LensOverlayDismissalSource::kTabClosed);
+      return;
+    case tabs::TabInterface::DetachReason::kInsertIntoOtherWindow:
+      CloseLensSync(lens::LensOverlayDismissalSource::kTabDragNewWindow);
+      return;
+  }
+}
diff --git a/chrome/browser/ui/lens/lens_search_controller.h b/chrome/browser/ui/lens/lens_search_controller.h
index d4beb3ca..f4af98a 100644
--- a/chrome/browser/ui/lens/lens_search_controller.h
+++ b/chrome/browser/ui/lens/lens_search_controller.h
@@ -10,8 +10,8 @@
 #include "base/memory/raw_ptr.h"
 #include "base/memory/weak_ptr.h"
 #include "chrome/browser/lens/core/mojom/geometry.mojom.h"
-#include "chrome/browser/ui/lens/lens_overlay_query_controller.h"
 #include "chrome/browser/ui/lens/lens_overlay_controller.h"
+#include "chrome/browser/ui/lens/lens_overlay_query_controller.h"
 #include "components/lens/lens_overlay_dismissal_source.h"
 #include "components/lens/lens_overlay_invocation_source.h"
 #include "components/omnibox/browser/autocomplete_match_type.h"
@@ -26,7 +26,9 @@
 namespace lens {
 class LensOverlayGen204Controller;
 class LensOverlaySidePanelCoordinator;
+class LensPermissionBubbleController;
 class LensSearchboxController;
+class LensSearchContextualizationController;
 }  // namespace lens
 
 namespace variations {
@@ -92,7 +94,7 @@
 
   // Convenience method for calling OpenLensOverlayWithPendingRegion, that will
   // convert the bounds into a CenterRotated Box to pass to the overlay.
-  void OpenLensOverlayWithPendingRegion(
+  void OpenLensOverlayWithPendingRegionFromBounds(
       lens::LensOverlayInvocationSource invocation_source,
       const gfx::Rect& tab_bounds,
       const gfx::Rect& view_bounds,
@@ -137,6 +139,7 @@
 
   // Returns the LensOverlayController.
   LensOverlayController* lens_overlay_controller();
+  const LensOverlayController* lens_overlay_controller() const;
 
   // Returns the LensOverlayQueryController.
   lens::LensOverlayQueryController* lens_overlay_query_controller();
@@ -149,6 +152,10 @@
 
   optimization_guide::PageContextEligibility* page_context_eligibility();
 
+  // Returns the LensSearchContextualizationController.
+  lens::LensSearchContextualizationController*
+  lens_search_contextualization_controller();
+
   // Testing function for setting the page context eligibility API for this
   // controller.
   void set_page_context_eligibility_for_testing(
@@ -156,6 +163,11 @@
     page_context_eligibility_ = page_context_eligibility;
   }
 
+  lens::LensPermissionBubbleController*
+  get_lens_permission_bubble_controller_for_testing() {
+    return lens_permission_bubble_controller_.get();
+  }
+
  protected:
   friend class LensOverlayController;
 
@@ -191,11 +203,16 @@
   virtual std::unique_ptr<lens::LensOverlaySidePanelCoordinator>
   CreateLensOverlaySidePanelCoordinator();
 
-  // Override these methods to be able to track calls made to the side panel
-  // coordinator.
+  // Override these methods to be able to track calls made to the searchbox
+  // controller.
   virtual std::unique_ptr<lens::LensSearchboxController>
   CreateLensSearchboxController();
 
+  // Override these methods to be able to track calls made to the
+  // contextualization controller.
+  virtual std::unique_ptr<lens::LensSearchContextualizationController>
+  CreateLensSearchContextualizationController();
+
   // Called by the Lens overlay when it has finished opening and has moved to
   // the kOverlay state. This is how this class knows it can move into kActive
   // state.
@@ -223,6 +240,11 @@
     // One or more Lens features are active on this tab.
     kActive,
 
+    // The UI has been made inactive / backgrounded and is hidden. This differs
+    // from kSuspended as the overlay and web view are not freed and could be
+    // immediately reshown.
+    kBackground,
+
     // The controller is in the process of closing all dependencies and cleaning
     // up. Will soon be kOff.
     kClosing,
@@ -241,6 +263,14 @@
   std::unique_ptr<lens::LensOverlayQueryController> CreateLensQueryController(
       lens::LensOverlayInvocationSource invocation_source);
 
+  // Runs the eligibility checks necessary for Lens to open on this tab. If the
+  // user has not granted permission to use Lens on this tab, the permission
+  // request will be shown and callback will be called after the user accepts.
+  // Returns true if the checks pass and its safe to open Lens, false otherwise.
+  bool RunLensEligibilityChecks(
+      lens::LensOverlayInvocationSource invocation_source,
+      base::RepeatingClosure permission_granted_callback);
+
   // Callback used by the query controller to notify the search controller of
   // the response to the initial image upload request.
   void HandleStartQueryResponse(
@@ -273,6 +303,21 @@
   // the progress of the page content upload.
   void HandlePageContentUploadProgress(uint64_t position, uint64_t total);
 
+  // Called when the associated tab enters the foreground.
+  void TabForegrounded(tabs::TabInterface* tab);
+
+  // Called when the associated tab will enter the background.
+  void TabWillEnterBackground(tabs::TabInterface* tab);
+
+  // Called when the tab's WebContents is discarded.
+  void WillDiscardContents(tabs::TabInterface* tab,
+                           content::WebContents* old_contents,
+                           content::WebContents* new_contents);
+
+  // Called when the tab will be removed from the window.
+  void WillDetach(tabs::TabInterface* tab,
+                  tabs::TabInterface::DetachReason reason);
+
   // Whether the LensSearchController has been initialized. Meaning, all the
   // dependencies have been initialized and the controller is ready to use.
   bool initialized_ = false;
@@ -285,6 +330,9 @@
   std::unique_ptr<lens::LensOverlayQueryController>
       lens_overlay_query_controller_;
 
+  std::unique_ptr<lens::LensPermissionBubbleController>
+      lens_permission_bubble_controller_;
+
   // The overlay controller for the Lens Search feature on this tab.
   std::unique_ptr<LensOverlayController> lens_overlay_controller_;
 
@@ -302,6 +350,13 @@
   // interactions, without a dependency on the overlay controller.
   std::unique_ptr<lens::LensSearchboxController> lens_searchbox_controller_;
 
+  // The contextualization controller for the Lens Search feature on this tab.
+  std::unique_ptr<lens::LensSearchContextualizationController>
+      lens_contextualization_controller_;
+
+  // Holds subscriptions for TabInterface callbacks.
+  std::vector<base::CallbackListSubscription> tab_subscriptions_;
+
   // The page context eligibility API if it has been fetched. Can be nullptr.
   raw_ptr<optimization_guide::PageContextEligibility> page_context_eligibility_;
 
diff --git a/chrome/browser/ui/page_info/BUILD.gn b/chrome/browser/ui/page_info/BUILD.gn
index cc1b6e3..5ad86d7 100644
--- a/chrome/browser/ui/page_info/BUILD.gn
+++ b/chrome/browser/ui/page_info/BUILD.gn
@@ -39,7 +39,10 @@
     "chrome_page_info_delegate.cc",
     "chrome_page_info_ui_delegate.cc",
   ]
-  deps = [ "//chrome/browser/privacy_sandbox:headers" ]
+  deps = [
+    "//chrome/browser/privacy_sandbox:headers",
+    "//chrome/browser/serial",
+  ]
 
   if (is_android) {
     sources += [ "chrome_page_info_client.cc" ]
diff --git a/chrome/browser/ui/performance_controls/performance_controls_metrics_unittest.cc b/chrome/browser/ui/performance_controls/performance_controls_metrics_unittest.cc
index 782c4b2..f9c4b12 100644
--- a/chrome/browser/ui/performance_controls/performance_controls_metrics_unittest.cc
+++ b/chrome/browser/ui/performance_controls/performance_controls_metrics_unittest.cc
@@ -15,6 +15,7 @@
 #include "chrome/browser/performance_manager/public/user_tuning/performance_detection_manager.h"
 #include "chrome/browser/ui/performance_controls/performance_intervention_button_controller.h"
 #include "chrome/common/pref_names.h"
+#include "chrome/test/base/scoped_testing_local_state.h"
 #include "chrome/test/base/testing_browser_process.h"
 #include "chrome/test/base/testing_profile.h"
 #include "components/performance_manager/public/features.h"
@@ -34,20 +35,17 @@
       : RenderViewHostTestHarness(
             base::test::TaskEnvironment::TimeSource::MOCK_TIME) {}
 
-  void SetUp() override {
-    content::RenderViewHostTestHarness::SetUp();
-    PerformanceInterventionMetricsReporter::RegisterLocalStatePrefs(
-        prefs()->registry());
-  }
-
   std::unique_ptr<content::BrowserContext> CreateBrowserContext() override {
     return std::make_unique<TestingProfile>();
   }
 
-  TestingPrefServiceSimple* prefs() { return &prefs_; }
+  TestingPrefServiceSimple* prefs() {
+    return scoped_testing_local_state_.Get();
+  }
 
  private:
-  TestingPrefServiceSimple prefs_;
+  ScopedTestingLocalState scoped_testing_local_state_{
+      TestingBrowserProcess::GetGlobal()};
 };
 
 TEST_F(PerformanceControlsMetricsTest, DailyMetricsResets) {
@@ -143,14 +141,6 @@
             kPerformanceInterventionNotificationImprovements);
 
     PerformanceControlsMetricsTest::SetUp();
-    performance_manager::user_tuning::prefs::RegisterLocalStatePrefs(
-        prefs()->registry());
-    TestingBrowserProcess::GetGlobal()->SetLocalState(prefs());
-  }
-
-  void TearDown() override {
-    TestingBrowserProcess::GetGlobal()->SetLocalState(nullptr);
-    PerformanceControlsMetricsTest::TearDown();
   }
 
  private:
diff --git a/chrome/browser/ui/performance_controls/performance_intervention_button_controller_unittest.cc b/chrome/browser/ui/performance_controls/performance_intervention_button_controller_unittest.cc
index 72b8e1d2..ff87109 100644
--- a/chrome/browser/ui/performance_controls/performance_intervention_button_controller_unittest.cc
+++ b/chrome/browser/ui/performance_controls/performance_intervention_button_controller_unittest.cc
@@ -42,9 +42,6 @@
           {}},
          {feature_engagement::kIPHPerformanceInterventionDialogFeature,
           params}});
-    performance_manager::user_tuning::prefs::RegisterLocalStatePrefs(
-        local_state_.registry());
-    TestingBrowserProcess::GetGlobal()->SetLocalState(&local_state_);
 
     tracker_ = feature_engagement::CreateTestTracker();
     base::RunLoop run_loop;
@@ -60,10 +57,6 @@
         nullptr, nullptr);
   }
 
-  void TearDown() override {
-    TestingBrowserProcess::GetGlobal()->SetLocalState(nullptr);
-  }
-
   base::test::SingleThreadTaskEnvironment& task_environment() {
     return task_environment_;
   }
@@ -120,7 +113,8 @@
   feature_engagement::test::ScopedIphFeatureList feature_list_;
   base::test::SingleThreadTaskEnvironment task_environment_{
       base::test::TaskEnvironment::TimeSource::MOCK_TIME};
-  TestingPrefServiceSimple local_state_;
+  ScopedTestingLocalState scoped_testing_local_state_{
+      TestingBrowserProcess::GetGlobal()};
   std::unique_ptr<PerformanceInterventionButtonController> controller_;
   std::unique_ptr<feature_engagement::Tracker> tracker_;
 };
diff --git a/chrome/browser/ui/safety_hub/revoked_permissions_service_unittest.cc b/chrome/browser/ui/safety_hub/revoked_permissions_service_unittest.cc
index a8dddec..4b26e44 100644
--- a/chrome/browser/ui/safety_hub/revoked_permissions_service_unittest.cc
+++ b/chrome/browser/ui/safety_hub/revoked_permissions_service_unittest.cc
@@ -60,6 +60,10 @@
 #include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
+#if BUILDFLAG(IS_CHROMEOS)
+#include "chrome/test/base/scoped_testing_local_state.h"
+#endif  // BUILDFLAG(IS_CHROMEOS)
+
 using ::testing::UnorderedElementsAre;
 
 namespace {
@@ -567,18 +571,10 @@
         fake_database_manager_.get());
     TestingBrowserProcess::GetGlobal()->SetSafeBrowsingService(
         safe_browsing_factory_->CreateSafeBrowsingService());
-#if BUILDFLAG(IS_CHROMEOS)
-    // Local state is needed to construct ProxyConfigService, which is a
-    // dependency of PingManager on ChromeOS.
-    TestingBrowserProcess::GetGlobal()->SetLocalState(profile()->GetPrefs());
-#endif
   }
 
   void TearDownSafeBrowsingService() {
     TestingBrowserProcess::GetGlobal()->SetSafeBrowsingService(nullptr);
-#if BUILDFLAG(IS_CHROMEOS)
-    TestingBrowserProcess::GetGlobal()->SetLocalState(nullptr);
-#endif
   }
 
   bool IsUrlInContentSettings(ContentSettingsForOneType content_settings,
@@ -596,6 +592,13 @@
     return false;
   }
 
+#if BUILDFLAG(IS_CHROMEOS)
+  // Local state is needed to construct ProxyConfigService, which is a
+  // dependency of PingManager on ChromeOS.
+  ScopedTestingLocalState scoped_testing_local_state_{
+      TestingBrowserProcess::GetGlobal()};
+#endif  // BUILDFLAG(IS_CHROMEOS)
+
   base::SimpleTestClock clock_;
   uint8_t callback_count_;
   base::test::ScopedFeatureList feature_list_;
diff --git a/chrome/browser/ui/serial/BUILD.gn b/chrome/browser/ui/serial/BUILD.gn
index f36a0866..485e66b 100644
--- a/chrome/browser/ui/serial/BUILD.gn
+++ b/chrome/browser/ui/serial/BUILD.gn
@@ -2,15 +2,69 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
-static_library("test_support") {
+source_set("serial") {
+  sources = [ "serial_chooser_controller.h" ]
+  public_deps = [
+    "//base",
+    "//chrome/browser/serial",
+    "//components/permissions",
+    "//content/public/browser",
+    "//device/bluetooth",
+    "//services/device/public/mojom",
+    "//third_party/blink/public/common",
+    "//url",
+  ]
+}
+
+source_set("impl") {
+  sources = [ "serial_chooser_controller.cc" ]
+  deps = [
+    ":serial",
+    "//base",
+    "//chrome/app:branded_strings",
+    "//chrome/app:generated_resources",
+    "//chrome/browser/profiles:profile",
+    "//chrome/common",
+    "//components/strings:components_strings",
+    "//content/public/browser",
+    "//device/bluetooth",
+    "//device/bluetooth/public/cpp",
+    "//services/device/public/cpp/bluetooth",
+    "//services/device/public/mojom",
+    "//ui/base",
+  ]
+  if (is_chromeos) {
+    deps += [ "//ash/webui/settings/public/constants:mojom" ]
+  }
+
+  public_deps = [ "//chrome/browser:browser_public_dependencies" ]
+}
+
+source_set("test_support") {
   testonly = true
   sources = [
     "mock_serial_chooser_controller.cc",
     "mock_serial_chooser_controller.h",
   ]
+  deps = [ "//chrome/test:test_support" ]
+}
 
+source_set("unit_tests") {
+  testonly = true
+  sources = [ "serial_chooser_controller_unittest.cc" ]
   deps = [
-    "//chrome/browser/ui",
-    "//testing/gmock",
+    ":serial",
+    "//base/test:test_support",
+    "//chrome/browser/serial",
+    "//chrome/test:test_support",
+    "//components/permissions:test_support",
+    "//device/bluetooth",
+    "//device/bluetooth:mocks",
+    "//device/bluetooth/public/cpp",
+    "//mojo/public/cpp/bindings",
+    "//services/device/public/cpp:test_support",
+    "//services/device/public/cpp/bluetooth",
+    "//services/device/public/mojom",
+    "//third_party/blink/public/common",
   ]
 }
diff --git a/chrome/browser/ui/tabs/public/tab_features.h b/chrome/browser/ui/tabs/public/tab_features.h
index 313a7903..364ae0fe 100644
--- a/chrome/browser/ui/tabs/public/tab_features.h
+++ b/chrome/browser/ui/tabs/public/tab_features.h
@@ -212,6 +212,7 @@
   }
 
   LensOverlayController* lens_overlay_controller();
+  const LensOverlayController* lens_overlay_controller() const;
 
   PwaInstallPageActionController* pwa_install_page_action_controller() {
     return pwa_install_page_action_controller_.get();
diff --git a/chrome/browser/ui/tabs/saved_tab_groups/collaboration_messaging_observer.cc b/chrome/browser/ui/tabs/saved_tab_groups/collaboration_messaging_observer.cc
index 96ba3e0..625e60b6 100644
--- a/chrome/browser/ui/tabs/saved_tab_groups/collaboration_messaging_observer.cc
+++ b/chrome/browser/ui/tabs/saved_tab_groups/collaboration_messaging_observer.cc
@@ -4,6 +4,9 @@
 
 #include "chrome/browser/ui/tabs/saved_tab_groups/collaboration_messaging_observer.h"
 
+#include <set>
+
+#include "base/uuid.h"
 #include "chrome/browser/collaboration/collaboration_service_factory.h"
 #include "chrome/browser/collaboration/messaging/messaging_backend_service_factory.h"
 #include "chrome/browser/tab_group_sync/tab_group_sync_service_factory.h"
@@ -257,6 +260,11 @@
                                            std::move(success_callback));
 }
 
+void CollaborationMessagingObserver::HideInstantaneousMessage(
+    const std::set<base::Uuid>& message_ids) {
+  // TODO(crbug.com/416265338): Implement this.
+}
+
 void CollaborationMessagingObserver::ReopenTabForCurrentInstantMessage() {
   CHECK(instant_message_queue_processor_.IsMessageShowing());
 
diff --git a/chrome/browser/ui/tabs/saved_tab_groups/collaboration_messaging_observer.h b/chrome/browser/ui/tabs/saved_tab_groups/collaboration_messaging_observer.h
index 0a2c38b..e06c262 100644
--- a/chrome/browser/ui/tabs/saved_tab_groups/collaboration_messaging_observer.h
+++ b/chrome/browser/ui/tabs/saved_tab_groups/collaboration_messaging_observer.h
@@ -5,8 +5,11 @@
 #ifndef CHROME_BROWSER_UI_TABS_SAVED_TAB_GROUPS_COLLABORATION_MESSAGING_OBSERVER_H_
 #define CHROME_BROWSER_UI_TABS_SAVED_TAB_GROUPS_COLLABORATION_MESSAGING_OBSERVER_H_
 
+#include <set>
+
 #include "base/memory/raw_ptr.h"
 #include "base/scoped_observation.h"
+#include "base/uuid.h"
 #include "chrome/browser/ui/tabs/saved_tab_groups/instant_message_queue_processor.h"
 #include "components/collaboration/public/messaging/messaging_backend_service.h"
 #include "components/keyed_service/core/keyed_service.h"
@@ -88,6 +91,8 @@
   void DisplayInstantaneousMessage(
       InstantMessage message,
       InstantMessageSuccessCallback success_callback) override;
+  void HideInstantaneousMessage(
+      const std::set<base::Uuid>& message_ids) override;
 
  private:
   // Finds the tab group designated by this message and sets/hides an
diff --git a/chrome/browser/ui/tabs/tab_features.cc b/chrome/browser/ui/tabs/tab_features.cc
index 8fc18dd0..a7b2a8dc 100644
--- a/chrome/browser/ui/tabs/tab_features.cc
+++ b/chrome/browser/ui/tabs/tab_features.cc
@@ -120,6 +120,13 @@
              : nullptr;
 }
 
+const LensOverlayController* TabFeatures::lens_overlay_controller() const {
+  // LensSearchController won't exist on non-normal windows.
+  return lens_search_controller_
+             ? lens_search_controller_->lens_overlay_controller()
+             : nullptr;
+}
+
 void TabFeatures::Init(TabInterface& tab, Profile* profile) {
   CHECK(!initialized_);
   initialized_ = true;
diff --git a/chrome/browser/ui/tabs/tab_model.cc b/chrome/browser/ui/tabs/tab_model.cc
index f1b2cd5..f1de80e 100644
--- a/chrome/browser/ui/tabs/tab_model.cc
+++ b/chrome/browser/ui/tabs/tab_model.cc
@@ -273,6 +273,10 @@
   return tab_features_.get();
 }
 
+const tabs::TabFeatures* TabModel::GetTabFeatures() const {
+  return tab_features_.get();
+}
+
 bool TabModel::IsPinned() const {
   return pinned_;
 }
diff --git a/chrome/browser/ui/tabs/tab_model.h b/chrome/browser/ui/tabs/tab_model.h
index 7f3881b..84d48190 100644
--- a/chrome/browser/ui/tabs/tab_model.h
+++ b/chrome/browser/ui/tabs/tab_model.h
@@ -140,6 +140,7 @@
   BrowserWindowInterface* GetBrowserWindowInterface() override;
   const BrowserWindowInterface* GetBrowserWindowInterface() const override;
   tabs::TabFeatures* GetTabFeatures() override;
+  const tabs::TabFeatures* GetTabFeatures() const override;
   bool IsPinned() const override;
   bool IsSplit() const override;
   std::optional<tab_groups::TabGroupId> GetGroup() const override;
diff --git a/chrome/browser/ui/tabs/tab_strip_api/BUILD.gn b/chrome/browser/ui/tabs/tab_strip_api/BUILD.gn
index 42f4fe9..eadb3ff9 100644
--- a/chrome/browser/ui/tabs/tab_strip_api/BUILD.gn
+++ b/chrome/browser/ui/tabs/tab_strip_api/BUILD.gn
@@ -14,6 +14,8 @@
     "//url/mojom:url_mojom_gurl",
   ]
 
+  webui_module_path = "/"
+
   cpp_typemaps = [
     {
       types = [
@@ -53,10 +55,14 @@
 }
 
 source_set("tab_strip_api") {
-  sources = [ "tab_strip_service_impl.h" ]
+  sources = [
+    "tab_strip_service_impl.h",
+    "tab_strip_service_register.h",
+  ]
 
   public_deps = [
     ":mojom",
+    "adapters",
     "//chrome/browser/ui/tabs:tab_strip_model_observer",
   ]
 }
@@ -69,6 +75,8 @@
   deps = [
     ":tab_strip_api",
     ":types",
+    "adapters:impl",
+    "converters",
     "//chrome/browser/ui",
     "//chrome/browser/ui/browser_window:browser_window",
     "//chrome/browser/ui/tabs:tab_strip",
@@ -94,6 +102,7 @@
   deps = [
     ":impl",
     ":tab_strip_api",
+    "converters:unit_tests",
     "//base/test:test_support",
     "//chrome/browser/ui/browser_window:browser_window",
     "//chrome/browser/ui/tabs:tab_strip",
diff --git a/chrome/browser/ui/tabs/tab_strip_api/adapters/BUILD.gn b/chrome/browser/ui/tabs/tab_strip_api/adapters/BUILD.gn
new file mode 100644
index 0000000..7474679a
--- /dev/null
+++ b/chrome/browser/ui/tabs/tab_strip_api/adapters/BUILD.gn
@@ -0,0 +1,34 @@
+# Copyright 2025 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+source_set("adapters") {
+  sources = [
+    "browser_adapter.h",
+    "tab_strip_model_adapter.h",
+  ]
+
+  public_deps = [
+    "//chrome/browser/ui/tabs",
+    "//chrome/browser/ui/tabs:tab_strip_model_observer",
+    "//content/public/browser",
+  ]
+}
+
+source_set("impl") {
+  sources = [
+    "browser_adapter_impl.cc",
+    "browser_adapter_impl.h",
+    "tab_strip_model_adapter_impl.cc",
+    "tab_strip_model_adapter_impl.h",
+  ]
+
+  deps = [
+    ":adapters",
+    "//chrome/browser/ui",
+    "//chrome/browser/ui/browser_window:browser_window",
+    "//chrome/browser/ui/tabs:tab_strip",
+    "//chrome/browser/ui/tabs:tab_strip_model_observer",
+    "//content/public/browser",
+  ]
+}
diff --git a/chrome/browser/ui/tabs/tab_strip_api/adapters/browser_adapter.h b/chrome/browser/ui/tabs/tab_strip_api/adapters/browser_adapter.h
new file mode 100644
index 0000000..5468739a
--- /dev/null
+++ b/chrome/browser/ui/tabs/tab_strip_api/adapters/browser_adapter.h
@@ -0,0 +1,24 @@
+// Copyright 2025 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_UI_TABS_TAB_STRIP_API_ADAPTERS_BROWSER_ADAPTER_H_
+#define CHROME_BROWSER_UI_TABS_TAB_STRIP_API_ADAPTERS_BROWSER_ADAPTER_H_
+
+#include "content/public/browser/web_contents.h"
+#include "url/gurl.h"
+
+namespace tabs_api {
+
+// Pull out a subset of browser APIs into an adapter object. This allows us
+// to more easily control dependencies when testing.
+class BrowserAdapter {
+ public:
+  virtual ~BrowserAdapter() {}
+
+  virtual content::WebContents* AddTabAt(const GURL& url, int index) = 0;
+};
+
+}  // namespace tabs_api
+
+#endif  // CHROME_BROWSER_UI_TABS_TAB_STRIP_API_ADAPTERS_BROWSER_ADAPTER_H_
diff --git a/chrome/browser/ui/tabs/tab_strip_api/adapters/browser_adapter_impl.cc b/chrome/browser/ui/tabs/tab_strip_api/adapters/browser_adapter_impl.cc
new file mode 100644
index 0000000..897f7ad
--- /dev/null
+++ b/chrome/browser/ui/tabs/tab_strip_api/adapters/browser_adapter_impl.cc
@@ -0,0 +1,16 @@
+// Copyright 2025 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/ui/tabs/tab_strip_api/adapters/browser_adapter_impl.h"
+
+#include "chrome/browser/ui/browser_tabstrip.h"
+
+namespace tabs_api {
+
+content::WebContents* BrowserAdapterImpl::AddTabAt(const GURL& url, int index) {
+  return chrome::AddAndReturnTabAt(browser_->GetBrowserForMigrationOnly(), url,
+                                   index, true);
+}
+
+}  // namespace tabs_api
diff --git a/chrome/browser/ui/tabs/tab_strip_api/adapters/browser_adapter_impl.h b/chrome/browser/ui/tabs/tab_strip_api/adapters/browser_adapter_impl.h
new file mode 100644
index 0000000..d40b4b88
--- /dev/null
+++ b/chrome/browser/ui/tabs/tab_strip_api/adapters/browser_adapter_impl.h
@@ -0,0 +1,31 @@
+// Copyright 2025 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_UI_TABS_TAB_STRIP_API_ADAPTERS_BROWSER_ADAPTER_IMPL_H_
+#define CHROME_BROWSER_UI_TABS_TAB_STRIP_API_ADAPTERS_BROWSER_ADAPTER_IMPL_H_
+
+#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
+#include "chrome/browser/ui/tabs/tab_strip_api/adapters/browser_adapter.h"
+
+namespace tabs_api {
+
+// A simple forwarder proxy for the browser. Avoid adding logic to this class.
+// It should *only* forward requests to the browser window.
+class BrowserAdapterImpl : public BrowserAdapter {
+ public:
+  explicit BrowserAdapterImpl(BrowserWindowInterface* browser)
+      : browser_(browser) {}
+  BrowserAdapterImpl(const BrowserAdapterImpl&) = delete;
+  BrowserAdapterImpl operator=(const BrowserAdapterImpl&) = delete;
+  ~BrowserAdapterImpl() override = default;
+
+  content::WebContents* AddTabAt(const GURL& url, int index) override;
+
+ private:
+  raw_ptr<BrowserWindowInterface> browser_;
+};
+
+}  // namespace tabs_api
+
+#endif  // CHROME_BROWSER_UI_TABS_TAB_STRIP_API_ADAPTERS_BROWSER_ADAPTER_IMPL_H_
diff --git a/chrome/browser/ui/tabs/tab_strip_api/adapters/tab_strip_model_adapter.h b/chrome/browser/ui/tabs/tab_strip_api/adapters/tab_strip_model_adapter.h
new file mode 100644
index 0000000..eae1e62
--- /dev/null
+++ b/chrome/browser/ui/tabs/tab_strip_api/adapters/tab_strip_model_adapter.h
@@ -0,0 +1,29 @@
+// Copyright 2025 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_UI_TABS_TAB_STRIP_API_ADAPTERS_TAB_STRIP_MODEL_ADAPTER_H_
+#define CHROME_BROWSER_UI_TABS_TAB_STRIP_API_ADAPTERS_TAB_STRIP_MODEL_ADAPTER_H_
+
+#include "chrome/browser/ui/tabs/tab_renderer_data.h"
+#include "chrome/browser/ui/tabs/tab_strip_model_observer.h"
+#include "components/tabs/public/tab_interface.h"
+
+namespace tabs_api {
+
+// Tab strip has a large API service that is difficult to implement under test.
+// We only need a subset of the AP, so an adapter is used to proxy those
+// methods. This makes it easier to swap in a fake for test.
+class TabStripModelAdapter {
+ public:
+  virtual ~TabStripModelAdapter() {}
+
+  virtual void AddObserver(TabStripModelObserver* observer) = 0;
+  virtual void RemoveObserver(TabStripModelObserver* observer) = 0;
+  virtual std::vector<tabs::TabHandle> GetTabs() = 0;
+  virtual TabRendererData GetTabRendererData(int index) = 0;
+};
+
+}  // namespace tabs_api
+
+#endif  // CHROME_BROWSER_UI_TABS_TAB_STRIP_API_ADAPTERS_TAB_STRIP_MODEL_ADAPTER_H_
diff --git a/chrome/browser/ui/tabs/tab_strip_api/adapters/tab_strip_model_adapter_impl.cc b/chrome/browser/ui/tabs/tab_strip_api/adapters/tab_strip_model_adapter_impl.cc
new file mode 100644
index 0000000..4395acfd
--- /dev/null
+++ b/chrome/browser/ui/tabs/tab_strip_api/adapters/tab_strip_model_adapter_impl.cc
@@ -0,0 +1,31 @@
+// Copyright 2025 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/ui/tabs/tab_strip_api/adapters/tab_strip_model_adapter_impl.h"
+
+#include "components/tabs/public/tab_interface.h"
+
+namespace tabs_api {
+
+void TabStripModelAdapterImpl::AddObserver(TabStripModelObserver* observer) {
+  tab_strip_model_->AddObserver(observer);
+}
+
+void TabStripModelAdapterImpl::RemoveObserver(TabStripModelObserver* observer) {
+  tab_strip_model_->RemoveObserver(observer);
+}
+
+std::vector<tabs::TabHandle> TabStripModelAdapterImpl::GetTabs() {
+  std::vector<tabs::TabHandle> tabs;
+  for (auto* tab : *tab_strip_model_) {
+    tabs.push_back(tab->GetHandle());
+  }
+  return tabs;
+}
+
+TabRendererData TabStripModelAdapterImpl::GetTabRendererData(int index) {
+  return TabRendererData::FromTabInModel(tab_strip_model_, index);
+}
+
+}  // namespace tabs_api
diff --git a/chrome/browser/ui/tabs/tab_strip_api/adapters/tab_strip_model_adapter_impl.h b/chrome/browser/ui/tabs/tab_strip_api/adapters/tab_strip_model_adapter_impl.h
new file mode 100644
index 0000000..127e4e9
--- /dev/null
+++ b/chrome/browser/ui/tabs/tab_strip_api/adapters/tab_strip_model_adapter_impl.h
@@ -0,0 +1,34 @@
+// Copyright 2025 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_UI_TABS_TAB_STRIP_API_ADAPTERS_TAB_STRIP_MODEL_ADAPTER_IMPL_H_
+#define CHROME_BROWSER_UI_TABS_TAB_STRIP_API_ADAPTERS_TAB_STRIP_MODEL_ADAPTER_IMPL_H_
+
+#include "chrome/browser/ui/tabs/tab_strip_api/adapters/tab_strip_model_adapter.h"
+#include "chrome/browser/ui/tabs/tab_strip_model.h"
+
+namespace tabs_api {
+
+// A simple forwarder proxy for the tab strip model. Avoid adding logic to this
+// class. It should *only* forward requests to the tab strip model.
+class TabStripModelAdapterImpl : public TabStripModelAdapter {
+ public:
+  explicit TabStripModelAdapterImpl(TabStripModel* tab_strip_model)
+      : tab_strip_model_(tab_strip_model) {}
+  TabStripModelAdapterImpl(const TabStripModelAdapterImpl&) = delete;
+  TabStripModelAdapterImpl operator=(const TabStripModelAdapterImpl&) = delete;
+  ~TabStripModelAdapterImpl() override {}
+
+  void AddObserver(TabStripModelObserver* observer) override;
+  void RemoveObserver(TabStripModelObserver* observer) override;
+  std::vector<tabs::TabHandle> GetTabs() override;
+  TabRendererData GetTabRendererData(int index) override;
+
+ private:
+  raw_ptr<TabStripModel> tab_strip_model_;
+};
+
+}  // namespace tabs_api
+
+#endif  // CHROME_BROWSER_UI_TABS_TAB_STRIP_API_ADAPTERS_TAB_STRIP_MODEL_ADAPTER_IMPL_H_
diff --git a/chrome/browser/ui/tabs/tab_strip_api/converters/BUILD.gn b/chrome/browser/ui/tabs/tab_strip_api/converters/BUILD.gn
new file mode 100644
index 0000000..f90cea0
--- /dev/null
+++ b/chrome/browser/ui/tabs/tab_strip_api/converters/BUILD.gn
@@ -0,0 +1,30 @@
+# Copyright 2025 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+source_set("converters") {
+  sources = [
+    "tab_converters.cc",
+    "tab_converters.h",
+  ]
+
+  deps = [
+    "//chrome/browser/ui/tabs:tab_strip",
+    "//chrome/browser/ui/tabs/tab_strip_api:mojom",
+    "//chrome/browser/ui/tabs/tab_strip_api:types",
+  ]
+}
+
+source_set("unit_tests") {
+  testonly = true
+
+  sources = [ "tab_converters_unittest.cc" ]
+
+  deps = [
+    ":converters",
+    "//chrome/browser/ui/tabs:tab_strip",
+    "//chrome/browser/ui/tabs/tab_strip_api:mojom",
+    "//chrome/browser/ui/tabs/tab_strip_api:types",
+    "//testing/gtest",
+  ]
+}
diff --git a/chrome/browser/ui/tabs/tab_strip_api/converters/tab_converters.cc b/chrome/browser/ui/tabs/tab_strip_api/converters/tab_converters.cc
new file mode 100644
index 0000000..649b24a7
--- /dev/null
+++ b/chrome/browser/ui/tabs/tab_strip_api/converters/tab_converters.cc
@@ -0,0 +1,34 @@
+// Copyright 2025 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/ui/tabs/tab_strip_api/converters/tab_converters.h"
+
+#include "base/strings/string_number_conversions.h"
+#include "base/strings/utf_string_conversions.h"
+#include "chrome/browser/ui/tabs/tab_utils.h"
+
+namespace tabs_api::converters {
+
+tabs_api::mojom::TabPtr BuildMojoTab(tabs::TabHandle handle,
+                                     const TabRendererData& data) {
+  auto result = tabs_api::mojom::Tab::New();
+
+  result->id = tabs_api::TabId(tabs_api::TabId::Type::kContent,
+                               base::NumberToString(handle.raw_value()));
+  result->title = base::UTF16ToUTF8(data.title);
+  // TODO(crbug.com/414630734). Integrate the favicon_url after it is
+  // typemapped.
+  result->url = data.visible_url;
+  result->network_state = data.network_state;
+  if (handle.Get() != nullptr) {
+    for (const auto alert_state :
+         GetTabAlertStatesForContents(handle.Get()->GetContents())) {
+      result->alert_states.push_back(alert_state);
+    }
+  }
+
+  return result;
+}
+
+}  // namespace tabs_api::converters
diff --git a/chrome/browser/ui/tabs/tab_strip_api/converters/tab_converters.h b/chrome/browser/ui/tabs/tab_strip_api/converters/tab_converters.h
new file mode 100644
index 0000000..e23c5f1
--- /dev/null
+++ b/chrome/browser/ui/tabs/tab_strip_api/converters/tab_converters.h
@@ -0,0 +1,19 @@
+// Copyright 2025 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_UI_TABS_TAB_STRIP_API_CONVERTERS_TAB_CONVERTERS_H_
+#define CHROME_BROWSER_UI_TABS_TAB_STRIP_API_CONVERTERS_TAB_CONVERTERS_H_
+
+#include "chrome/browser/ui/tabs/tab_renderer_data.h"
+#include "chrome/browser/ui/tabs/tab_strip_api/tab_strip_api.mojom.h"
+#include "components/tabs/public/tab_interface.h"
+
+namespace tabs_api::converters {
+
+tabs_api::mojom::TabPtr BuildMojoTab(tabs::TabHandle handle,
+                                     const TabRendererData& data);
+
+}  // namespace tabs_api::converters
+
+#endif  // CHROME_BROWSER_UI_TABS_TAB_STRIP_API_CONVERTERS_TAB_CONVERTERS_H_
diff --git a/chrome/browser/ui/tabs/tab_strip_api/converters/tab_converters_unittest.cc b/chrome/browser/ui/tabs/tab_strip_api/converters/tab_converters_unittest.cc
new file mode 100644
index 0000000..b679ab31
--- /dev/null
+++ b/chrome/browser/ui/tabs/tab_strip_api/converters/tab_converters_unittest.cc
@@ -0,0 +1,31 @@
+// Copyright 2025 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/ui/tabs/tab_strip_api/converters/tab_converters.h"
+
+#include "chrome/browser/ui/tabs/tab_renderer_data.h"
+#include "chrome/browser/ui/tabs/tab_strip_api/tab_id.h"
+#include "components/tabs/public/tab_interface.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "url/gurl.h"
+
+namespace tabs_api::converters {
+namespace {
+
+TEST(TabStripServiceConverters, ConvertTab) {
+  tabs::TabHandle handle(888);
+  TabRendererData data;
+  data.visible_url = GURL("http://nowhere");
+  data.title = std::u16string(u"title");
+
+  auto mojo = BuildMojoTab(handle, data);
+
+  ASSERT_EQ("888", mojo->id.Id());
+  ASSERT_EQ(TabId::Type::kContent, mojo->id.Type());
+  ASSERT_EQ(GURL("http://nowhere"), mojo->url);
+  ASSERT_EQ("title", mojo->title);
+}
+
+}  // namespace
+}  // namespace tabs_api::converters
diff --git a/chrome/browser/ui/tabs/tab_strip_api/tab_strip_api.mojom b/chrome/browser/ui/tabs/tab_strip_api/tab_strip_api.mojom
index 4a3db38..a7591fc0 100644
--- a/chrome/browser/ui/tabs/tab_strip_api/tab_strip_api.mojom
+++ b/chrome/browser/ui/tabs/tab_strip_api/tab_strip_api.mojom
@@ -82,10 +82,30 @@
   uint32 index;
 };
 
+// A snapshot of the current tabs in the tab strip.
+struct TabsSnapshot {
+  array<Tab> tabs;
+  // Updates to tabs would be sent through this update stream. Clients may
+  // subscribe to this stream to receive update events.
+  pending_receiver<TabsObserver> stream;
+};
+
 // The TabStripService is an object that lives alongside the
 // TabstripModel. It acts as the bridge between the model and any UI Dialog
 // or client.
 interface TabStripService {
+  // Gets the current state of the tab tree. This also returns a stream of
+  // future update events. Clients can implement the |TabsObserver| interface
+  // and receive all future updates from the snapshot. Note that all messages
+  // since the snapshot will be present in the stream, even if the client
+  // does not immediately register to the update stream.
+  [Sync]
+  GetTabs() => result<TabsSnapshot, mojo_base.mojom.Error>;
+
+  // Get a single tab.
+  [Sync]
+  GetTab(TabId id) => result<Tab, mojo_base.mojom.Error>;
+
   // Creates a new tab.
   // Position specifies the location of the Tab after creation. If position is
   // empty, the new tab will be appended to the end of the Tabstrip.
@@ -94,11 +114,6 @@
   [Sync]
   CreateTabAt(Position? pos, url.mojom.Url? url)
       => result<bool, mojo_base.mojom.Error>;
-
-  // Get tab data based on the Tab id. Currently the TabId refers to the Tab
-  // handle value.
-  [Sync]
-  GetTab(TabId id) => result<Tab, mojo_base.mojom.Error>;
 };
 
 // TODO (crbug.com/412955607)
diff --git a/chrome/browser/ui/tabs/tab_strip_api/tab_strip_service_impl.cc b/chrome/browser/ui/tabs/tab_strip_api/tab_strip_service_impl.cc
index 6479652..6b5cb75 100644
--- a/chrome/browser/ui/tabs/tab_strip_api/tab_strip_service_impl.cc
+++ b/chrome/browser/ui/tabs/tab_strip_api/tab_strip_service_impl.cc
@@ -3,28 +3,34 @@
 // found in the LICENSE file.
 #include "chrome/browser/ui/tabs/tab_strip_api/tab_strip_service_impl.h"
 
-#include "base/strings/string_number_conversions.h"
-#include "base/strings/utf_string_conversions.h"
 #include "base/types/expected.h"
 #include "chrome/browser/ui/browser_tabstrip.h"
 #include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
-#include "chrome/browser/ui/tabs/tab_renderer_data.h"
+#include "chrome/browser/ui/tabs/tab_strip_api/adapters/browser_adapter_impl.h"
+#include "chrome/browser/ui/tabs/tab_strip_api/adapters/tab_strip_model_adapter_impl.h"
+#include "chrome/browser/ui/tabs/tab_strip_api/converters/tab_converters.h"
 #include "chrome/browser/ui/tabs/tab_strip_model.h"
 #include "chrome/browser/ui/tabs/tab_strip_model_delegate.h"
-#include "chrome/browser/ui/tabs/tab_utils.h"
 #include "mojo/public/mojom/base/error.mojom.h"
 #include "url/gurl.h"
 
 TabStripServiceImpl::TabStripServiceImpl(BrowserWindowInterface* browser,
                                          TabStripModel* tab_strip_model)
-    : browser_(browser), model_(tab_strip_model) {
-  model_->AddObserver(this);
+    : TabStripServiceImpl(
+          std::make_unique<tabs_api::BrowserAdapterImpl>(browser),
+          std::make_unique<tabs_api::TabStripModelAdapterImpl>(
+              tab_strip_model)) {}
+
+TabStripServiceImpl::TabStripServiceImpl(
+    std::unique_ptr<tabs_api::BrowserAdapter> browser_adapter,
+    std::unique_ptr<tabs_api::TabStripModelAdapter> tab_strip_model_adapter)
+    : browser_adapter_(std::move(browser_adapter)),
+      tab_strip_model_adapter_(std::move(tab_strip_model_adapter)) {
+  tab_strip_model_adapter_->AddObserver(this);
 }
 
 TabStripServiceImpl::~TabStripServiceImpl() {
-  if (model_) {
-    model_->RemoveObserver(this);
-  }
+  tab_strip_model_adapter_->RemoveObserver(this);
 
   // Clear all observers
   // TODO (crbug.com/412955607): Implement a removal mechanism similar to
@@ -33,6 +39,65 @@
   observers_.Clear();
 }
 
+void TabStripServiceImpl::GetTabs(GetTabsCallback callback) {
+  auto snapshot = tabs_api::mojom::TabsSnapshot::New();
+
+  std::vector<tabs_api::mojom::TabPtr> result;
+  auto tabs = tab_strip_model_adapter_->GetTabs();
+  for (unsigned int i = 0; i < tabs.size(); ++i) {
+    auto& handle = tabs.at(i);
+    auto renderer_data = tab_strip_model_adapter_->GetTabRendererData(i);
+    auto entry = tabs_api::converters::BuildMojoTab(handle, renderer_data);
+    result.push_back(std::move(entry));
+  }
+  snapshot->tabs = std::move(result);
+
+  // Now that we have a snapshot, create a event stream that will capture all
+  // subsequent updates.
+  mojo::Remote<tabs_api::mojom::TabsObserver> stream;
+  auto pending_receiver = stream.BindNewPipeAndPassReceiver();
+  observers_.Add(std::move(stream));
+  snapshot->stream = std::move(pending_receiver);
+
+  std::move(callback).Run(std::move(snapshot));
+}
+
+void TabStripServiceImpl::GetTab(const tabs_api::TabId& tab_mojom_id,
+                                 GetTabCallback callback) {
+  if (tab_mojom_id.Type() != tabs_api::TabId::Type::kContent) {
+    std::move(callback).Run(base::unexpected(
+        mojo_base::mojom::Error::New(mojo_base::mojom::Code::kInvalidArgument,
+                                     "only tab content ids accepted")));
+    return;
+  }
+
+  int32_t tab_id;
+  if (!base::StringToInt(tab_mojom_id.Id(), &tab_id)) {
+    std::move(callback).Run(base::unexpected(mojo_base::mojom::Error::New(
+        mojo_base::mojom::Code::kInvalidArgument, "invalid tab id provided")));
+    return;
+  }
+
+  tabs_api::mojom::TabPtr tab_result;
+  // TODO (crbug.com/412709270) TabStripModel or TabCollections should have an
+  // api that can fetch id without of relying on indexes.
+  auto tabs = tab_strip_model_adapter_->GetTabs();
+  for (unsigned int i = 0; i < tabs.size(); ++i) {
+    auto& handle = tabs.at(i);
+    if (tab_id == handle.raw_value()) {
+      auto renderer_data = tab_strip_model_adapter_->GetTabRendererData(i);
+      tab_result = tabs_api::converters::BuildMojoTab(handle, renderer_data);
+    }
+  }
+
+  if (tab_result) {
+    std::move(callback).Run(std::move(tab_result));
+  } else {
+    std::move(callback).Run(base::unexpected(mojo_base::mojom::Error::New(
+        mojo_base::mojom::Code::kNotFound, "Tab not found")));
+  }
+}
+
 void TabStripServiceImpl::CreateTabAt(tabs_api::mojom::PositionPtr pos,
                                       const std::optional<GURL>& url,
                                       CreateTabAtCallback callback) {
@@ -45,7 +110,7 @@
     index = pos->index;
   }
 
-  content::WebContents* content = AddTabAt(target_url, index);
+  content::WebContents* content = browser_adapter_->AddTabAt(target_url, index);
   if (!content) {
     // Missing content can happen for a number of reasons. i.e. If the profile
     // is shutting down or if navigation requests are blocked due to some
@@ -59,57 +124,6 @@
   }
 }
 
-content::WebContents* TabStripServiceImpl::AddTabAt(const GURL& url,
-                                                    int index) {
-  // TODO (crbug.com/411134070) chrome::AddAndReturnTabAt does not support
-  // BrowserWindowInterface. Navigation should handle BrowserWindowInterface
-  // instead of Browser.
-  return chrome::AddAndReturnTabAt(browser_->GetBrowserForMigrationOnly(), url,
-                                   index, true);
-}
-
-tabs_api::mojom::TabPtr TabStripServiceImpl::ConvertTabToData(
-    tabs::TabInterface* tab_interface,
-    int index) {
-  auto result = tabs_api::mojom::Tab::New();
-
-  auto tab_renderer_data = TabRendererData::FromTabInModel(model_, index);
-  result->title = base::UTF16ToUTF8(tab_renderer_data.title);
-  // TODO(crbug.com/414630734). Integrate the favicon_url after it is
-  // typemapped.
-  result->url = tab_renderer_data.visible_url;
-  result->network_state = tab_renderer_data.network_state;
-  for (const auto alert_state :
-       GetTabAlertStatesForContents(tab_interface->GetContents())) {
-    result->alert_states.push_back(alert_state);
-  }
-
-  return result;
-}
-
-void TabStripServiceImpl::GetTab(const tabs_api::TabId& tab_mojom_id,
-                                 GetTabCallback callback) {
-  int32_t tab_id;
-  tabs_api::mojom::TabPtr tab_result;
-  if (base::StringToInt(tab_mojom_id.Id(), &tab_id)) {
-    // TODO (crbug.com/412709270) TabStripModel or TabCollections should have an
-    // api that can fetch id without of relying on indexes.
-    for (int index = 0; index < model_->count(); index++) {
-      tabs::TabInterface* tab = model_->GetTabAtIndex(index);
-      if (tab_id == tab->GetHandle().raw_value()) {
-        tab_result = ConvertTabToData(tab, index);
-      }
-    }
-  }
-
-  if (tab_result) {
-    std::move(callback).Run(std::move(tab_result));
-  } else {
-    std::move(callback).Run(base::unexpected(mojo_base::mojom::Error::New(
-        mojo_base::mojom::Code::kNotFound, "Tab not found")));
-  }
-}
-
 void TabStripServiceImpl::OnTabStripModelChanged(
     TabStripModel* tab_strip_model,
     const TabStripModelChange& change,
@@ -139,19 +153,10 @@
       pos->index = content.index;
       positions.emplace_back(std::move(pos));
     }
-    observer.OnTabsCreated(std::move(positions));
+    observer->OnTabsCreated(std::move(positions));
   }
 }
 
-void TabStripServiceImpl::AddObserver(tabs_api::mojom::TabsObserver* observer) {
-  observers_.AddObserver(observer);
-}
-
-void TabStripServiceImpl::RemoveObserver(
-    tabs_api::mojom::TabsObserver* observer) {
-  observers_.RemoveObserver(observer);
-}
-
 void TabStripServiceImpl::Accept(
     mojo::PendingReceiver<tabs_api::mojom::TabStripService> client) {
   clients_.Add(this, std::move(client));
diff --git a/chrome/browser/ui/tabs/tab_strip_api/tab_strip_service_impl.h b/chrome/browser/ui/tabs/tab_strip_api/tab_strip_service_impl.h
index 508857c8..e0c5576 100644
--- a/chrome/browser/ui/tabs/tab_strip_api/tab_strip_service_impl.h
+++ b/chrome/browser/ui/tabs/tab_strip_api/tab_strip_service_impl.h
@@ -8,10 +8,14 @@
 #include "base/functional/callback.h"
 #include "base/memory/raw_ptr.h"
 #include "base/observer_list.h"
+#include "chrome/browser/ui/tabs/tab_strip_api/adapters/browser_adapter.h"
+#include "chrome/browser/ui/tabs/tab_strip_api/adapters/tab_strip_model_adapter.h"
 #include "chrome/browser/ui/tabs/tab_strip_api/tab_id.h"
 #include "chrome/browser/ui/tabs/tab_strip_api/tab_strip_api.mojom.h"
+#include "chrome/browser/ui/tabs/tab_strip_api/tab_strip_service_register.h"
 #include "chrome/browser/ui/tabs/tab_strip_model_observer.h"
 #include "mojo/public/cpp/bindings/receiver_set.h"
+#include "mojo/public/cpp/bindings/remote_set.h"
 
 class BrowserWindowInterface;
 class TabStripModel;
@@ -20,50 +24,43 @@
 // tabs_api::mojom::TabStripController is an experimental TabStrip Api between
 // any view and the TabStripModel.
 class TabStripServiceImpl : public tabs_api::mojom::TabStripService,
-                            public TabStripModelObserver {
+                            public TabStripModelObserver,
+                            public TabStripServiceRegister {
  public:
   explicit TabStripServiceImpl(BrowserWindowInterface* browser,
                                TabStripModel* tab_strip_model);
+  explicit TabStripServiceImpl(
+      std::unique_ptr<tabs_api::BrowserAdapter> browser_adapter,
+      std::unique_ptr<tabs_api::TabStripModelAdapter> tab_strip_adapter);
   TabStripServiceImpl(const TabStripServiceImpl&) = delete;
   TabStripServiceImpl& operator=(const TabStripServiceImpl&) = delete;
   ~TabStripServiceImpl() override;
 
-  void AddObserver(tabs_api::mojom::TabsObserver* observer);
-  void RemoveObserver(tabs_api::mojom::TabsObserver* observer);
-
-  void Accept(mojo::PendingReceiver<tabs_api::mojom::TabStripService> client);
+  // TabStripServiceregister overrides
+  void Accept(
+      mojo::PendingReceiver<tabs_api::mojom::TabStripService> client) override;
 
   // tabs_api::mojom::TabStripService overrides
+  void GetTabs(GetTabsCallback callback) override;
+  void GetTab(const tabs_api::TabId& id, GetTabCallback callback) override;
   void CreateTabAt(tabs_api::mojom::PositionPtr pos,
                    const std::optional<GURL>& url,
                    CreateTabAtCallback callback) override;
 
-  void GetTab(const tabs_api::TabId& id, GetTabCallback callback) override;
-
-  // TabStripModelObserver
+  // TabStripModelObserver overrides
   void OnTabStripModelChanged(
       TabStripModel* tab_strip_model,
       const TabStripModelChange& change,
       const TabStripSelectionChange& selection) override;
 
- protected:
-  // Helper method used to add tab. This is primarily to mock for unit tests
-  // until there is a better way to mock chrome::AddAndReturnTabAt.
-  virtual content::WebContents* AddTabAt(const GURL& url, int index);
-  tabs_api::mojom::TabPtr ConvertTabToData(tabs::TabInterface* tab_interface,
-                                           int index);
-
  private:
   void OnTabStripModelChangeAdded(const TabStripModelChange::Insert& change);
 
-  raw_ptr<BrowserWindowInterface> browser_;
-  raw_ptr<TabStripModel> model_;
+  std::unique_ptr<tabs_api::BrowserAdapter> browser_adapter_;
+  std::unique_ptr<tabs_api::TabStripModelAdapter> tab_strip_model_adapter_;
 
-  // Reminder that ObserverList is currently not thread-safe.
-  // If usage expands to different threads, this needs to transition to
-  // ObserverListThreadSafe.
-  base::ObserverList<tabs_api::mojom::TabsObserver, true>::Unchecked observers_;
   mojo::ReceiverSet<tabs_api::mojom::TabStripService> clients_;
+  mojo::RemoteSet<tabs_api::mojom::TabsObserver> observers_;
 };
 
 #endif  // CHROME_BROWSER_UI_TABS_TAB_STRIP_API_TAB_STRIP_SERVICE_IMPL_H_
diff --git a/chrome/browser/ui/tabs/tab_strip_api/tab_strip_service_impl_browsertest.cc b/chrome/browser/ui/tabs/tab_strip_api/tab_strip_service_impl_browsertest.cc
index d3c34e3..9d5b972 100644
--- a/chrome/browser/ui/tabs/tab_strip_api/tab_strip_service_impl_browsertest.cc
+++ b/chrome/browser/ui/tabs/tab_strip_api/tab_strip_service_impl_browsertest.cc
@@ -79,6 +79,9 @@
 };
 
 IN_PROC_BROWSER_TEST_F(TabStripServiceImplBrowserTest, CreateTabAt) {
+  mojo::Remote<tabs_api::mojom::TabStripService> remote;
+  tab_strip_service_impl_->Accept(remote.BindNewPipeAndPassReceiver());
+
   TabStripModel* model = GetTabStripModel();
   const int expected_tab_count = model->count() + 1;
   const GURL url("http://example.com/");
@@ -87,7 +90,7 @@
   base::RunLoop run_loop;
   tabs_api::mojom::PositionPtr position = CreatePosition(0);
 
-  tab_strip_service_impl_->CreateTabAt(
+  remote->CreateTabAt(
       std::move(position), std::make_optional(url),
       base::BindOnce(&TabStripServiceImplBrowserTest::CreateTabAtApiCallback,
                      base::Unretained(this), &run_loop, &result));
@@ -100,8 +103,10 @@
 }
 
 IN_PROC_BROWSER_TEST_F(TabStripServiceImplBrowserTest, ObserverOnTabsCreated) {
+  mojo::Remote<tabs_api::mojom::TabStripService> remote;
+  tab_strip_service_impl_->Accept(remote.BindNewPipeAndPassReceiver());
   MockTabsObserver mock_observer;
-  tab_strip_service_impl_->AddObserver(&mock_observer);
+  mojo::Receiver<tabs_api::mojom::TabsObserver> receiver(&mock_observer);
   const GURL url("http://example.com/");
   uint32_t target_index = 0;
 
@@ -124,7 +129,16 @@
   base::RunLoop run_loop;
   tabs_api::mojom::PositionPtr position = CreatePosition(target_index);
 
-  tab_strip_service_impl_->CreateTabAt(
+  base::RunLoop get_tabs_loop;
+  remote->GetTabs(base::BindLambdaForTesting(
+      [&](tabs_api::mojom::TabStripService::GetTabsResult result) {
+        ASSERT_TRUE(result.has_value());
+        receiver.Bind(std::move(result.value()->stream));
+        get_tabs_loop.Quit();
+      }));
+  get_tabs_loop.Run();
+
+  remote->CreateTabAt(
       std::move(position), std::make_optional(url),
       base::BindOnce(&TabStripServiceImplBrowserTest::CreateTabAtApiCallback,
                      base::Unretained(this), &run_loop, &result));
@@ -133,6 +147,4 @@
   ASSERT_TRUE(result.has_value())
       << "CreateTabAt failed: " << (result.error()->message);
   EXPECT_TRUE(result.value());
-
-  tab_strip_service_impl_->RemoveObserver(&mock_observer);
 }
diff --git a/chrome/browser/ui/tabs/tab_strip_api/tab_strip_service_impl_unittest.cc b/chrome/browser/ui/tabs/tab_strip_api/tab_strip_service_impl_unittest.cc
index 3259762..25a6fa9 100644
--- a/chrome/browser/ui/tabs/tab_strip_api/tab_strip_service_impl_unittest.cc
+++ b/chrome/browser/ui/tabs/tab_strip_api/tab_strip_service_impl_unittest.cc
@@ -9,71 +9,68 @@
 #include "base/test/task_environment.h"
 #include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
 #include "chrome/browser/ui/browser_window/test/mock_browser_window_interface.h"
+#include "chrome/browser/ui/tabs/tab_strip_api/adapters/browser_adapter.h"
+#include "chrome/browser/ui/tabs/tab_strip_api/adapters/tab_strip_model_adapter.h"
+#include "chrome/browser/ui/tabs/tab_strip_api/tab_id.h"
 #include "chrome/browser/ui/tabs/tab_strip_api/tab_strip_api.mojom.h"
-#include "chrome/browser/ui/tabs/tab_strip_model.h"
-#include "chrome/browser/ui/tabs/test_tab_strip_model_delegate.h"
+#include "chrome/browser/ui/tabs/tab_strip_model_observer.h"
 #include "chrome/test/base/testing_profile.h"
+#include "components/tabs/public/tab_interface.h"
 #include "content/public/test/browser_task_environment.h"
 #include "mojo/public/cpp/bindings/receiver.h"
 #include "mojo/public/cpp/bindings/remote.h"
 #include "mojo/public/mojom/base/error.mojom.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
+namespace tabs_api {
 namespace {
 
-// chrome::AddAndReturnTabAt is not easily mockable. For now, use a wrapper
-// class and abstract away the method such that the functionality of
-// TabStripServiceImpl can still be tested.
-class MockTabStripServiceImpl : public TabStripServiceImpl {
+class FakeTabStripAdapter : public tabs_api::TabStripModelAdapter {
  public:
-  MockTabStripServiceImpl(BrowserWindowInterface* browser,
-                          TabStripModel* tab_strip_model)
-      : TabStripServiceImpl(browser, tab_strip_model) {}
-  MockTabStripServiceImpl(const MockTabStripServiceImpl&) = delete;
-  MockTabStripServiceImpl operator=(const MockTabStripServiceImpl&) = delete;
-  ~MockTabStripServiceImpl() override = default;
+  FakeTabStripAdapter() = default;
+  FakeTabStripAdapter(const FakeTabStripAdapter&) = delete;
+  FakeTabStripAdapter operator=(const FakeTabStripAdapter&) = delete;
+  ~FakeTabStripAdapter() override = default;
+  void AddObserver(TabStripModelObserver*) override {}
+  void RemoveObserver(TabStripModelObserver*) override {}
+  std::vector<tabs::TabHandle> GetTabs() override {
+    return {tabs::TabHandle(888)};
+  }
+  TabRendererData GetTabRendererData(int index) override {
+    return TabRendererData();
+  }
+};
 
- protected:
+class FakeBrowserAdapter : public tabs_api::BrowserAdapter {
+ public:
+  FakeBrowserAdapter() = default;
+  FakeBrowserAdapter(const FakeBrowserAdapter&) = delete;
+  FakeBrowserAdapter operator=(const FakeBrowserAdapter&) = delete;
+  ~FakeBrowserAdapter() override = default;
+
   content::WebContents* AddTabAt(const GURL& url, int index) override {
-    // Integrate with chrome::AddTabAt and add success case.
     return nullptr;
   }
 };
 
 class TabStripServiceImplTest : public testing::Test {
  protected:
-  TabStripServiceImplTest()
-      : profile_(std::make_unique<TestingProfile>()),
-        delegate_(std::make_unique<TestTabStripModelDelegate>()),
-        tab_strip_model_(
-            std::make_unique<TabStripModel>(delegate(), profile())),
-        browser_window_interface_(
-            std::make_unique<MockBrowserWindowInterface>()) {}
+  TabStripServiceImplTest() = default;
   TabStripServiceImplTest(const TabStripServiceImplTest&) = delete;
   TabStripServiceImplTest operator=(const TabStripServiceImplTest&) = delete;
   ~TabStripServiceImplTest() override = default;
 
   void SetUp() override {
-    impl_ = std::make_unique<MockTabStripServiceImpl>(
-        browser_window_interface(), tab_strip_model());
+    impl_ = std::make_unique<TabStripServiceImpl>(
+        std::make_unique<FakeBrowserAdapter>(),
+        std::make_unique<FakeTabStripAdapter>());
     impl_->Accept(client_.BindNewPipeAndPassReceiver());
   }
 
-  TestingProfile* profile() { return profile_.get(); }
-  TestTabStripModelDelegate* delegate() { return delegate_.get(); }
-  TabStripModel* tab_strip_model() { return tab_strip_model_.get(); }
-  MockBrowserWindowInterface* browser_window_interface() {
-    return browser_window_interface_.get();
-  }
-
   mojo::Remote<tabs_api::mojom::TabStripService> client_;
 
  private:
   content::BrowserTaskEnvironment task_environment_;
-  std::unique_ptr<TestingProfile> profile_;
-  std::unique_ptr<TestTabStripModelDelegate> delegate_;
-  std::unique_ptr<TabStripModel> tab_strip_model_;
-  std::unique_ptr<MockBrowserWindowInterface> browser_window_interface_;
   std::unique_ptr<TabStripServiceImpl> impl_;
 };
 
@@ -86,6 +83,19 @@
   ASSERT_EQ(result.error()->code, mojo_base::mojom::Code::kFailedPrecondition);
 }
 
+TEST_F(TabStripServiceImplTest, GetTabs) {
+  tabs_api::mojom::TabStripService::GetTabsResult result;
+  bool success = client_->GetTabs(&result);
+
+  ASSERT_TRUE(success);
+  ASSERT_EQ(1u, result.value()->tabs.size());
+  ASSERT_EQ("888", result.value()->tabs[0]->id.Id());
+  ASSERT_EQ(TabId::Type::kContent, result.value()->tabs[0]->id.Type());
+  // TODO(crbug.com/412709270): we can probably easily test the observation
+  // in unit test as well. But it is already covered by the browser
+  // test, so skipping for now.
+}
+
 TEST_F(TabStripServiceImplTest, GetTab) {
   tabs_api::mojom::TabStripService::GetTabResult result;
   tabs_api::TabId tab_id;
@@ -93,7 +103,8 @@
 
   ASSERT_TRUE(success);
   ASSERT_FALSE(result.has_value());
-  ASSERT_EQ(result.error()->code, mojo_base::mojom::Code::kNotFound);
+  ASSERT_EQ(result.error()->code, mojo_base::mojom::Code::kInvalidArgument);
 }
 
 }  // namespace
+}  // namespace tabs_api
diff --git a/chrome/browser/ui/tabs/tab_strip_api/tab_strip_service_register.h b/chrome/browser/ui/tabs/tab_strip_api/tab_strip_service_register.h
new file mode 100644
index 0000000..2eda2c4
--- /dev/null
+++ b/chrome/browser/ui/tabs/tab_strip_api/tab_strip_service_register.h
@@ -0,0 +1,20 @@
+// Copyright 2025 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_UI_TABS_TAB_STRIP_API_TAB_STRIP_SERVICE_REGISTER_H_
+#define CHROME_BROWSER_UI_TABS_TAB_STRIP_API_TAB_STRIP_SERVICE_REGISTER_H_
+
+#include "chrome/browser/ui/tabs/tab_strip_api/tab_strip_api.mojom.h"
+#include "mojo/public/cpp/bindings/pending_receiver.h"
+
+// This is a public interface used to accept pending receivers to
+// tabs_api::mojom::TabStripService.
+class TabStripServiceRegister {
+ public:
+  virtual ~TabStripServiceRegister() = default;
+  virtual void Accept(
+      mojo::PendingReceiver<tabs_api::mojom::TabStripService> client) = 0;
+};
+
+#endif  // CHROME_BROWSER_UI_TABS_TAB_STRIP_API_TAB_STRIP_SERVICE_REGISTER_H_
diff --git a/chrome/browser/ui/tabs/tab_strip_model.cc b/chrome/browser/ui/tabs/tab_strip_model.cc
index 6f6d634..e9796954 100644
--- a/chrome/browser/ui/tabs/tab_strip_model.cc
+++ b/chrome/browser/ui/tabs/tab_strip_model.cc
@@ -539,11 +539,9 @@
 
   // Send possible split detach notification.
   for (auto const& [split_id, tabs_with_indices] : splits_in_group) {
-    for (TabStripModelObserver& observer : observers_) {
-      observer.OnSplitTabRemoved(tabs_with_indices, split_id,
-                                 TabStripModelObserver::SplitTabRemoveReason::
-                                     kDetachedToAnotherTabstrip);
-    }
+    OnSplitTabRemoved(
+        split_id, tabs_with_indices,
+        SplitTabChange::SplitTabRemoveReason::kDetachedToAnotherTabstrip);
   }
 
   // Notify tab is removed from model
@@ -650,12 +648,10 @@
 
   // Send split attach notification
   for (const split_tabs::SplitTabId& split_id : splits_in_group) {
-    for (TabStripModelObserver& observer : observers_) {
-      observer.OnSplitTabCreated(GetTabsAndIndicesInSplit(split_id), split_id,
-                                 TabStripModelObserver::SplitTabAddReason::
-                                     kInsertedFromAnotherTabstrip,
-                                 *GetSplitData(split_id)->visual_data());
-    }
+    OnSplitTabCreated(
+        split_id, GetTabsAndIndicesInSplit(split_id),
+        SplitTabChange::SplitTabAddReason::kInsertedFromAnotherTabstrip,
+        *GetSplitData(split_id)->visual_data());
   }
 
   return tabs_in_group;
@@ -891,9 +887,8 @@
       MoveBreaksSplitContiguity(static_cast<int>(tabs_in_group.start()),
                                 tabs_in_group.length(), to_index);
   if (destination_split.has_value()) {
-    RemoveSplitImpl(
-        destination_split.value(),
-        TabStripModelObserver::SplitTabRemoveReason::kSplitTabRemoved);
+    RemoveSplitImpl(destination_split.value(),
+                    SplitTabChange::SplitTabRemoveReason::kSplitTabRemoved);
   }
 
   MoveGroupToImpl(group, to_index);
@@ -1551,9 +1546,8 @@
   // Remove the split of the origin tab if it is not moving within the
   // split collection.
   if (tab->IsSplit()) {
-    RemoveSplitImpl(
-        tab->GetSplit().value(),
-        TabStripModelObserver::SplitTabRemoveReason::kSplitTabRemoved);
+    RemoveSplitImpl(tab->GetSplit().value(),
+                    SplitTabChange::SplitTabRemoveReason::kSplitTabRemoved);
   }
 
   // Maybe remove the split tab of the destination if it results in
@@ -1562,9 +1556,8 @@
       MoveBreaksSplitContiguity(initial_index, 1, final_index);
 
   if (destination_split.has_value()) {
-    RemoveSplitImpl(
-        destination_split.value(),
-        TabStripModelObserver::SplitTabRemoveReason::kSplitTabRemoved);
+    RemoveSplitImpl(destination_split.value(),
+                    SplitTabChange::SplitTabRemoveReason::kSplitTabRemoved);
   }
 }
 
@@ -1583,10 +1576,8 @@
 
   split_data->visual_data()->set_split_layout(tab_layout);
 
-  for (TabStripModelObserver& observer : observers_) {
-    observer.OnSplitTabVisualsChanged(split_id, old_visual_data,
-                                      *split_data->visual_data());
-  }
+  OnSplitTabVisualsChanged(split_id, old_visual_data,
+                           *split_data->visual_data());
 }
 
 void TabStripModel::UpdateSplitRatio(split_tabs::SplitTabId split_id,
@@ -1600,10 +1591,8 @@
   split_tabs::SplitTabVisualData old_visual_data = *split_data->visual_data();
   split_data->visual_data()->set_split_ratio(split_ratio);
 
-  for (TabStripModelObserver& observer : observers_) {
-    observer.OnSplitTabVisualsChanged(split_id, old_visual_data,
-                                      *split_data->visual_data());
-  }
+  OnSplitTabVisualsChanged(split_id, old_visual_data,
+                           *split_data->visual_data());
 }
 
 void TabStripModel::UpdateActiveTabInSplit(split_tabs::SplitTabId split_id,
@@ -1627,8 +1616,8 @@
   // 3. Close the previous active tab (if we are replacing the tab).
   // 4. Re-split the other tabs that were a part of the split collection with
   // the new active tab (the initial tab at `update_index`)
-  RemoveSplitImpl(
-      split_id, TabStripModelObserver::SplitTabRemoveReason::kSplitTabUpdated);
+  RemoveSplitImpl(split_id,
+                  SplitTabChange::SplitTabRemoveReason::kSplitTabUpdated);
 
   if (update_type == SplitUpdateType::kReplace) {
     int destination_index =
@@ -1643,16 +1632,12 @@
         GetTabGroupForTab(update_index);
     bool destination_pinned = IsTabPinned(update_index);
 
+    MoveTabToIndexImpl(update_index, active_index(), GetActiveTab()->GetGroup(),
+                       GetActiveTab()->IsPinned(), true);
     if (active_index() < update_index) {
-      MoveTabToIndexImpl(update_index, active_index(),
-                         GetActiveTab()->GetGroup(), GetActiveTab()->IsPinned(),
-                         true);
       MoveTabToIndexImpl(initial_active_index + 1, update_index,
                          destination_group, destination_pinned, false);
     } else {
-      MoveTabToIndexImpl(update_index, active_index(),
-                         GetActiveTab()->GetGroup(), GetActiveTab()->IsPinned(),
-                         true);
       MoveTabToIndexImpl(initial_active_index - 1, update_index,
                          destination_group, destination_pinned, false);
     }
@@ -1664,7 +1649,7 @@
   }
 
   AddToSplitImpl(split_id, split_indices, split_visual_data,
-                 TabStripModelObserver::SplitTabAddReason::kSplitTabUpdated);
+                 SplitTabChange::SplitTabAddReason::kSplitTabUpdated);
 }
 
 void TabStripModel::ReverseTabsInSplit(split_tabs::SplitTabId split_id) {
@@ -1691,9 +1676,9 @@
 
   split_tabs::SplitTabId split_id = split_tabs::SplitTabId::GenerateNew();
 
-  return AddToSplitImpl(
-      split_id, indices, split_tabs::SplitTabVisualData(tab_layout, 0.5),
-      TabStripModelObserver::SplitTabAddReason::kNewSplitTabAdded);
+  return AddToSplitImpl(split_id, indices,
+                        split_tabs::SplitTabVisualData(tab_layout, 0.5),
+                        SplitTabChange::SplitTabAddReason::kNewSplitTabAdded);
 }
 
 void TabStripModel::AddTabGroup(const tab_groups::TabGroupId group_id,
@@ -1816,8 +1801,8 @@
 
 void TabStripModel::RemoveSplit(split_tabs::SplitTabId split_id) {
   ReentrancyCheck reentrancy_check(&reentrancy_guard_);
-  RemoveSplitImpl(
-      split_id, TabStripModelObserver::SplitTabRemoveReason::kSplitTabRemoved);
+  RemoveSplitImpl(split_id,
+                  SplitTabChange::SplitTabRemoveReason::kSplitTabRemoved);
 }
 
 bool TabStripModel::IsReadLaterSupportedForAny(
@@ -1941,6 +1926,57 @@
   }
 }
 
+void TabStripModel::OnSplitTabCreated(
+    split_tabs::SplitTabId split_id,
+    const std::vector<std::pair<tabs::TabInterface*, int>>& tabs_with_indices,
+    SplitTabChange::SplitTabAddReason reason,
+    const split_tabs::SplitTabVisualData& visual_data) {
+  SplitTabChange change(
+      this, split_id,
+      SplitTabChange::AddedChange(tabs_with_indices, reason, visual_data));
+
+  for (auto& observer : observers_) {
+    observer.OnSplitTabChanged(change);
+  }
+}
+
+void TabStripModel::OnSplitTabVisualsChanged(
+    split_tabs::SplitTabId split_id,
+    const split_tabs::SplitTabVisualData& old_visual_data,
+    const split_tabs::SplitTabVisualData& new_visual_data) {
+  SplitTabChange change(
+      this, split_id,
+      SplitTabChange::VisualsChange(old_visual_data, new_visual_data));
+
+  for (auto& observer : observers_) {
+    observer.OnSplitTabChanged(change);
+  }
+}
+
+void TabStripModel::OnSplitTabContentsUpdated(
+    split_tabs::SplitTabId split_id,
+    const std::vector<std::pair<tabs::TabInterface*, int>>& prev_tabs,
+    const std::vector<std::pair<tabs::TabInterface*, int>>& new_tabs) {
+  SplitTabChange change(this, split_id,
+                        SplitTabChange::ContentsChange(prev_tabs, new_tabs));
+
+  for (auto& observer : observers_) {
+    observer.OnSplitTabChanged(change);
+  }
+}
+
+void TabStripModel::OnSplitTabRemoved(
+    split_tabs::SplitTabId split_id,
+    const std::vector<std::pair<tabs::TabInterface*, int>>& tabs_with_indices,
+    SplitTabChange::SplitTabRemoveReason reason) {
+  SplitTabChange change(
+      this, split_id, SplitTabChange::RemovedChange(tabs_with_indices, reason));
+
+  for (auto& observer : observers_) {
+    observer.OnSplitTabChanged(change);
+  }
+}
+
 std::u16string TabStripModel::GetTitleAt(int index) const {
   return TabUIHelper::FromWebContents(GetWebContentsAt(index))->GetTitle();
 }
@@ -3308,7 +3344,7 @@
     split_tabs::SplitTabId split_id,
     std::vector<int> indices,
     split_tabs::SplitTabVisualData visual_data,
-    TabStripModelObserver::SplitTabAddReason reason) {
+    SplitTabChange::SplitTabAddReason reason) {
   // Insert the active index into the sorted `indices`.
   auto position = lower_bound(indices.begin(), indices.end(), active_index());
   indices.insert(position, active_index());
@@ -3345,17 +3381,14 @@
   TabStripModelChange change;
   OnChange(change, selection);
 
-  for (TabStripModelObserver& observer : observers_) {
-    observer.OnSplitTabCreated(tabs_with_indices, split_id, reason,
-                               visual_data);
-  }
+  OnSplitTabCreated(split_id, tabs_with_indices, reason, visual_data);
 
   return split_id;
 }
 
 void TabStripModel::RemoveSplitImpl(
     split_tabs::SplitTabId split_id,
-    TabStripModelObserver::SplitTabRemoveReason reason) {
+    SplitTabChange::SplitTabRemoveReason reason) {
   std::vector<std::pair<tabs::TabInterface*, int>> tabs_with_indices =
       GetTabsAndIndicesInSplit(split_id);
 
@@ -3377,9 +3410,7 @@
     OnChange(change, selection);
   }
 
-  for (TabStripModelObserver& observer : observers_) {
-    observer.OnSplitTabRemoved(tabs_with_indices, split_id, reason);
-  }
+  OnSplitTabRemoved(split_id, tabs_with_indices, reason);
 }
 
 void TabStripModel::AddToNewGroupImpl(
@@ -3554,9 +3585,8 @@
   if (std::optional<split_tabs::SplitTabId> split_id =
           InsertionBreaksSplitContiguity(index);
       split_id.has_value()) {
-    RemoveSplitImpl(
-        split_id.value(),
-        TabStripModelObserver::SplitTabRemoveReason::kSplitTabRemoved);
+    RemoveSplitImpl(split_id.value(),
+                    SplitTabChange::SplitTabRemoveReason::kSplitTabRemoved);
   }
 
   tabs::TabInterface* old_active_tab = GetActiveTab();
@@ -3601,9 +3631,8 @@
   std::optional<int> next_selected_index = DetermineNewSelectedIndex(index);
   const bool removed_tab_is_split = tab->IsSplit();
   if (removed_tab_is_split) {
-    RemoveSplitImpl(
-        tab->GetSplit().value(),
-        TabStripModelObserver::SplitTabRemoveReason::kSplitTabRemoved);
+    RemoveSplitImpl(tab->GetSplit().value(),
+                    SplitTabChange::SplitTabRemoveReason::kSplitTabRemoved);
   }
 
   // Remove the tab.
@@ -3707,11 +3736,9 @@
   }
 
   if (move_within_split) {
-    for (TabStripModelObserver& observer : observers_) {
-      observer.OnSplitTabContentsUpdated(
-          tab->GetSplit().value(), initial_split_tabs,
-          GetTabsAndIndicesInSplit(tab->GetSplit().value()));
-    }
+    OnSplitTabContentsUpdated(
+        tab->GetSplit().value(), initial_split_tabs,
+        GetTabsAndIndicesInSplit(tab->GetSplit().value()));
   }
 
   if (initial_pinned_state != tab->IsPinned()) {
diff --git a/chrome/browser/ui/tabs/tab_strip_model.h b/chrome/browser/ui/tabs/tab_strip_model.h
index 95e8a5c8..df3f4893 100644
--- a/chrome/browser/ui/tabs/tab_strip_model.h
+++ b/chrome/browser/ui/tabs/tab_strip_model.h
@@ -822,6 +822,31 @@
   // Notify observers that `group` is attached to the model.
   void OnTabGroupAttached(tabs::TabGroupTabCollection* group_collection);
 
+  // Notify observers that split with `split_id` has been created.
+  void OnSplitTabCreated(
+      split_tabs::SplitTabId split_id,
+      const std::vector<std::pair<tabs::TabInterface*, int>>& tabs_with_indices,
+      SplitTabChange::SplitTabAddReason reason,
+      const split_tabs::SplitTabVisualData& visual_data);
+
+  // Notify observers that visual data for a split has changed.
+  void OnSplitTabVisualsChanged(
+      split_tabs::SplitTabId split_id,
+      const split_tabs::SplitTabVisualData& old_visual_data,
+      const split_tabs::SplitTabVisualData& new_visual_data);
+
+  // Notify observers that contents of a split has been reordered.
+  void OnSplitTabContentsUpdated(
+      split_tabs::SplitTabId split_id,
+      const std::vector<std::pair<tabs::TabInterface*, int>>& prev_tabs,
+      const std::vector<std::pair<tabs::TabInterface*, int>>& new_tabs);
+
+  // Notify observers that split with `split_id` has been removed.
+  void OnSplitTabRemoved(
+      split_tabs::SplitTabId split_id,
+      const std::vector<std::pair<tabs::TabInterface*, int>>& tabs_with_indices,
+      SplitTabChange::SplitTabRemoveReason reason);
+
   // Detaches the tab at the specified `index` from this strip.
   // `web_contents_remove_reason` is used to indicate to observers what is going
   // to happen to the WebContents (i.e. deleted or reinserted into another tab
@@ -978,10 +1003,10 @@
       split_tabs::SplitTabId split_id,
       std::vector<int> indices,
       split_tabs::SplitTabVisualData visual_data,
-      TabStripModelObserver::SplitTabAddReason reasons);
+      SplitTabChange::SplitTabAddReason reasons);
 
   void RemoveSplitImpl(split_tabs::SplitTabId split_id,
-                       TabStripModelObserver::SplitTabRemoveReason reason);
+                       SplitTabChange::SplitTabRemoveReason reason);
 
   // Adds tabs to newly-allocated group id |new_group|. This group must be new
   // and have no tabs in it.
diff --git a/chrome/browser/ui/tabs/tab_strip_model_observer.cc b/chrome/browser/ui/tabs/tab_strip_model_observer.cc
index 631865d..406ac3f 100644
--- a/chrome/browser/ui/tabs/tab_strip_model_observer.cc
+++ b/chrome/browser/ui/tabs/tab_strip_model_observer.cc
@@ -233,6 +233,101 @@
                      std::make_unique<CloseChange>(std::move(deltap))) {}
 
 ////////////////////////////////////////////////////////////////////////////////
+// SplitTabChange
+//
+SplitTabChange::SplitTabChange(TabStripModel* model,
+                               split_tabs::SplitTabId split_id,
+                               Type type,
+                               std::unique_ptr<Delta> deltap)
+    : split_id(split_id), model(model), type(type), delta(std::move(deltap)) {}
+
+SplitTabChange::AddedChange::AddedChange(
+    const std::vector<std::pair<tabs::TabInterface*, int>>& tabs,
+    SplitTabAddReason reason,
+    const split_tabs::SplitTabVisualData& visual_data)
+    : tabs_(tabs), reason_(reason), visual_data_(visual_data) {}
+SplitTabChange::AddedChange::~AddedChange() = default;
+SplitTabChange::AddedChange::AddedChange(const SplitTabChange::AddedChange&) =
+    default;
+
+SplitTabChange::VisualsChange::VisualsChange(
+    const split_tabs::SplitTabVisualData& old_visual_data,
+    const split_tabs::SplitTabVisualData& new_visual_data)
+    : old_visual_data_(old_visual_data), new_visual_data_(new_visual_data) {}
+SplitTabChange::VisualsChange::~VisualsChange() = default;
+
+SplitTabChange::ContentsChange::ContentsChange(
+    const std::vector<std::pair<tabs::TabInterface*, int>>& prev_tabs,
+    const std::vector<std::pair<tabs::TabInterface*, int>>& new_tabs)
+    : prev_tabs_(prev_tabs), new_tabs_(new_tabs) {}
+SplitTabChange::ContentsChange::~ContentsChange() = default;
+SplitTabChange::ContentsChange::ContentsChange(
+    const SplitTabChange::ContentsChange&) = default;
+
+SplitTabChange::RemovedChange::RemovedChange(
+    const std::vector<std::pair<tabs::TabInterface*, int>>& tabs,
+    SplitTabRemoveReason reason)
+    : tabs_(tabs), reason_(reason) {}
+SplitTabChange::RemovedChange::~RemovedChange() = default;
+SplitTabChange::RemovedChange::RemovedChange(
+    const SplitTabChange::RemovedChange&) = default;
+
+SplitTabChange::SplitTabChange(TabStripModel* model,
+                               split_tabs::SplitTabId split_id,
+                               AddedChange deltap)
+    : SplitTabChange(model,
+                     split_id,
+                     Type::kAdded,
+                     std::make_unique<AddedChange>(std::move(deltap))) {}
+
+SplitTabChange::SplitTabChange(TabStripModel* model,
+                               split_tabs::SplitTabId split_id,
+                               VisualsChange deltap)
+    : SplitTabChange(model,
+                     split_id,
+                     Type::kVisualsChanged,
+                     std::make_unique<VisualsChange>(std::move(deltap))) {}
+
+SplitTabChange::SplitTabChange(TabStripModel* model,
+                               split_tabs::SplitTabId split_id,
+                               ContentsChange deltap)
+    : SplitTabChange(model,
+                     split_id,
+                     Type::kContentsChanged,
+                     std::make_unique<ContentsChange>(std::move(deltap))) {}
+
+SplitTabChange::SplitTabChange(TabStripModel* model,
+                               split_tabs::SplitTabId split_id,
+                               RemovedChange deltap)
+    : SplitTabChange(model,
+                     split_id,
+                     Type::kRemoved,
+                     std::make_unique<RemovedChange>(std::move(deltap))) {}
+
+SplitTabChange::~SplitTabChange() = default;
+
+const SplitTabChange::AddedChange* SplitTabChange::GetAddedChange() const {
+  DCHECK_EQ(type, Type::kAdded);
+  return static_cast<const AddedChange*>(delta.get());
+}
+
+const SplitTabChange::VisualsChange* SplitTabChange::GetVisualsChange() const {
+  DCHECK_EQ(type, Type::kVisualsChanged);
+  return static_cast<const VisualsChange*>(delta.get());
+}
+
+const SplitTabChange::ContentsChange* SplitTabChange::GetContentsChange()
+    const {
+  DCHECK_EQ(type, Type::kContentsChanged);
+  return static_cast<const ContentsChange*>(delta.get());
+}
+
+const SplitTabChange::RemovedChange* SplitTabChange::GetRemovedChange() const {
+  DCHECK_EQ(type, Type::kRemoved);
+  return static_cast<const RemovedChange*>(delta.get());
+}
+
+////////////////////////////////////////////////////////////////////////////////
 // TabStripModelObserver
 //
 TabStripModelObserver::TabStripModelObserver() = default;
@@ -263,26 +358,7 @@
 void TabStripModelObserver::OnTabGroupWillBeRemoved(
     const tab_groups::TabGroupId& group_id) {}
 
-void TabStripModelObserver::OnSplitTabCreated(
-    std::vector<std::pair<tabs::TabInterface*, int>> tabs,
-    split_tabs::SplitTabId split_id,
-    TabStripModelObserver::SplitTabAddReason reason,
-    split_tabs::SplitTabVisualData visual_data) {}
-
-void TabStripModelObserver::OnSplitTabRemoved(
-    std::vector<std::pair<tabs::TabInterface*, int>> tabs,
-    split_tabs::SplitTabId split_id,
-    TabStripModelObserver::SplitTabRemoveReason reason) {}
-
-void TabStripModelObserver::OnSplitTabVisualsChanged(
-    split_tabs::SplitTabId split_id,
-    split_tabs::SplitTabVisualData old_visual_data,
-    split_tabs::SplitTabVisualData new_visual_data) {}
-
-void TabStripModelObserver::OnSplitTabContentsUpdated(
-    split_tabs::SplitTabId split_id,
-    std::vector<std::pair<tabs::TabInterface*, int>> prev_tabs,
-    std::vector<std::pair<tabs::TabInterface*, int>> new_tabs) {}
+void TabStripModelObserver::OnSplitTabChanged(const SplitTabChange& change) {}
 
 void TabStripModelObserver::TabChangedAt(WebContents* contents,
                                          int index,
diff --git a/chrome/browser/ui/tabs/tab_strip_model_observer.h b/chrome/browser/ui/tabs/tab_strip_model_observer.h
index d05169b..99a7c3f 100644
--- a/chrome/browser/ui/tabs/tab_strip_model_observer.h
+++ b/chrome/browser/ui/tabs/tab_strip_model_observer.h
@@ -16,6 +16,8 @@
 #include "components/sessions/core/session_id.h"
 #include "components/tab_groups/tab_group_id.h"
 #include "components/tab_groups/tab_group_visual_data.h"
+#include "components/tabs/public/split_tab_id.h"
+#include "components/tabs/public/split_tab_visual_data.h"
 #include "components/tabs/public/tab_interface.h"
 #include "third_party/perfetto/include/perfetto/tracing/traced_value_forward.h"
 #include "ui/base/models/list_selection_model.h"
@@ -25,10 +27,6 @@
 class TabGroupTabCollection;
 }  // namespace tabs
 
-namespace split_tabs {
-class SplitTabVisualData;
-}
-
 namespace content {
 class WebContents;
 }
@@ -347,6 +345,131 @@
   std::unique_ptr<Delta> delta;
 };
 
+struct SplitTabChange {
+  enum class Type { kAdded, kVisualsChanged, kContentsChanged, kRemoved };
+
+  enum class SplitTabAddReason {
+    kNewSplitTabAdded,
+    kSplitTabUpdated,
+    kInsertedFromAnotherTabstrip
+  };
+
+  enum class SplitTabRemoveReason {
+    kSplitTabRemoved,
+    kSplitTabUpdated,
+    kDetachedToAnotherTabstrip
+  };
+
+  // Base class for all changes. Similar to TabStripModelChange::Delta.
+  struct Delta {
+    virtual ~Delta() = default;
+  };
+
+  struct AddedChange : public Delta {
+    AddedChange(const std::vector<std::pair<tabs::TabInterface*, int>>& tabs,
+                SplitTabAddReason reason,
+                const split_tabs::SplitTabVisualData& visual_data);
+    ~AddedChange() override;
+    AddedChange(const AddedChange&);
+
+    const std::vector<std::pair<tabs::TabInterface*, int>>& tabs() const {
+      return tabs_;
+    }
+    const split_tabs::SplitTabVisualData& visual_data() const {
+      return visual_data_;
+    }
+    SplitTabAddReason reason() const { return reason_; }
+
+   private:
+    std::vector<std::pair<tabs::TabInterface*, int>> tabs_;
+    SplitTabAddReason reason_;
+    split_tabs::SplitTabVisualData visual_data_;
+  };
+
+  struct VisualsChange : public Delta {
+    VisualsChange(const split_tabs::SplitTabVisualData& old_visual_data,
+                  const split_tabs::SplitTabVisualData& new_visual_data);
+    ~VisualsChange() override;
+
+    const split_tabs::SplitTabVisualData& old_visual_data() const {
+      return old_visual_data_;
+    }
+    const split_tabs::SplitTabVisualData& new_visual_data() const {
+      return new_visual_data_;
+    }
+
+   private:
+    split_tabs::SplitTabVisualData old_visual_data_;
+    split_tabs::SplitTabVisualData new_visual_data_;
+  };
+
+  struct ContentsChange : public Delta {
+    ContentsChange(
+        const std::vector<std::pair<tabs::TabInterface*, int>>& prev_tabs,
+        const std::vector<std::pair<tabs::TabInterface*, int>>& new_tabs);
+    ~ContentsChange() override;
+    ContentsChange(const ContentsChange&);
+
+    const std::vector<std::pair<tabs::TabInterface*, int>>& prev_tabs() const {
+      return prev_tabs_;
+    }
+    const std::vector<std::pair<tabs::TabInterface*, int>>& new_tabs() const {
+      return new_tabs_;
+    }
+
+   private:
+    std::vector<std::pair<tabs::TabInterface*, int>> prev_tabs_;
+    std::vector<std::pair<tabs::TabInterface*, int>> new_tabs_;
+  };
+
+  struct RemovedChange : public Delta {
+    RemovedChange(const std::vector<std::pair<tabs::TabInterface*, int>>& tabs,
+                  SplitTabRemoveReason reason);
+    ~RemovedChange() override;
+    RemovedChange(const RemovedChange&);
+
+    const std::vector<std::pair<tabs::TabInterface*, int>>& tabs() const {
+      return tabs_;
+    }
+    SplitTabRemoveReason reason() const { return reason_; }
+
+   private:
+    std::vector<std::pair<tabs::TabInterface*, int>> tabs_;
+    SplitTabRemoveReason reason_;
+  };
+
+  SplitTabChange(TabStripModel* model,
+                 split_tabs::SplitTabId split_id,
+                 Type type,
+                 std::unique_ptr<Delta> deltap);
+  SplitTabChange(TabStripModel* model,
+                 split_tabs::SplitTabId split_id,
+                 AddedChange deltap);
+  SplitTabChange(TabStripModel* model,
+                 split_tabs::SplitTabId split_id,
+                 VisualsChange deltap);
+  SplitTabChange(TabStripModel* model,
+                 split_tabs::SplitTabId split_id,
+                 ContentsChange deltap);
+  SplitTabChange(TabStripModel* model,
+                 split_tabs::SplitTabId split_id,
+                 RemovedChange deltap);
+
+  ~SplitTabChange();
+
+  const AddedChange* GetAddedChange() const;
+  const VisualsChange* GetVisualsChange() const;
+  const ContentsChange* GetContentsChange() const;
+  const RemovedChange* GetRemovedChange() const;
+
+  split_tabs::SplitTabId split_id;
+  raw_ptr<TabStripModel> model;
+  Type type;
+
+ private:
+  std::unique_ptr<Delta> delta;
+};
+
 ////////////////////////////////////////////////////////////////////////////////
 //
 // TabStripModelObserver
@@ -419,42 +542,9 @@
   // Notfies us when a Tab Group will be removed from the Tab Group Model.
   virtual void OnTabGroupWillBeRemoved(const tab_groups::TabGroupId& group_id);
 
-  enum class SplitTabAddReason {
-    kNewSplitTabAdded,
-    kSplitTabUpdated,
-    kInsertedFromAnotherTabstrip
-  };
-
-  enum class SplitTabRemoveReason {
-    kSplitTabRemoved,
-    kSplitTabUpdated,
-    kDetachedToAnotherTabstrip
-  };
-
-  // Notification that a new split view has been added to the TabStripModel.
-  virtual void OnSplitTabCreated(
-      std::vector<std::pair<tabs::TabInterface*, int>> tabs,
-      split_tabs::SplitTabId split_id,
-      SplitTabAddReason reason,
-      split_tabs::SplitTabVisualData visual_data);
-
-  // Notification that a split view has been removed from the TabStripModel.
-  virtual void OnSplitTabRemoved(
-      std::vector<std::pair<tabs::TabInterface*, int>> tabs,
-      split_tabs::SplitTabId split_id,
-      SplitTabRemoveReason reason);
-
-  // Notification that the visuals of a split view is updated.
-  virtual void OnSplitTabVisualsChanged(
-      split_tabs::SplitTabId split_id,
-      split_tabs::SplitTabVisualData old_visual_data,
-      split_tabs::SplitTabVisualData new_visual_data);
-
-  // Notification that the contents of a split view is updated.
-  virtual void OnSplitTabContentsUpdated(
-      split_tabs::SplitTabId split_id,
-      std::vector<std::pair<tabs::TabInterface*, int>> prev_tabs,
-      std::vector<std::pair<tabs::TabInterface*, int>> new_tabs);
+  // Notifies us when there is a change to split tab state in the TabStripModel.
+  // The |change| provides details of the change to split tab.
+  virtual void OnSplitTabChanged(const SplitTabChange& change);
 
   // The specified WebContents at |index| changed in some way. |contents|
   // may be an entirely different object and the old value is no longer
diff --git a/chrome/browser/ui/tabs/test/mock_tab_interface.h b/chrome/browser/ui/tabs/test/mock_tab_interface.h
index 44590d1..9543a64 100644
--- a/chrome/browser/ui/tabs/test/mock_tab_interface.h
+++ b/chrome/browser/ui/tabs/test/mock_tab_interface.h
@@ -75,6 +75,7 @@
               (),
               (const override));
   MOCK_METHOD(TabFeatures*, GetTabFeatures, (), (override));
+  MOCK_METHOD(const TabFeatures*, GetTabFeatures, (), (const override));
   MOCK_METHOD(bool, IsPinned, (), (const override));
   MOCK_METHOD(bool, IsSplit, (), (const override));
   MOCK_METHOD(std::optional<tab_groups::TabGroupId>,
diff --git a/chrome/browser/ui/views/find_bar_host.cc b/chrome/browser/ui/views/find_bar_host.cc
index 603a952..577c01d 100644
--- a/chrome/browser/ui/views/find_bar_host.cc
+++ b/chrome/browser/ui/views/find_bar_host.cc
@@ -208,16 +208,16 @@
       return false;
   }
 
-  content::WebContents* contents = find_bar_controller_->web_contents();
-  if (!contents) {
+  if (!web_contents()) {
     return false;
   }
 
   // Make sure we don't have a text field element interfering with keyboard
   // input. Otherwise Up and Down arrow key strokes get eaten. "Nom Nom Nom".
-  contents->ClearFocusedElement();
+  web_contents()->ClearFocusedElement();
   NativeWebKeyboardEvent event(key_event);
-  contents->GetPrimaryMainFrame()
+  web_contents()
+      ->GetPrimaryMainFrame()
       ->GetRenderViewHost()
       ->GetWidget()
       ->ForwardKeyboardEventWithLatencyInfo(event, *key_event.latency());
@@ -250,7 +250,7 @@
   }
 }
 
-void FindBarHost::Show(bool animate) {
+void FindBarHost::Show(bool animate, bool focus) {
   RestoreOrCreateFocusTracker();
   DCHECK(host_);
 
@@ -263,7 +263,11 @@
     animation_->End();
   }
 
-  host_->Show();
+  if (focus) {
+    host_->Show();
+  } else {
+    host_->ShowInactive();
+  }
 
   bool was_visible = is_visible_;
   is_visible_ = true;
@@ -312,6 +316,7 @@
 
 void FindBarHost::SetFocusAndSelection() {
   view_->FocusAndSelectAll();
+  SetFindBarIsFocusedOnCurrentTab(true);
 }
 
 void FindBarHost::ClearResults(
@@ -372,16 +377,15 @@
 }
 
 void FindBarHost::RestoreSavedFocus() {
+  SetFindBarIsFocusedOnCurrentTab(false);
+
   std::unique_ptr<views::ExternalFocusTracker> focus_tracker_from_web_contents;
   views::ExternalFocusTracker* tracker = focus_tracker_.get();
-  if (!tracker) {
-    auto* web_contents = find_bar_controller_->web_contents();
-    if (web_contents) {
-      auto* helper = FindBarHostHelper::FromWebContents(web_contents);
-      if (helper) {
-        focus_tracker_from_web_contents = helper->TakeExternalFocusTracker();
-        tracker = focus_tracker_from_web_contents.get();
-      }
+  if (!tracker && web_contents()) {
+    auto* helper = FindBarHostHelper::FromWebContents(web_contents());
+    if (helper) {
+      focus_tracker_from_web_contents = helper->TakeExternalFocusTracker();
+      tracker = focus_tracker_from_web_contents.get();
     }
   }
 
@@ -390,7 +394,7 @@
     focus_tracker_.reset();
   } else {
     // TODO(brettw): Focus() should be on WebContentsView.
-    find_bar_controller_->web_contents()->Focus();
+    web_contents()->Focus();
   }
 }
 
@@ -506,8 +510,7 @@
 
 void FindBarHost::GetWidgetPositionNative(gfx::Rect* avoid_overlapping_rect) {
   gfx::Rect frame_rect = host_->GetTopLevelWidget()->GetWindowBoundsInScreen();
-  gfx::Rect webcontents_rect =
-      find_bar_controller_->web_contents()->GetViewBounds();
+  gfx::Rect webcontents_rect = web_contents()->GetViewBounds();
   avoid_overlapping_rect->Offset(0, webcontents_rect.y() - frame_rect.y());
 }
 
@@ -516,13 +519,12 @@
   // We only move the window if one is active for the current WebContents. If we
   // don't check this, then SetDialogPosition below will end up making the Find
   // Bar visible.
-  content::WebContents* web_contents = find_bar_controller_->web_contents();
-  if (!web_contents) {
+  if (!web_contents()) {
     return;
   }
 
   find_in_page::FindTabHelper* find_tab_helper =
-      find_in_page::FindTabHelper::FromWebContents(web_contents);
+      find_in_page::FindTabHelper::FromWebContents(web_contents());
   if (!find_tab_helper || !find_tab_helper->find_ui_active()) {
     return;
   }
@@ -536,26 +538,24 @@
 }
 
 void FindBarHost::SaveFocusTracker() {
-  auto* web_contents = find_bar_controller_->web_contents();
-  if (!web_contents) {
+  if (!web_contents()) {
     return;
   }
 
   if (focus_tracker_) {
     focus_tracker_->SetFocusManager(nullptr);
-    FindBarHostHelper::CreateOrGetFromWebContents(web_contents)
+    FindBarHostHelper::CreateOrGetFromWebContents(web_contents())
         ->SetExternalFocusTracker(std::move(focus_tracker_));
   }
 }
 
 void FindBarHost::RestoreOrCreateFocusTracker() {
-  auto* web_contents = find_bar_controller_->web_contents();
-  if (!web_contents) {
+  if (!web_contents()) {
     return;
   }
 
   std::unique_ptr<views::ExternalFocusTracker> focus_tracker =
-      FindBarHostHelper::CreateOrGetFromWebContents(web_contents)
+      FindBarHostHelper::CreateOrGetFromWebContents(web_contents())
           ->TakeExternalFocusTracker();
   if (focus_tracker) {
     focus_tracker_ = std::move(focus_tracker);
@@ -566,6 +566,13 @@
   }
 }
 
+void FindBarHost::SetFindBarIsFocusedOnCurrentTab(bool focus) {
+  if (web_contents()) {
+    find_in_page::FindTabHelper::FromWebContents(web_contents())
+        ->set_find_ui_focused(focus);
+  }
+}
+
 void FindBarHost::OnVisibilityChanged() {
   // Tell the immersive mode controller about the find bar's bounds. The
   // immersive mode controller uses the bounds to keep the top-of-window views
@@ -681,11 +688,16 @@
     // We are gaining focus from outside the dropdown widget so we must register
     // a handler for Escape.
     RegisterAccelerators();
+    SetFindBarIsFocusedOnCurrentTab(true);
   } else if (our_view_before && !our_view_now) {
     // We are losing focus to something outside our widget so we restore the
     // original handler for Escape.
     UnregisterAccelerators();
   }
+
+  if (!our_view_now) {
+    SetFindBarIsFocusedOnCurrentTab(false);
+  }
 }
 
 void FindBarHost::AnimationProgressed(const gfx::Animation* animation) {
diff --git a/chrome/browser/ui/views/find_bar_host.h b/chrome/browser/ui/views/find_bar_host.h
index 3846313..60af547 100644
--- a/chrome/browser/ui/views/find_bar_host.h
+++ b/chrome/browser/ui/views/find_bar_host.h
@@ -9,6 +9,7 @@
 
 #include "base/memory/raw_ptr.h"
 #include "chrome/browser/ui/find_bar/find_bar.h"
+#include "chrome/browser/ui/find_bar/find_bar_controller.h"
 #include "chrome/browser/ui/views/find_bar_view.h"
 #include "ui/gfx/geometry/rect.h"
 #include "ui/gfx/native_widget_types.h"
@@ -75,7 +76,7 @@
   // FindBar implementation:
   FindBarController* GetFindBarController() const override;
   void SetFindBarController(FindBarController* find_bar_controller) override;
-  void Show(bool animate) override;
+  void Show(bool animate, bool focus) override;
   void Hide(bool animate) override;
   void SetFocusAndSelection() override;
   void ClearResults(
@@ -119,6 +120,12 @@
   friend class FindInPageTest;
   friend class LegacyFindInPageTest;
 
+  // Return the current web contents.
+  content::WebContents* web_contents() {
+    return find_bar_controller_ ? find_bar_controller_->web_contents()
+                                : nullptr;
+  }
+
   // Allows implementation to tweak widget position.
   void GetWidgetPositionNative(gfx::Rect* avoid_overlapping_rect);
 
@@ -138,6 +145,11 @@
   // FindBarHost. If no focus tracker is set, creates one.
   void RestoreOrCreateFocusTracker();
 
+  // Call when the find bar gains or loses focus on current tab. Used to restore
+  // focus state in tab switching. i.e. the focus state should not change after
+  // switching away and then back to the current tab.
+  void SetFindBarIsFocusedOnCurrentTab(bool focused);
+
   // Called when `is_visible_` changes.
   void OnVisibilityChanged();
 
diff --git a/chrome/browser/ui/views/find_bar_views_interactive_uitest.cc b/chrome/browser/ui/views/find_bar_views_interactive_uitest.cc
index 649d1b57..655a12e 100644
--- a/chrome/browser/ui/views/find_bar_views_interactive_uitest.cc
+++ b/chrome/browser/ui/views/find_bar_views_interactive_uitest.cc
@@ -46,6 +46,7 @@
 #include "ui/views/focus/focus_manager.h"
 #include "ui/views/interaction/element_tracker_views.h"
 #include "ui/views/interaction/view_focus_observer.h"
+#include "ui/views/interaction/widget_focus_observer.h"
 #include "ui/views/style/platform_style.h"
 #include "ui/views/view.h"
 #include "ui/views/view_class_properties.h"
@@ -253,7 +254,8 @@
         ObserveState(
             views::test::kCurrentFocusedViewId,
             BrowserView::GetBrowserViewForBrowser(browser())->GetWidget()),
-        InstrumentTab(kTabId), NavigateWebContents(kTabId, url));
+        ObserveState(views::test::kCurrentWidgetFocus), InstrumentTab(kTabId),
+        NavigateWebContents(kTabId, url));
   }
 
   template <typename M>
@@ -633,6 +635,36 @@
       CheckHasFocus(ContentsWebView::kContentsWebViewElementId));
 }
 
+// Test for crbug.com/40164081. When a tab has find bar and the web content has
+// focus, the web content should retain the focus after switching the tab away
+// and then back.
+IN_PROC_BROWSER_TEST_F(FindBarViewsUiTest,
+                       FocusRetainedOnPageWhenFindBarIsOpenOnTabSwitch) {
+  const GURL page_a = embedded_test_server()->GetURL("/a.html");
+  const GURL page_b = embedded_test_server()->GetURL("/b.html");
+
+  RunTestSequence(
+      // Open tab A and show the Find bar.
+      Init(page_a), ShowFindBar(), EnsurePresent(FindBarView::kElementId),
+      CheckHasFocus(FindBarView::kTextField),
+      // Focus tab A content.
+      Focus(ContentsWebView::kContentsWebViewElementId),
+      CheckHasFocus(ContentsWebView::kContentsWebViewElementId),
+      // Open tab B.
+      AddInstrumentedTab(kTabBId, page_b), WaitForHide(FindBarView::kTextField),
+      // Switch to tab A
+      SelectTab(kTabStripElementId, 0), WaitForShow(FindBarView::kTextField),
+      // The browser frame should be active.
+      WaitForState(views::test::kCurrentWidgetFocus,
+                   [this]() {
+                     return BrowserView::GetBrowserViewForBrowser(browser())
+                         ->GetWidget()
+                         ->GetNativeView();
+                   }),
+      // The content view should be focused.
+      CheckHasFocus(ContentsWebView::kContentsWebViewElementId));
+}
+
 // FindInPage on Mac doesn't use prepopulated values. Search there is global.
 #if !BUILDFLAG(IS_MAC) && !defined(USE_AURA)
 // Flaky because the test server fails to start? See: http://crbug.com/96594.
diff --git a/chrome/browser/ui/views/frame/browser_view.cc b/chrome/browser/ui/views/frame/browser_view.cc
index 0597ca4b..b1fc0ba 100644
--- a/chrome/browser/ui/views/frame/browser_view.cc
+++ b/chrome/browser/ui/views/frame/browser_view.cc
@@ -1035,6 +1035,7 @@
 
   devtools_scrim_view_ =
       contents_container->AddChildView(std::make_unique<ScrimView>());
+  devtools_scrim_view_->layer()->SetName("DevtoolsScrimView");
 
   views::View* contents_view;
   if (base::FeatureList::IsEnabled(features::kSideBySide)) {
@@ -1042,7 +1043,7 @@
         this,
         base::BindRepeating(&BrowserView::ActivateWebContents,
                             base::Unretained(this)),
-        base::BindRepeating(&BrowserView::OnSplitTabResize,
+        base::BindRepeating(&BrowserView::ResizeWebContents,
                             base::Unretained(this)));
     multi_contents_view_ =
         contents_container->AddChildView(std::move(multi_contents_view));
@@ -1078,6 +1079,8 @@
 
   contents_scrim_view_ =
       contents_container->AddChildView(std::make_unique<ScrimView>());
+  contents_scrim_view_->layer()->SetName("ContentsScrimView");
+
 #if BUILDFLAG(ENABLE_GLIC)
   // `IsProfileEligible` returns true if the feature flags are present and the
   // profile can potentially enable the feature. If the feature is disabled the
@@ -1142,6 +1145,7 @@
   find_bar_host_view_ = AddChildView(std::make_unique<View>());
 
   window_scrim_view_ = AddChildView(std::make_unique<ScrimView>());
+  window_scrim_view_->layer()->SetName("WindowScrimView");
 
   UpgradeNotificationController::CreateForBrowser(browser_.get());
 
@@ -1543,7 +1547,56 @@
   multi_contents_view_->SetActiveIndex(relative_active_position);
 }
 
-void BrowserView::SwapTabsInActiveSplit() {
+void BrowserView::UpdateContentsInSplitView(
+    const std::vector<std::pair<tabs::TabInterface*, int>>& prev_tabs,
+    const std::vector<std::pair<tabs::TabInterface*, int>>& new_tabs) {
+  CHECK(multi_contents_view_ && multi_contents_view_->IsInSplitView());
+
+  std::optional<split_tabs::SplitTabId> split_id =
+      browser_->GetActiveTabInterface()->GetSplit();
+  CHECK(split_id.has_value());
+
+  split_tabs::SplitTabData* split_data =
+      browser_->tab_strip_model()->GetSplitData(split_id.value());
+  const int first_split_tab_index =
+      browser_->tab_strip_model()->GetIndexOfTab(split_data->ListTabs()[0]);
+
+  const bool active_view_has_focus =
+      multi_contents_view_->GetActiveContentsView()->HasFocus();
+
+  // Clear web contents for prev_tabs in preparation to reset for new_tabs.
+  for (std::pair<tabs::TabInterface*, int> split_tab_with_index : prev_tabs) {
+    CHECK(split_id == split_tab_with_index.first->GetSplit());
+    int relative_index = split_tab_with_index.second - first_split_tab_index;
+    multi_contents_view_->SetWebContentsAtIndex(nullptr, relative_index);
+  }
+  // Set web contents in multi_contents_view_ to match new_tabs and update the
+  // active multi_contents_view_ index.
+  for (std::pair<tabs::TabInterface*, int> split_tab_with_index : new_tabs) {
+    CHECK(split_id == split_tab_with_index.first->GetSplit());
+    int relative_index = split_tab_with_index.second - first_split_tab_index;
+    multi_contents_view_->SetWebContentsAtIndex(
+        split_tab_with_index.first->GetContents(), relative_index);
+    if (split_tab_with_index.first->IsActivated()) {
+      multi_contents_view_->SetActiveIndex(relative_index);
+    }
+  }
+  // Focus the active contents view if it previously had focus prior to swap.
+  if (active_view_has_focus) {
+    multi_contents_view_->GetActiveContentsView()->RequestFocus();
+  }
+}
+
+bool BrowserView::IsTabChangeInSplitView(content::WebContents* old_contents,
+                                         content::WebContents* new_contents) {
+  return multi_contents_view_ && multi_contents_view_->IsInSplitView() &&
+         multi_contents_view_->GetActiveContentsView()->web_contents() ==
+             old_contents &&
+         multi_contents_view_->GetInactiveContentsView()->web_contents() ==
+             new_contents;
+}
+
+void BrowserView::ReverseWebContents() {
   CHECK(multi_contents_view_);
   const int active_index = browser_->tab_strip_model()->active_index();
 
@@ -1554,13 +1607,14 @@
   browser_->tab_strip_model()->ReverseTabsInSplit(split_tab_id.value());
 }
 
-bool BrowserView::IsTabChangeInSplitView(content::WebContents* old_contents,
-                                         content::WebContents* new_contents) {
-  return multi_contents_view_ && multi_contents_view_->IsInSplitView() &&
-         multi_contents_view_->GetActiveContentsView()->web_contents() ==
-             old_contents &&
-         multi_contents_view_->GetInactiveContentsView()->web_contents() ==
-             new_contents;
+void BrowserView::ResizeWebContents(double start_ratio) {
+  const tabs::TabInterface* active_tab =
+      browser_->tab_strip_model()->GetActiveTab();
+
+  if (active_tab->GetSplit().has_value()) {
+    browser_->tab_strip_model()->UpdateSplitRatio(
+        active_tab->GetSplit().value(), start_ratio);
+  }
 }
 
 void BrowserView::ActivateWebContents(content::WebContents* web_contents) {
@@ -3868,85 +3922,56 @@
 ///////////////////////////////////////////////////////////////////////////////
 // BrowserView, TabStripModelObserver implementation:
 
-void BrowserView::OnSplitTabContentsUpdated(
-    split_tabs::SplitTabId split_id,
-    std::vector<std::pair<tabs::TabInterface*, int>> prev_tabs,
-    std::vector<std::pair<tabs::TabInterface*, int>> new_tabs) {
-  // If the updated split is not active, do nothing.
-  if (const tabs::TabInterface* active_tab = browser_->GetActiveTabInterface();
-      !active_tab || !active_tab->IsSplit() ||
-      active_tab->GetSplit().value() != split_id) {
-    return;
-  }
-
-  split_tabs::SplitTabData* split_data =
-      browser_->tab_strip_model()->GetSplitData(split_id);
-  const int first_split_tab_index =
-      browser_->tab_strip_model()->GetIndexOfTab(split_data->ListTabs()[0]);
-
-  const bool active_view_has_focus =
-      multi_contents_view_->GetActiveContentsView()->HasFocus();
-
-  // Clear web contents for prev_tabs in preparation to reset for new_tabs.
-  for (std::pair<tabs::TabInterface*, int> split_tab_with_index : prev_tabs) {
-    int relative_index = split_tab_with_index.second - first_split_tab_index;
-    multi_contents_view_->SetWebContentsAtIndex(nullptr, relative_index);
-  }
-  // Set web contents in multi_contents_view_ to match new_tabs and update the
-  // active multi_contents_view_ index.
-  for (std::pair<tabs::TabInterface*, int> split_tab_with_index : new_tabs) {
-    int relative_index = split_tab_with_index.second - first_split_tab_index;
-    multi_contents_view_->SetWebContentsAtIndex(
-        split_tab_with_index.first->GetContents(), relative_index);
-    if (split_tab_with_index.first->IsActivated()) {
-      multi_contents_view_->SetActiveIndex(relative_index);
-    }
-  }
-  // Focus the active contents view if it previously had focus prior to swap.
-  if (active_view_has_focus) {
-    multi_contents_view_->GetActiveContentsView()->RequestFocus();
-  }
-}
-
-void BrowserView::OnSplitTabCreated(
-    std::vector<std::pair<tabs::TabInterface*, int>> tabs,
-    split_tabs::SplitTabId split_id,
-    SplitTabAddReason reason,
-    split_tabs::SplitTabVisualData visual_data) {
-  const tabs::TabInterface* active_tab =
-      browser_->tab_strip_model()->GetActiveTab();
-  if (active_tab->IsSplit()) {
-    ShowSplitView(GetContentsView()->HasFocus());
-  }
-}
-
-void BrowserView::OnSplitTabRemoved(
-    std::vector<std::pair<tabs::TabInterface*, int>> tabs,
-    split_tabs::SplitTabId split_id,
-    SplitTabRemoveReason reason) {
+void BrowserView::OnSplitTabChanged(const SplitTabChange& change) {
   CHECK(multi_contents_view_);
-  content::WebContents* active_web_contents =
-      multi_contents_view_->GetActiveContentsView()->web_contents();
+  switch (change.type) {
+    case SplitTabChange::Type::kAdded: {
+      const tabs::TabInterface* active_tab =
+          browser_->tab_strip_model()->GetActiveTab();
+      if (active_tab->IsSplit()) {
+        ShowSplitView(GetContentsView()->HasFocus());
+      }
+      break;
+    }
 
-  if (std::any_of(tabs.begin(), tabs.end(),
-                  [active_web_contents](
-                      const std::pair<tabs::TabInterface*, int>& pair) {
-                    return pair.first->GetContents() == active_web_contents;
-                  })) {
-    HideSplitView();
-  }
-}
+    case SplitTabChange::Type::kVisualsChanged: {
+      const tabs::TabInterface* active_tab =
+          browser_->tab_strip_model()->GetActiveTab();
 
-void BrowserView::OnSplitTabVisualsChanged(
-    split_tabs::SplitTabId split_id,
-    split_tabs::SplitTabVisualData old_visual_data,
-    split_tabs::SplitTabVisualData new_visual_data) {
-  const tabs::TabInterface* active_tab =
-      browser_->tab_strip_model()->GetActiveTab();
+      if (active_tab->GetSplit() == change.split_id) {
+        if (change.GetVisualsChange()->new_visual_data().split_ratio() !=
+            change.GetVisualsChange()->old_visual_data().split_ratio()) {
+          multi_contents_view_->UpdateSplitRatio(
+              change.GetVisualsChange()->new_visual_data().split_ratio());
+        }
+      }
+      break;
+    }
 
-  if (active_tab->GetSplit() == split_id) {
-    if (new_visual_data.split_ratio() != old_visual_data.split_ratio()) {
-      multi_contents_view_->UpdateSplitRatio(new_visual_data.split_ratio());
+    case SplitTabChange::Type::kContentsChanged: {
+      const tabs::TabInterface* active_tab =
+          browser_->tab_strip_model()->GetActiveTab();
+
+      if (active_tab->GetSplit() == change.split_id) {
+        UpdateContentsInSplitView(change.GetContentsChange()->prev_tabs(),
+                                  change.GetContentsChange()->new_tabs());
+      }
+      break;
+    }
+
+    case SplitTabChange::Type::kRemoved: {
+      content::WebContents* active_web_contents =
+          multi_contents_view_->GetActiveContentsView()->web_contents();
+
+      if (std::any_of(change.GetRemovedChange()->tabs().begin(),
+                      change.GetRemovedChange()->tabs().end(),
+                      [active_web_contents](
+                          const std::pair<tabs::TabInterface*, int>& pair) {
+                        return pair.first->GetContents() == active_web_contents;
+                      })) {
+        HideSplitView();
+      }
+      break;
     }
   }
 }
@@ -4045,16 +4070,6 @@
   }
 }
 
-void BrowserView::OnSplitTabResize(double start_ratio) {
-  const tabs::TabInterface* active_tab =
-      browser_->tab_strip_model()->GetActiveTab();
-
-  if (active_tab->GetSplit().has_value()) {
-    browser_->tab_strip_model()->UpdateSplitRatio(
-        active_tab->GetSplit().value(), start_ratio);
-  }
-}
-
 ///////////////////////////////////////////////////////////////////////////////
 // BrowserView, ui::AcceleratorProvider implementation:
 
diff --git a/chrome/browser/ui/views/frame/browser_view.h b/chrome/browser/ui/views/frame/browser_view.h
index 469ac91..f9a8984 100644
--- a/chrome/browser/ui/views/frame/browser_view.h
+++ b/chrome/browser/ui/views/frame/browser_view.h
@@ -104,10 +104,6 @@
 enum class Channel;
 }
 
-namespace split_tabs {
-class SplitTabVisualData;
-}
-
 namespace views {
 class ExternalFocusTracker;
 class WebView;
@@ -492,17 +488,26 @@
   // side-by-side display.
   void HideSplitView();
 
-  // Update the index of the active split based on the active tab's web contents
+  // Update the index of the active split based on the active tab's web
+  // contents.
   void UpdateActiveTabInSplitView();
 
-  // Reverses the order of the tabs in the active split.
-  void SwapTabsInActiveSplit();
+  // Updates the contents in the active split view.
+  void UpdateContentsInSplitView(
+      const std::vector<std::pair<tabs::TabInterface*, int>>& prev_tabs,
+      const std::vector<std::pair<tabs::TabInterface*, int>>& new_tabs);
 
   // True if an activation from `old_contents` to `new_contents` happens between
   // tabs that are already in a split-view configuration.
   bool IsTabChangeInSplitView(content::WebContents* old_contents,
                               content::WebContents* new_contents);
 
+  // Reverses the order of the contents in the active split.
+  void ReverseWebContents();
+
+  // Resize the ratio of the contents in the active split.
+  void ResizeWebContents(double start_ratio);
+
   // Activate the tab containing the given WebContents (if any).
   void ActivateWebContents(content::WebContents* web_contents);
 
@@ -730,31 +735,15 @@
       TabStripModel* tab_strip_model,
       const TabStripModelChange& change,
       const TabStripSelectionChange& selection) override;
-  void OnSplitTabContentsUpdated(
-      split_tabs::SplitTabId split_id,
-      std::vector<std::pair<tabs::TabInterface*, int>> prev_tabs,
-      std::vector<std::pair<tabs::TabInterface*, int>> new_tabs) override;
   void TabChangedAt(content::WebContents* contents,
                     int index,
                     TabChangeType change_type) override;
-  void OnSplitTabCreated(std::vector<std::pair<tabs::TabInterface*, int>> tabs,
-                         split_tabs::SplitTabId split_id,
-                         SplitTabAddReason reason,
-                         split_tabs::SplitTabVisualData visual_data) override;
-  void OnSplitTabRemoved(std::vector<std::pair<tabs::TabInterface*, int>> tabs,
-                         split_tabs::SplitTabId split_id,
-                         SplitTabRemoveReason reason) override;
-  void OnSplitTabVisualsChanged(
-      split_tabs::SplitTabId split_id,
-      split_tabs::SplitTabVisualData old_visual_data,
-      split_tabs::SplitTabVisualData new_visual_data) override;
+  void OnSplitTabChanged(const SplitTabChange& change) override;
   void TabStripEmpty() override;
   void WillCloseAllTabs(TabStripModel* tab_strip_model) override;
   void CloseAllTabsStopped(TabStripModel* tab_strip_model,
                            CloseAllStoppedReason reason) override;
 
-  void OnSplitTabResize(double start_ratio);
-
   // ui::AcceleratorProvider:
   bool GetAcceleratorForCommandId(int command_id,
                                   ui::Accelerator* accelerator) const override;
diff --git a/chrome/browser/ui/views/frame/browser_view_browsertest.cc b/chrome/browser/ui/views/frame/browser_view_browsertest.cc
index 5c0d69f..b2d0d089 100644
--- a/chrome/browser/ui/views/frame/browser_view_browsertest.cc
+++ b/chrome/browser/ui/views/frame/browser_view_browsertest.cc
@@ -264,7 +264,7 @@
   EXPECT_EQ(full_bounds, contents_web_view()->bounds());
 }
 
-// Verifies that the side panel's rounded corner is being correctly laid out.
+// Verifies that the side panel's rounded corner is being correctly layed out.
 IN_PROC_BROWSER_TEST_F(BrowserViewTest, SidePanelRoundedCornerLayout) {
   SidePanelCoordinator* coordinator =
       (browser())->GetFeatures().side_panel_coordinator();
@@ -272,7 +272,7 @@
   coordinator->Show(SidePanelEntry::Id::kBookmarks);
   EXPECT_EQ(side_panel()->bounds().x(),
             side_panel_rounded_corner()->bounds().right());
-  EXPECT_EQ(side_panel()->bounds().y() - views::Separator::kThickness,
+  EXPECT_EQ(side_panel()->bounds().y(),
             side_panel_rounded_corner()->bounds().y());
 }
 
diff --git a/chrome/browser/ui/views/frame/browser_view_layout.cc b/chrome/browser/ui/views/frame/browser_view_layout.cc
index 663d78f2..f13479e0 100644
--- a/chrome/browser/ui/views/frame/browser_view_layout.cc
+++ b/chrome/browser/ui/views/frame/browser_view_layout.cc
@@ -85,15 +85,6 @@
 
 constexpr int BrowserViewLayout::kMainBrowserContentsMinimumWidth;
 
-struct BrowserViewLayout::ContentsContainerLayoutResult {
-  gfx::Rect contents_container_bounds;
-  gfx::Rect side_panel_bounds;
-  bool side_panel_visible;
-  bool side_panel_right_aligned;
-  bool contents_container_after_side_panel;
-  gfx::Rect separator_bounds;
-};
-
 class BrowserViewLayout::WebContentsModalDialogHostViews
     : public WebContentsModalDialogHost,
       public views::WidgetObserver {
@@ -121,35 +112,13 @@
     observer_list_.Notify(&ModalDialogHostObserver::OnPositionRequiresUpdate);
   }
 
-  gfx::Point GetDialogPosition(const gfx::Size& dialog_size) override {
+  gfx::Point GetDialogPosition(const gfx::Size& size) override {
     // Horizontally places the dialog at the center of the content.
-
     views::View* view = browser_view_layout_->contents_container_;
-    // Recalculate bounds of `contents_container_`. It may be stale due to
-    // pending layouts (from switching tabs, for example). The `top` and
-    // `bottom` parameters should not be relevant to the result, since we only
-    // care about the resulting width here.
-    BrowserViewLayout::ContentsContainerLayoutResult layout_result =
-        browser_view_layout_->CalculateContentsContainerLayout(
-            view->bounds().y(), view->bounds().bottom());
-
-    int leading_x;
-    if (base::i18n::IsRTL()) {
-      // Dialog coordinates are not flipped for RTL, but the View's coordinates
-      // are. Calculate the left edge of `contents_container_bounds`.
-      if (layout_result.contents_container_after_side_panel) {
-        leading_x = 0;
-      } else {
-        leading_x = browser_view_layout_->vertical_layout_rect_.width() -
-                    layout_result.contents_container_bounds.width();
-      }
-    } else {
-      leading_x = layout_result.contents_container_bounds.x();
-    }
-    const int middle_x =
-        leading_x + layout_result.contents_container_bounds.width() / 2;
-    return gfx::Point(middle_x - dialog_size.width() / 2,
-                      browser_view_layout_->dialog_top_y_);
+    gfx::Rect rect = view->ConvertRectToWidget(view->GetLocalBounds());
+    const int middle_x = rect.x() + rect.width() / 2;
+    const int top = browser_view_layout_->dialog_top_y_;
+    return gfx::Point(middle_x - size.width() / 2, top);
   }
 
   bool ShouldActivateDialog() const override {
@@ -644,8 +613,10 @@
   return content_top;
 }
 
-BrowserViewLayout::ContentsContainerLayoutResult
-BrowserViewLayout::CalculateContentsContainerLayout(int top, int bottom) const {
+void BrowserViewLayout::LayoutContentsContainerView(int top, int bottom) {
+  TRACE_EVENT0("ui", "BrowserViewLayout::LayoutContentsContainerView");
+  // |contents_container_| contains web page contents and devtools.
+  // See browser_view.h for details.
   gfx::Rect contents_container_bounds(vertical_layout_rect_.x(), top,
                                       vertical_layout_rect_.width(),
                                       std::max(0, bottom - top));
@@ -656,26 +627,47 @@
         gfx::Insets().set_bottom(-webui_tab_strip_->size().height()));
   }
 
-  const bool side_panel_visible =
-      unified_side_panel_ && unified_side_panel_->GetVisible();
-  if (!side_panel_visible) {
-    // The contents container takes all available space, and we're done.
-    return ContentsContainerLayoutResult{contents_container_bounds,
-                                         gfx::Rect(),
-                                         false,
-                                         false,
-                                         false,
-                                         gfx::Rect()};
+  LayoutSidePanelView(unified_side_panel_, contents_container_bounds);
+
+  contents_container_->SetBoundsRect(contents_container_bounds);
+}
+
+void BrowserViewLayout::LayoutSidePanelView(
+    views::View* side_panel,
+    gfx::Rect& contents_container_bounds) {
+  const bool side_panel_visible = side_panel && side_panel->GetVisible();
+  // Update side panel rounded corner visibility to match side panel visibility.
+  SetViewVisibility(side_panel_rounded_corner_, side_panel_visible);
+
+  if (left_aligned_side_panel_separator_) {
+    const bool side_panel_visible_on_left =
+        side_panel_visible &&
+        !views::AsViewClass<SidePanel>(unified_side_panel_)->IsRightAligned();
+    SetViewVisibility(left_aligned_side_panel_separator_,
+                      side_panel_visible_on_left);
   }
 
-  SidePanel* side_panel = views::AsViewClass<SidePanel>(unified_side_panel_);
+  if (right_aligned_side_panel_separator_) {
+    const bool side_panel_visible_on_right =
+        side_panel_visible &&
+        views::AsViewClass<SidePanel>(unified_side_panel_)->IsRightAligned();
+    SetViewVisibility(right_aligned_side_panel_separator_,
+                      side_panel_visible_on_right);
+  }
 
-  const bool side_panel_right_aligned = side_panel->IsRightAligned();
+  if (!side_panel || !side_panel->GetVisible()) {
+    return;
+  }
+
+  DCHECK(side_panel == unified_side_panel_);
+  bool is_right_aligned =
+      views::AsViewClass<SidePanel>(side_panel)->IsRightAligned();
+
   views::View* side_panel_separator =
-      side_panel_right_aligned ? right_aligned_side_panel_separator_.get()
-                               : left_aligned_side_panel_separator_.get();
-  CHECK(side_panel_separator);
-  const int separator_width = side_panel_separator->GetPreferredSize().width();
+      is_right_aligned ? right_aligned_side_panel_separator_.get()
+                       : left_aligned_side_panel_separator_.get();
+
+  DCHECK(side_panel_separator);
 
   // Side panel occupies some of the container's space. The side panel should
   // never occupy more space than is available in the content window, and
@@ -685,16 +677,16 @@
 
   // If necessary, cap the side panel width at 2/3rds of the contents container
   // width as long as the side panel remains at or above its minimum width.
-  if (side_panel->ShouldRestrictMaxWidth()) {
+  if (views::AsViewClass<SidePanel>(side_panel)->ShouldRestrictMaxWidth()) {
     side_panel_bounds.set_width(
         std::max(std::min(side_panel->GetPreferredSize().width(),
                           contents_container_bounds.width() * 2 / 3),
                  side_panel->GetMinimumSize().width()));
   } else {
-    side_panel_bounds.set_width(std::min(side_panel->GetPreferredSize().width(),
-                                         contents_container_bounds.width() -
-                                             GetMinWebContentsWidth() -
-                                             separator_width));
+    side_panel_bounds.set_width(
+        std::min(side_panel->GetPreferredSize().width(),
+                 contents_container_bounds.width() - GetMinWebContentsWidth() -
+                     side_panel_separator->GetPreferredSize().width()));
   }
 
   double side_panel_visible_width =
@@ -702,21 +694,23 @@
       views::AsViewClass<SidePanel>(unified_side_panel_)->GetAnimationValue();
 
   // Shrink container bounds to fit the side panel.
-  contents_container_bounds.set_width(contents_container_bounds.width() -
-                                      side_panel_visible_width -
-                                      separator_width);
+  contents_container_bounds.set_width(
+      contents_container_bounds.width() - side_panel_visible_width -
+      side_panel_separator->GetPreferredSize().width());
 
   // In LTR, the point (0,0) represents the top left of the browser.
   // In RTL, the point (0,0) represents the top right of the browser.
-  const bool contents_container_after_side_panel =
-      (base::i18n::IsRTL() && side_panel_right_aligned) ||
-      (!base::i18n::IsRTL() && !side_panel_right_aligned);
+  const bool is_container_after_side_panel =
+      (base::i18n::IsRTL() && is_right_aligned) ||
+      (!base::i18n::IsRTL() && !is_right_aligned);
 
-  if (contents_container_after_side_panel) {
+  if (is_container_after_side_panel) {
     // When the side panel should appear before the main content area relative
     // to the ui direction, move `contents_container_bounds` after the side
     // panel. Also leave space for the separator.
-    contents_container_bounds.set_x(side_panel_visible_width + separator_width);
+    contents_container_bounds.set_x(
+        side_panel_visible_width +
+        side_panel_separator->GetPreferredSize().width());
     side_panel_bounds.set_x(side_panel_bounds.x() - (side_panel_bounds.width() -
                                                      side_panel_visible_width));
   } else {
@@ -724,81 +718,47 @@
     // the ui direction, move `side_panel_bounds` after the main content area.
     // Also leave space for the separator.
     side_panel_bounds.set_x(contents_container_bounds.right() +
-                            separator_width);
+                            side_panel_separator->GetPreferredSize().width());
   }
 
+  side_panel->SetBoundsRect(side_panel_bounds);
+
   // Adjust the side panel separator bounds based on the side panel bounds
   // calculated above.
-  gfx::Rect separator_bounds = side_panel_bounds;
+  gfx::Rect side_panel_separator_bounds = side_panel_bounds;
   // TODO (https://crbug.com/389972209): Adding 1px to the width as a bandaid
   // fix. This covers a case with subpixeling where a thin line of the
   // background finds its way to the front.
-  separator_bounds.set_width(separator_width + 1);
+  side_panel_separator_bounds.set_width(
+      side_panel_separator->GetPreferredSize().width() + 1);
+
   // If the side panel appears before `contents_container_bounds`, place the
   // separator immediately after the side panel but before the container bounds.
   // If the side panel appears after `contents_container_bounds`, place the
   // separator immediately after the contents bounds but before the side panel.
-  separator_bounds.set_x(contents_container_after_side_panel
-                             ? side_panel_bounds.right()
-                             : contents_container_bounds.right());
+  side_panel_separator_bounds.set_x(is_container_after_side_panel
+                                        ? side_panel_bounds.right()
+                                        : contents_container_bounds.right());
 
-  return BrowserViewLayout::ContentsContainerLayoutResult{
-      contents_container_bounds,
-      side_panel_bounds,
-      side_panel_visible,
-      side_panel_right_aligned,
-      contents_container_after_side_panel,
-      separator_bounds};
-}
+  side_panel_separator->SetBoundsRect(side_panel_separator_bounds);
 
-void BrowserViewLayout::LayoutContentsContainerView(int top, int bottom) {
-  TRACE_EVENT0("ui", "BrowserViewLayout::LayoutContentsContainerView");
-  // |contents_container_| contains web page contents and devtools.
-  // See browser_view.h for details.
-
-  BrowserViewLayout::ContentsContainerLayoutResult layout_result =
-      CalculateContentsContainerLayout(top, bottom);
-
-  contents_container_->SetBoundsRect(layout_result.contents_container_bounds);
-
-  if (unified_side_panel_) {
-    unified_side_panel_->SetBoundsRect(layout_result.side_panel_bounds);
-  }
-  if (right_aligned_side_panel_separator_) {
-    SetViewVisibility(right_aligned_side_panel_separator_,
-                      layout_result.side_panel_visible &&
-                          layout_result.side_panel_right_aligned);
-    right_aligned_side_panel_separator_->SetBoundsRect(
-        layout_result.separator_bounds);
-  }
-  if (left_aligned_side_panel_separator_) {
-    SetViewVisibility(left_aligned_side_panel_separator_,
-                      layout_result.side_panel_visible &&
-                          !layout_result.side_panel_right_aligned);
-    left_aligned_side_panel_separator_->SetBoundsRect(
-        layout_result.separator_bounds);
-  }
-
-  if (side_panel_rounded_corner_) {
-    SetViewVisibility(side_panel_rounded_corner_,
-                      layout_result.side_panel_visible);
-    // Adjust the rounded corner bounds based on the side panel bounds.
-    const float corner_radius =
-        side_panel_rounded_corner_->GetLayoutProvider()->GetCornerRadiusMetric(
-            views::ShapeContextTokens::kSidePanelPageContentRadius);
-    const float corner_size = corner_radius + views::Separator::kThickness;
-    if (layout_result.contents_container_after_side_panel) {
-      side_panel_rounded_corner_->SetBounds(
-          layout_result.side_panel_bounds.right(),
-          layout_result.side_panel_bounds.y() - views::Separator::kThickness,
-          corner_size, corner_size);
-    } else {
-      side_panel_rounded_corner_->SetBounds(
-          layout_result.side_panel_bounds.x() - corner_radius -
-              views::Separator::kThickness,
-          layout_result.side_panel_bounds.y() - views::Separator::kThickness,
-          corner_size, corner_size);
-    }
+  // Adjust the side panel rounded corner bounds based on the side panel bounds
+  // calculated above.
+  const float corner_radius =
+      side_panel_rounded_corner_->GetLayoutProvider()->GetCornerRadiusMetric(
+          views::ShapeContextTokens::kSidePanelPageContentRadius);
+  if (is_container_after_side_panel) {
+    side_panel_rounded_corner_->SetBounds(
+        side_panel_bounds.right(),
+        side_panel_bounds.y() - views::Separator::kThickness,
+        corner_radius + views::Separator::kThickness,
+        corner_radius + views::Separator::kThickness);
+  } else {
+    side_panel_rounded_corner_->SetBounds(
+        side_panel_bounds.x() - corner_radius - views::Separator::kThickness,
+        side_panel_bounds.y() - views::Separator::kThickness,
+        corner_radius + views::Separator::kThickness,
+        corner_radius + views::Separator::kThickness);
   }
 }
 
diff --git a/chrome/browser/ui/views/frame/browser_view_layout.h b/chrome/browser/ui/views/frame/browser_view_layout.h
index 493d444..a4602bd 100644
--- a/chrome/browser/ui/views/frame/browser_view_layout.h
+++ b/chrome/browser/ui/views/frame/browser_view_layout.h
@@ -133,18 +133,16 @@
   int LayoutBookmarkBar(int top);
   int LayoutInfoBar(int top);
 
-  // Helper struct and function for LayoutContentsContainerView that calculates
-  // bounds for |contents_container_| and |unified_side_panel_|.
-  struct ContentsContainerLayoutResult;
-  ContentsContainerLayoutResult CalculateContentsContainerLayout(
-      int top,
-      int bottom) const;
-
   // Layout the |contents_container_| view between the coordinates |top| and
   // |bottom|. See browser_view.h for details of the relationship between
-  // |contents_container_| and other views. Also lays out |unified_side_panel_|.
+  // |contents_container_| and other views.
   void LayoutContentsContainerView(int top, int bottom);
 
+  // Layout the `side_panel`. This updates the passed in
+  // `contents_container_bounds` to accommodate the side panel.
+  void LayoutSidePanelView(views::View* side_panel,
+                           gfx::Rect& contents_container_bounds);
+
   // Updates |top_container_|'s bounds. The new bounds depend on the size of
   // the bookmark bar and the toolbar.
   void UpdateTopContainerBounds();
diff --git a/chrome/browser/ui/views/frame/multi_contents_view.cc b/chrome/browser/ui/views/frame/multi_contents_view.cc
index 5ab73de..014b356 100644
--- a/chrome/browser/ui/views/frame/multi_contents_view.cc
+++ b/chrome/browser/ui/views/frame/multi_contents_view.cc
@@ -148,7 +148,7 @@
 
 void MultiContentsView::OnSwap() {
   CHECK(IsInSplitView());
-  browser_view_->SwapTabsInActiveSplit();
+  browser_view_->ReverseWebContents();
 }
 
 void MultiContentsView::UpdateSplitRatio(double ratio) {
diff --git a/chrome/browser/ui/views/frame/multi_contents_view_drop_target_controller.cc b/chrome/browser/ui/views/frame/multi_contents_view_drop_target_controller.cc
index 4efa26a..2bf033af 100644
--- a/chrome/browser/ui/views/frame/multi_contents_view_drop_target_controller.cc
+++ b/chrome/browser/ui/views/frame/multi_contents_view_drop_target_controller.cc
@@ -4,6 +4,7 @@
 
 #include "chrome/browser/ui/views/frame/multi_contents_view_drop_target_controller.h"
 
+#include "base/task/single_thread_task_runner.h"
 #include "content/public/common/drop_data.h"
 #include "ui/views/view_class_properties.h"
 
@@ -24,10 +25,31 @@
   const bool should_show_drop_zone =
       data.url.is_valid() &&
       point.x() >= drop_target_view_->parent()->width() - kDropEntryPointWidth;
-  // TODO(crbug.com/394369035): Add a timer to delay showing the drop zone.
-  drop_target_view_->SetVisible(should_show_drop_zone);
+
+  UpdateDropTargetTimer(should_show_drop_zone);
 }
 
 void MultiContentsViewDropTargetController::OnWebContentsDragExit() {
-  drop_target_view_->SetVisible(false);
+  UpdateDropTargetTimer(/*should_run_timer=*/false);
+}
+
+void MultiContentsViewDropTargetController::UpdateDropTargetTimer(
+    bool should_run_timer) {
+  if (!should_run_timer) {
+    // The view itself isn't hidden immediately. If the view is already
+    // visible, then it has the responsibility of handling drags and hiding
+    // itself.
+    show_drop_target_timer_.Stop();
+  } else if (!drop_target_view_->GetVisible() &&
+             !show_drop_target_timer_.IsRunning()) {
+    // TODO(crbug.com/394369035): Settle on an appropriate value for this.
+    constexpr base::TimeDelta kDropTargetDelay = base::Seconds(1);
+    show_drop_target_timer_.Start(
+        FROM_HERE, kDropTargetDelay, this,
+        &MultiContentsViewDropTargetController::ShowDropTarget);
+  }
+}
+
+void MultiContentsViewDropTargetController::ShowDropTarget() {
+  drop_target_view_->SetVisible(true);
 }
diff --git a/chrome/browser/ui/views/frame/multi_contents_view_drop_target_controller.h b/chrome/browser/ui/views/frame/multi_contents_view_drop_target_controller.h
index 4aaf681..fdc3027 100644
--- a/chrome/browser/ui/views/frame/multi_contents_view_drop_target_controller.h
+++ b/chrome/browser/ui/views/frame/multi_contents_view_drop_target_controller.h
@@ -5,6 +5,8 @@
 #ifndef CHROME_BROWSER_UI_VIEWS_FRAME_MULTI_CONTENTS_VIEW_DROP_TARGET_CONTROLLER_H_
 #define CHROME_BROWSER_UI_VIEWS_FRAME_MULTI_CONTENTS_VIEW_DROP_TARGET_CONTROLLER_H_
 
+#include "base/time/time.h"
+#include "base/timer/timer.h"
 #include "ui/base/interaction/element_identifier.h"
 #include "ui/views/view.h"
 
@@ -33,9 +35,18 @@
   void OnWebContentsDragExit();
 
  private:
+  // Starts or stops the drop target timer according to `should_run_timer`.
+  void UpdateDropTargetTimer(bool should_run_timer);
+
+  void ShowDropTarget();
+
   // The view that is displayed when drags hover over the "drop" region of
   // the content area.
   const raw_ref<views::View> drop_target_view_;
+
+  // This timer is used for showing the drop target a delay, and may be
+  // canceled in case a drag exits the drop area before the target is shown.
+  base::OneShotTimer show_drop_target_timer_;
 };
 
 #endif  // CHROME_BROWSER_UI_VIEWS_FRAME_MULTI_CONTENTS_VIEW_DROP_TARGET_CONTROLLER_H_
diff --git a/chrome/browser/ui/views/frame/multi_contents_view_drop_target_controller_unittest.cc b/chrome/browser/ui/views/frame/multi_contents_view_drop_target_controller_unittest.cc
index 748063e..4d76e23 100644
--- a/chrome/browser/ui/views/frame/multi_contents_view_drop_target_controller_unittest.cc
+++ b/chrome/browser/ui/views/frame/multi_contents_view_drop_target_controller_unittest.cc
@@ -6,6 +6,7 @@
 
 #include <memory>
 
+#include "base/test/task_environment.h"
 #include "content/public/common/drop_data.h"
 #include "testing/gtest/include/gtest/gtest.h"
 #include "ui/base/interaction/element_identifier.h"
@@ -17,6 +18,12 @@
 
 static constexpr gfx::PointF kDragPointForDropTargetShow(450, 450);
 
+content::DropData ValidUrlDropData() {
+  content::DropData valid_url_data;
+  valid_url_data.url = GURL("https://mail.google.com");
+  return valid_url_data;
+}
+
 class MultiContentsViewDropTargetControllerTest : public testing::Test {
  public:
   MultiContentsViewDropTargetControllerTest() = default;
@@ -43,54 +50,64 @@
 
   views::View& drop_target_view() { return *drop_target_view_; }
 
+  // Fast forwards by an arbitrary time to ensure timed events are executed.
+  void FastForward() { task_environment_.FastForwardBy(base::Seconds(60)); }
+
+  void TriggerDropTargetShowTimer() {
+    controller().OnWebContentsDragUpdate(ValidUrlDropData(),
+                                         kDragPointForDropTargetShow);
+  }
+
  private:
   std::unique_ptr<MultiContentsViewDropTargetController> controller_;
   std::unique_ptr<views::View> multi_contents_view_;
   raw_ptr<views::View> drop_target_view_;
+
+  base::test::TaskEnvironment task_environment_{
+      base::test::TaskEnvironment::TimeSource::MOCK_TIME};
 };
 
 // Tests that the drop target is shown when a drag reaches enters the "drop
 // area" and a valid url is being dragged.
 TEST_F(MultiContentsViewDropTargetControllerTest,
        OnWebContentsDragUpdate_ShowDropTarget) {
-  ASSERT_FALSE(drop_target_view().GetVisible());
+  TriggerDropTargetShowTimer();
+  EXPECT_FALSE(drop_target_view().GetVisible());
 
-  content::DropData valid_url_data;
-  valid_url_data.url = GURL("https://mail.google.com");
-  controller().OnWebContentsDragUpdate(valid_url_data,
-                                       kDragPointForDropTargetShow);
-
+  FastForward();
   EXPECT_TRUE(drop_target_view().GetVisible());
 }
 
-// Tests that the drop target is hidden when an invalid url is being dragged.
+// Tests that the drop target is not shown when an invalid url is being dragged.
 TEST_F(MultiContentsViewDropTargetControllerTest,
        OnWebContentsDragUpdate_HideDropTargetOnInvalidURL) {
-  drop_target_view().SetVisible(true);
-  ASSERT_TRUE(drop_target_view().GetVisible());
-
   controller().OnWebContentsDragUpdate(content::DropData(),
                                        kDragPointForDropTargetShow);
 
+  FastForward();
   EXPECT_FALSE(drop_target_view().GetVisible());
 }
 
-// Tests that the drop target is hidden when a drag is not in the "drop area".
+// Tests that the drop target timer is cancelled when a drag is not in the
+// "drop area".
 TEST_F(MultiContentsViewDropTargetControllerTest,
        OnWebContentsDragUpdate_HideDropTargetOnOutOfBounds) {
-  drop_target_view().SetVisible(true);
-  ASSERT_TRUE(drop_target_view().GetVisible());
+  TriggerDropTargetShowTimer();
+  EXPECT_FALSE(drop_target_view().GetVisible());
 
-  content::DropData valid_url_data;
-  valid_url_data.url = GURL("https://mail.google.com");
-  controller().OnWebContentsDragUpdate(valid_url_data, gfx::PointF(0, 0));
-
+  controller().OnWebContentsDragUpdate(ValidUrlDropData(), gfx::PointF(0, 0));
+  FastForward();
   EXPECT_FALSE(drop_target_view().GetVisible());
 }
 
+// Tests that the drop target timer is cancelled when a drag exits the contents
+// view.
 TEST_F(MultiContentsViewDropTargetControllerTest, OnWebContentsDragExit) {
-  drop_target_view().SetVisible(true);
+  TriggerDropTargetShowTimer();
+  EXPECT_FALSE(drop_target_view().GetVisible());
+
   controller().OnWebContentsDragExit();
+  FastForward();
   EXPECT_FALSE(drop_target_view().GetVisible());
 }
 
diff --git a/chrome/browser/ui/views/page_action/collaboration_messaging_page_action_icon_view.cc b/chrome/browser/ui/views/page_action/collaboration_messaging_page_action_icon_view.cc
index 9603919e..29f04835 100644
--- a/chrome/browser/ui/views/page_action/collaboration_messaging_page_action_icon_view.cc
+++ b/chrome/browser/ui/views/page_action/collaboration_messaging_page_action_icon_view.cc
@@ -44,23 +44,25 @@
     ~CollaborationMessagingPageActionIconView() = default;
 
 void CollaborationMessagingPageActionIconView::UpdateImpl() {
+  // Get the current tab data.
   auto* tab_data = GetCollaborationTabData();
-  bool should_show_page_action = tab_data && tab_data->HasMessage();
-
-  if (should_show_page_action) {
-    UpdateContent(tab_data);
-  }
-
   if (tab_data) {
+    // Set a weak pointer to the current tab data. This will be used to get the
+    // icon when the page action needs it.
+    collaboration_messaging_tab_data_ = tab_data->GetWeakPtr();
+
+    // If the message changes, call this function again to update the page
+    // action.
     message_changed_callback_ =
         tab_data->RegisterMessageChangedCallback(base::BindRepeating(
             &CollaborationMessagingPageActionIconView::UpdateImpl,
             base::Unretained(this)));
   } else {
+    // Reset the callback.
     message_changed_callback_ = {};
   }
 
-  SetVisible(should_show_page_action);
+  UpdateContent(tab_data);
 }
 
 CollaborationMessagingTabData*
@@ -80,6 +82,13 @@
 
 void CollaborationMessagingPageActionIconView::UpdateContent(
     CollaborationMessagingTabData* collaboration_messaging_tab_data) {
+  bool should_show_page_action = collaboration_messaging_tab_data &&
+                                 collaboration_messaging_tab_data->HasMessage();
+  if (!should_show_page_action) {
+    SetVisible(false);
+    return;
+  }
+
   std::u16string label_text;
   switch (collaboration_messaging_tab_data->collaboration_event()) {
     case CollaborationEvent::TAB_ADDED:
@@ -95,13 +104,15 @@
       NOTREACHED();
   }
 
-  collaboration_messaging_tab_data_ =
-      collaboration_messaging_tab_data->GetWeakPtr();
+  // If the label text is empty, there is nothing to show.
+  if (label_text.empty()) {
+    SetVisible(false);
+    return;
+  }
 
-  // Label is always visible.
+  SetVisible(true);
   SetLabel(label_text);
   label()->SetVisible(true);
-
   UpdateIconImage();
 }
 
diff --git a/chrome/browser/ui/views/tabs/browser_tab_strip_controller.cc b/chrome/browser/ui/views/tabs/browser_tab_strip_controller.cc
index 8c420e5..c7e1f81 100644
--- a/chrome/browser/ui/views/tabs/browser_tab_strip_controller.cc
+++ b/chrome/browser/ui/views/tabs/browser_tab_strip_controller.cc
@@ -962,38 +962,38 @@
 #endif  // BUILDFLAG(IS_MAC)
 }
 
-void BrowserTabStripController::OnSplitTabCreated(
-    std::vector<std::pair<tabs::TabInterface*, int>> tabs,
-    split_tabs::SplitTabId split_id,
-    SplitTabAddReason reason,
-    split_tabs::SplitTabVisualData visual_data) {
-  std::vector<int> split_indices;
-  std::transform(
-      tabs.begin(), tabs.end(), std::back_inserter(split_indices),
-      [](const std::pair<tabs::TabInterface*, int>& p) { return p.second; });
+void BrowserTabStripController::OnSplitTabChanged(
+    const SplitTabChange& change) {
+  if (change.type == SplitTabChange::Type::kAdded) {
+    std::vector<int> split_indices;
+    std::transform(
+        change.GetAddedChange()->tabs().begin(),
+        change.GetAddedChange()->tabs().end(),
+        std::back_inserter(split_indices),
+        [](const std::pair<tabs::TabInterface*, int>& p) { return p.second; });
 
-  tabstrip_->OnSplitCreated(split_indices, split_id);
+    tabstrip_->OnSplitCreated(split_indices, change.split_id);
 
-  // Stop animating if we are updating an active split.
-  if (reason != SplitTabAddReason::kNewSplitTabAdded) {
-    tabstrip_->StopAnimating(true);
-  }
-}
+    // Stop animating if we are updating an active split.
+    if (change.GetAddedChange()->reason() !=
+        SplitTabChange::SplitTabAddReason::kNewSplitTabAdded) {
+      tabstrip_->StopAnimating(true);
+    }
+  } else if (change.type == SplitTabChange::Type::kRemoved) {
+    std::vector<int> split_indices;
+    std::transform(
+        change.GetRemovedChange()->tabs().begin(),
+        change.GetRemovedChange()->tabs().end(),
+        std::back_inserter(split_indices),
+        [](const std::pair<tabs::TabInterface*, int>& p) { return p.second; });
 
-void BrowserTabStripController::OnSplitTabRemoved(
-    std::vector<std::pair<tabs::TabInterface*, int>> tabs,
-    split_tabs::SplitTabId split_id,
-    SplitTabRemoveReason reason) {
-  std::vector<int> split_indices;
-  std::transform(
-      tabs.begin(), tabs.end(), std::back_inserter(split_indices),
-      [](const std::pair<tabs::TabInterface*, int>& p) { return p.second; });
+    tabstrip_->OnSplitRemoved(split_indices);
 
-  tabstrip_->OnSplitRemoved(split_indices);
-
-  // Stop animating if we are updating an active split.
-  if (reason != SplitTabRemoveReason::kSplitTabRemoved) {
-    tabstrip_->StopAnimating(true);
+    // Stop animating if we are updating an active split.
+    if (change.GetRemovedChange()->reason() !=
+        SplitTabChange::SplitTabRemoveReason::kSplitTabRemoved) {
+      tabstrip_->StopAnimating(true);
+    }
   }
 }
 
diff --git a/chrome/browser/ui/views/tabs/browser_tab_strip_controller.h b/chrome/browser/ui/views/tabs/browser_tab_strip_controller.h
index 741e9a3..04e9100c 100644
--- a/chrome/browser/ui/views/tabs/browser_tab_strip_controller.h
+++ b/chrome/browser/ui/views/tabs/browser_tab_strip_controller.h
@@ -35,10 +35,6 @@
 class TabGroupVisualData;
 }  // namespace tab_groups
 
-namespace split_tabs {
-class SplitTabVisualData;
-}
-
 namespace ui {
 class ListSelectionModel;
 }
@@ -161,14 +157,7 @@
                               int index) override;
   void SetTabNeedsAttentionAt(int index, bool attention) override;
   bool IsFrameButtonsRightAligned() const override;
-
-  void OnSplitTabCreated(std::vector<std::pair<tabs::TabInterface*, int>> tabs,
-                         split_tabs::SplitTabId split_id,
-                         SplitTabAddReason reason,
-                         split_tabs::SplitTabVisualData visual_data) override;
-  void OnSplitTabRemoved(std::vector<std::pair<tabs::TabInterface*, int>> tabs,
-                         split_tabs::SplitTabId split_id,
-                         SplitTabRemoveReason reason) override;
+  void OnSplitTabChanged(const SplitTabChange& change) override;
 
   const Browser* browser() const { return browser_view_->browser(); }
 
diff --git a/chrome/browser/ui/views/toolbar/split_tabs_button.cc b/chrome/browser/ui/views/toolbar/split_tabs_button.cc
index c81260d..439242c 100644
--- a/chrome/browser/ui/views/toolbar/split_tabs_button.cc
+++ b/chrome/browser/ui/views/toolbar/split_tabs_button.cc
@@ -67,22 +67,17 @@
     TabStripModel* tab_strip_model,
     const TabStripModelChange& change,
     const TabStripSelectionChange& selection) {
-  UpdateButtonVisibility();
+  if (selection.active_tab_changed()) {
+    UpdateButtonVisibility();
+  }
 }
 
-void SplitTabsToolbarButton::OnSplitTabCreated(
-    std::vector<std::pair<tabs::TabInterface*, int>> tabs,
-    split_tabs::SplitTabId split_id,
-    TabStripModelObserver::SplitTabAddReason reason,
-    split_tabs::SplitTabVisualData visual_data) {
-  UpdateButtonVisibility();
-}
-
-void SplitTabsToolbarButton::OnSplitTabRemoved(
-    std::vector<std::pair<tabs::TabInterface*, int>> tabs,
-    split_tabs::SplitTabId split_id,
-    SplitTabRemoveReason reason) {
-  UpdateButtonVisibility();
+void SplitTabsToolbarButton::OnSplitTabChanged(const SplitTabChange& change) {
+  if (change.type == SplitTabChange::Type::kAdded ||
+      change.type == SplitTabChange::Type::kRemoved ||
+      change.type == SplitTabChange::Type::kContentsChanged) {
+    UpdateButtonVisibility();
+  }
 }
 
 void SplitTabsToolbarButton::ButtonPressed(const ui::Event& event) {
diff --git a/chrome/browser/ui/views/toolbar/split_tabs_button.h b/chrome/browser/ui/views/toolbar/split_tabs_button.h
index 3943e6b..16be238b 100644
--- a/chrome/browser/ui/views/toolbar/split_tabs_button.h
+++ b/chrome/browser/ui/views/toolbar/split_tabs_button.h
@@ -15,10 +15,6 @@
 
 class Browser;
 
-namespace split_tabs {
-class SplitTabVisualData;
-}
-
 class SplitTabsToolbarButton : public ToolbarButton, TabStripModelObserver {
   METADATA_HEADER(SplitTabsToolbarButton, ToolbarButton)
 
@@ -38,14 +34,7 @@
       TabStripModel* tab_strip_model,
       const TabStripModelChange& change,
       const TabStripSelectionChange& selection) override;
-  void OnSplitTabCreated(std::vector<std::pair<tabs::TabInterface*, int>> tabs,
-                         split_tabs::SplitTabId split_id,
-                         TabStripModelObserver::SplitTabAddReason reason,
-                         split_tabs::SplitTabVisualData visual_data) override;
-
-  void OnSplitTabRemoved(std::vector<std::pair<tabs::TabInterface*, int>> tabs,
-                         split_tabs::SplitTabId split_id,
-                         SplitTabRemoveReason reason) override;
+  void OnSplitTabChanged(const SplitTabChange& change) override;
 
   const std::optional<ToolbarButton::VectorIcons>& GetIconsForTesting();
 
diff --git a/chrome/browser/ui/webui/ash/cloud_upload/cloud_open_metrics.cc b/chrome/browser/ui/webui/ash/cloud_upload/cloud_open_metrics.cc
index c7c93bc..5e90db0 100644
--- a/chrome/browser/ui/webui/ash/cloud_upload/cloud_open_metrics.cc
+++ b/chrome/browser/ui/webui/ash/cloud_upload/cloud_open_metrics.cc
@@ -81,6 +81,7 @@
     case OfficeTaskResult::kFallbackQuickOfficeAfterOpen:
     case OfficeTaskResult::kCancelledAtFallbackAfterOpen:
     case OfficeTaskResult::kCannotGetFallbackChoiceAfterOpen:
+    case OfficeTaskResult::kCannotGetSourceType:
       return false;
   }
 }
@@ -112,6 +113,7 @@
     case OfficeTaskResult::kCannotShowMoveConfirmation:
     case OfficeTaskResult::kNoFilesToOpen:
     case OfficeTaskResult::kFileAlreadyBeingOpened:
+    case OfficeTaskResult::kCannotGetSourceType:
       return false;
   }
 }
@@ -143,6 +145,7 @@
     case OfficeTaskResult::kCancelledAtFallbackAfterOpen:
     case OfficeTaskResult::kCannotGetFallbackChoiceAfterOpen:
     case OfficeTaskResult::kFileAlreadyBeingOpened:
+    case OfficeTaskResult::kCannotGetSourceType:
       return false;
   }
 }
@@ -261,7 +264,13 @@
     } else {
       // CloudOpenTask::OpenOrMoveFiles() was called.
       ExpectLogged(source_volume);
-      ExpectLogged(transfer_required);
+      if (task_result.value == OfficeTaskResult::kCannotGetSourceType) {
+        // Special case where an upload was required but type of upload couldn't
+        // be determined.
+        ExpectNotLogged(transfer_required);
+      } else {
+        ExpectLogged(transfer_required);
+      }
       if (DidEndAtFallback(task_result.value)) {
         // The cloud open/upload flow was exited at the Fallback Dialog after
         // an open was attempted. OpenErrors should give a fallback reason.
@@ -707,6 +716,7 @@
           case OfficeTaskResult::kCancelledAtFallbackAfterOpen:
           case OfficeTaskResult::kCannotGetFallbackChoiceAfterOpen:
           case OfficeTaskResult::kFileAlreadyBeingOpened:
+          case OfficeTaskResult::kCannotGetSourceType:
             SetWrongValueLogged(task_result);
             break;
         }
diff --git a/chrome/browser/ui/webui/ash/cloud_upload/cloud_open_metrics_unittest.cc b/chrome/browser/ui/webui/ash/cloud_upload/cloud_open_metrics_unittest.cc
index 53a3a07..1f9df47 100644
--- a/chrome/browser/ui/webui/ash/cloud_upload/cloud_open_metrics_unittest.cc
+++ b/chrome/browser/ui/webui/ash/cloud_upload/cloud_open_metrics_unittest.cc
@@ -1007,4 +1007,31 @@
   ASSERT_EQ(1, CloudOpenMetricsTest::number_of_dump_calls());
 }
 
+// Tests that the TransferRequired companion metric is set correctly when
+// TaskResult is logged as kCannotGetSourceType.
+TEST_F(CloudOpenMetricsTest,
+       MetricsConsistentWhenTaskResultIsCannotGetSourceType) {
+  {
+    CloudOpenMetrics cloud_open_metrics(CloudProvider::kOneDrive,
+                                        /*file_count=*/1);
+    cloud_open_metrics.LogTaskResult(OfficeTaskResult::kCannotGetSourceType);
+  }
+  histogram_.ExpectUniqueSample(kOneDriveTransferRequiredMetricStateMetric,
+                                MetricState::kCorrectlyNotLogged, 1);
+}
+
+// Tests that the TransferRequired companion metric is set correctly when
+// TaskResult is logged as kCannotGetSourceType but TransferRequired is logged.
+TEST_F(CloudOpenMetricsTest,
+       MetricsInconsistentWhenTaskResultIsCannotGetSourceType) {
+  {
+    CloudOpenMetrics cloud_open_metrics(CloudProvider::kOneDrive,
+                                        /*file_count=*/1);
+    cloud_open_metrics.LogTaskResult(OfficeTaskResult::kCannotGetSourceType);
+    cloud_open_metrics.LogTransferRequired(OfficeFilesTransferRequired::kCopy);
+  }
+  histogram_.ExpectUniqueSample(kOneDriveTransferRequiredMetricStateMetric,
+                                MetricState::kIncorrectlyLogged, 1);
+}
+
 }  // namespace ash::cloud_upload
diff --git a/chrome/browser/ui/webui/ash/cloud_upload/cloud_upload_dialog.cc b/chrome/browser/ui/webui/ash/cloud_upload/cloud_upload_dialog.cc
index b2c368b..24f2b94 100644
--- a/chrome/browser/ui/webui/ash/cloud_upload/cloud_upload_dialog.cc
+++ b/chrome/browser/ui/webui/ash/cloud_upload/cloud_upload_dialog.cc
@@ -484,8 +484,17 @@
     LOG(ERROR) << "Cannot get EventRouter";
   }
 
-  scoped_refptr<CloudOpenTask> upload_task = WrapRefCounted(new CloudOpenTask(
-      profile, file_urls, task, cloud_provider, std::move(cloud_open_metrics)));
+  std::optional<SourceType> source_type =
+      GetSourceType(profile, file_urls.front());
+  if (!source_type.has_value()) {
+    LOG(ERROR) << "Cannot get source type";
+    cloud_open_metrics->LogTaskResult(OfficeTaskResult::kCannotGetSourceType);
+    return false;
+  }
+
+  scoped_refptr<CloudOpenTask> upload_task = WrapRefCounted(
+      new CloudOpenTask(profile, file_urls, task, source_type.value(),
+                        cloud_provider, std::move(cloud_open_metrics)));
   // Keep `upload_task` alive until `TaskFinished` executes.
   bool status = upload_task->ExecuteInternal();
   return status;
@@ -495,11 +504,13 @@
     Profile* profile,
     std::vector<storage::FileSystemURL> file_urls,
     const fm_tasks::TaskDescriptor& task,
+    const SourceType source_type,
     const CloudProvider cloud_provider,
     std::unique_ptr<CloudOpenMetrics> cloud_open_metrics)
     : profile_(profile),
       file_urls_(file_urls),
       task_(task),
+      source_type_(source_type),
       cloud_provider_(cloud_provider),
       cloud_open_metrics_(std::move(cloud_open_metrics)) {
   BrowserList::AddObserver(this);
@@ -621,10 +632,9 @@
   }
 
   // The files need to be moved.
-  auto operation =
-      GetUploadType(profile_, file_urls_.front()) == UploadType::kCopy
-          ? OfficeFilesTransferRequired::kCopy
-          : OfficeFilesTransferRequired::kMove;
+  auto operation = SourceTypeToUploadType(source_type_) == UploadType::kCopy
+                       ? OfficeFilesTransferRequired::kCopy
+                       : OfficeFilesTransferRequired::kMove;
   // Set as WARNING as INFO is not allowed.
   LOG(WARNING) << (operation == OfficeFilesTransferRequired::kCopy ? "Copy"
                                                                    : "Mov")
@@ -736,10 +746,8 @@
 // file to a cloud location and opening it.
 bool CloudOpenTask::ShouldShowConfirmationDialog() {
   bool force_show_confirmation_dialog = false;
-  SourceType source_type = GetSourceType(profile_, file_urls_[0]);
-
   if (cloud_provider_ == CloudProvider::kGoogleDrive) {
-    switch (source_type) {
+    switch (source_type_) {
       case SourceType::READ_ONLY:
         force_show_confirmation_dialog =
             !fm_tasks::GetOfficeMoveConfirmationShownForLocalToDrive(
@@ -758,7 +766,7 @@
     return force_show_confirmation_dialog ||
            !fm_tasks::GetAlwaysMoveOfficeFilesToDrive(profile_);
   } else if (cloud_provider_ == CloudProvider::kOneDrive) {
-    switch (source_type) {
+    switch (source_type_) {
       case SourceType::READ_ONLY:
         force_show_confirmation_dialog =
             !fm_tasks::GetOfficeMoveConfirmationShownForLocalToOneDrive(
@@ -950,6 +958,7 @@
   DCHECK_LT(file_urls_idx_, file_urls_.size());
   drive_upload_handler_ = std::make_unique<DriveUploadHandler>(
       profile_, file_urls_[file_urls_idx_],
+      SourceTypeToUploadType(source_type_),
       base::BindOnce(&CloudOpenTask::FinishedDriveUpload, this),
       cloud_open_metrics_->GetSafeRef());
   drive_upload_handler_->Run();
@@ -959,6 +968,7 @@
   DCHECK_LT(file_urls_idx_, file_urls_.size());
   one_drive_upload_handler_ = std::make_unique<OneDriveUploadHandler>(
       profile_, file_urls_[file_urls_idx_],
+      SourceTypeToUploadType(source_type_),
       base::BindOnce(&CloudOpenTask::FinishedOneDriveUpload, this,
                      profile_->GetWeakPtr()),
       cloud_open_metrics_->GetSafeRef());
@@ -1139,7 +1149,7 @@
       auto move_confirmation_one_drive_dialog_args =
           mojom::MoveConfirmationOneDriveDialogArgs::New();
       move_confirmation_one_drive_dialog_args->operation_type =
-          UploadTypeToOperationType(GetUploadType(profile_, file_urls_[0]));
+          UploadTypeToOperationType(SourceTypeToUploadType(source_type_));
       args->dialog_specific_args =
           mojom::DialogSpecificArgs::NewMoveConfirmationOneDriveDialogArgs(
               std::move(move_confirmation_one_drive_dialog_args));
@@ -1149,7 +1159,7 @@
       auto move_confirmation_google_drive_dialog_args =
           mojom::MoveConfirmationGoogleDriveDialogArgs::New();
       move_confirmation_google_drive_dialog_args->operation_type =
-          UploadTypeToOperationType(GetUploadType(profile_, file_urls_[0]));
+          UploadTypeToOperationType(SourceTypeToUploadType(source_type_));
       args->dialog_specific_args =
           mojom::DialogSpecificArgs::NewMoveConfirmationGoogleDriveDialogArgs(
               std::move(move_confirmation_google_drive_dialog_args));
@@ -1372,8 +1382,7 @@
   // (and for StartUpload?).
   if (user_response == kUserActionUploadToGoogleDrive) {
     fm_tasks::SetOfficeMoveConfirmationShownForDrive(profile_, true);
-    SourceType source_type = GetSourceType(profile_, file_urls_[0]);
-    switch (source_type) {
+    switch (source_type_) {
       case SourceType::LOCAL:
         fm_tasks::SetOfficeMoveConfirmationShownForLocalToDrive(profile_, true);
         break;
@@ -1387,8 +1396,7 @@
     StartUpload();
   } else if (user_response == kUserActionUploadToOneDrive) {
     fm_tasks::SetOfficeMoveConfirmationShownForOneDrive(profile_, true);
-    SourceType source_type = GetSourceType(profile_, file_urls_[0]);
-    switch (source_type) {
+    switch (source_type_) {
       case SourceType::LOCAL:
         fm_tasks::SetOfficeMoveConfirmationShownForLocalToOneDrive(profile_,
                                                                    true);
diff --git a/chrome/browser/ui/webui/ash/cloud_upload/cloud_upload_dialog.h b/chrome/browser/ui/webui/ash/cloud_upload/cloud_upload_dialog.h
index f7efda0..9b10305e 100644
--- a/chrome/browser/ui/webui/ash/cloud_upload/cloud_upload_dialog.h
+++ b/chrome/browser/ui/webui/ash/cloud_upload/cloud_upload_dialog.h
@@ -170,6 +170,7 @@
   CloudOpenTask(Profile* profile,
                 std::vector<storage::FileSystemURL> file_urls,
                 const ::file_manager::file_tasks::TaskDescriptor& task,
+                const SourceType source_type,
                 const CloudProvider cloud_provider,
                 std::unique_ptr<CloudOpenMetrics> cloud_open_metrics);
 
@@ -249,6 +250,7 @@
   // File being currently uploaded.
   size_t file_urls_idx_ = 0;
   const ::file_manager::file_tasks::TaskDescriptor task_;
+  SourceType source_type_;
   CloudProvider cloud_provider_;
   std::unique_ptr<CloudOpenMetrics> cloud_open_metrics_;
   std::unique_ptr<DriveUploadHandler> drive_upload_handler_;
diff --git a/chrome/browser/ui/webui/ash/cloud_upload/cloud_upload_dialog_browsertest.cc b/chrome/browser/ui/webui/ash/cloud_upload/cloud_upload_dialog_browsertest.cc
index afbba4ac..0a317e2 100644
--- a/chrome/browser/ui/webui/ash/cloud_upload/cloud_upload_dialog_browsertest.cc
+++ b/chrome/browser/ui/webui/ash/cloud_upload/cloud_upload_dialog_browsertest.cc
@@ -1179,7 +1179,8 @@
 
     auto cloud_open_task = base::WrapRefCounted(new CloudOpenTask(
         profile(), files_, file_manager::file_tasks::TaskDescriptor(),
-        CloudProvider::kGoogleDrive, std::move(cloud_open_metrics)));
+        SourceType::LOCAL, CloudProvider::kGoogleDrive,
+        std::move(cloud_open_metrics)));
     cloud_open_task->SetTasksForTest(tasks_);
 
     for (int selected_task = 0; selected_task < num_tasks_; selected_task++) {
@@ -1215,7 +1216,8 @@
   {
     auto cloud_open_task = base::WrapRefCounted(new CloudOpenTask(
         profile(), files_, file_manager::file_tasks::TaskDescriptor(),
-        CloudProvider::kGoogleDrive, std::move(cloud_open_metrics)));
+        SourceType::LOCAL, CloudProvider::kGoogleDrive,
+        std::move(cloud_open_metrics)));
     cloud_open_task->SetTasksForTest(tasks_);
 
     int out_of_range_task = num_tasks_;
@@ -1561,7 +1563,7 @@
 
   auto cloud_open_task = base::WrapRefCounted(new CloudOpenTask(
       profile(), files_, file_manager::file_tasks::TaskDescriptor(),
-      CloudProvider::kOneDrive,
+      SourceType::LOCAL, CloudProvider::kOneDrive,
       std::make_unique<CloudOpenMetrics>(CloudProvider::kOneDrive,
                                          /*file_count=*/1)));
   mojom::DialogArgsPtr args =
@@ -1641,7 +1643,7 @@
 
   auto cloud_open_task = base::WrapRefCounted(new CloudOpenTask(
       profile(), files_, file_manager::file_tasks::TaskDescriptor(),
-      CloudProvider::kOneDrive,
+      SourceType::LOCAL, CloudProvider::kOneDrive,
       std::make_unique<CloudOpenMetrics>(CloudProvider::kOneDrive,
                                          /*file_count=*/1)));
   mojom::DialogArgsPtr args =
@@ -1723,7 +1725,7 @@
 
     upload_task_ = base::WrapRefCounted(new ash::cloud_upload::CloudOpenTask(
         profile(), source_files_, file_manager::file_tasks::TaskDescriptor(),
-        ash::cloud_upload::CloudProvider::kGoogleDrive,
+        SourceType::LOCAL, ash::cloud_upload::CloudProvider::kGoogleDrive,
         std::make_unique<CloudOpenMetrics>(CloudProvider::kGoogleDrive,
                                            /*file_count=*/1)));
   }
@@ -1738,7 +1740,7 @@
 
     upload_task_ = base::WrapRefCounted(new ash::cloud_upload::CloudOpenTask(
         profile(), source_files_, file_manager::file_tasks::TaskDescriptor(),
-        ash::cloud_upload::CloudProvider::kGoogleDrive,
+        SourceType::CLOUD, ash::cloud_upload::CloudProvider::kGoogleDrive,
         std::make_unique<CloudOpenMetrics>(CloudProvider::kGoogleDrive,
                                            /*file_count=*/1)));
   }
@@ -1753,7 +1755,7 @@
 
     upload_task_ = base::WrapRefCounted(new ash::cloud_upload::CloudOpenTask(
         profile(), source_files_, file_manager::file_tasks::TaskDescriptor(),
-        ash::cloud_upload::CloudProvider::kGoogleDrive,
+        SourceType::READ_ONLY, ash::cloud_upload::CloudProvider::kGoogleDrive,
         std::make_unique<CloudOpenMetrics>(CloudProvider::kGoogleDrive,
                                            /*file_count=*/1)));
   }
@@ -1768,7 +1770,7 @@
 
     upload_task_ = base::WrapRefCounted(new ash::cloud_upload::CloudOpenTask(
         profile(), source_files_, file_manager::file_tasks::TaskDescriptor(),
-        ash::cloud_upload::CloudProvider::kOneDrive,
+        SourceType::LOCAL, ash::cloud_upload::CloudProvider::kOneDrive,
         std::make_unique<CloudOpenMetrics>(CloudProvider::kOneDrive,
                                            /*file_count=*/1)));
   }
@@ -1783,7 +1785,7 @@
 
     upload_task_ = base::WrapRefCounted(new ash::cloud_upload::CloudOpenTask(
         profile(), source_files_, file_manager::file_tasks::TaskDescriptor(),
-        ash::cloud_upload::CloudProvider::kOneDrive,
+        SourceType::CLOUD, ash::cloud_upload::CloudProvider::kOneDrive,
         std::make_unique<CloudOpenMetrics>(CloudProvider::kOneDrive,
                                            /*file_count=*/1)));
   }
@@ -1798,7 +1800,7 @@
 
     upload_task_ = base::WrapRefCounted(new ash::cloud_upload::CloudOpenTask(
         profile(), source_files_, file_manager::file_tasks::TaskDescriptor(),
-        ash::cloud_upload::CloudProvider::kOneDrive,
+        SourceType::READ_ONLY, ash::cloud_upload::CloudProvider::kOneDrive,
         std::make_unique<CloudOpenMetrics>(CloudProvider::kOneDrive,
                                            /*file_count=*/1)));
   }
diff --git a/chrome/browser/ui/webui/ash/cloud_upload/cloud_upload_util.cc b/chrome/browser/ui/webui/ash/cloud_upload/cloud_upload_util.cc
index ae84eb4..a7b1efc 100644
--- a/chrome/browser/ui/webui/ash/cloud_upload/cloud_upload_util.cc
+++ b/chrome/browser/ui/webui/ash/cloud_upload/cloud_upload_util.cc
@@ -122,20 +122,18 @@
   }
 }
 
-SourceType GetSourceType(Profile* profile,
-                         const storage::FileSystemURL& source_url) {
+std::optional<SourceType> GetSourceType(
+    Profile* profile,
+    const storage::FileSystemURL& source_url) {
   file_manager::VolumeManager* volume_manager =
       file_manager::VolumeManager::Get(profile);
   base::WeakPtr<file_manager::Volume> source_volume =
       volume_manager->FindVolumeFromPath(source_url.path());
-  DCHECK(source_volume)
-      << "Unable to find source volume (source path filesystem_id: "
-      << source_url.filesystem_id() << ")";
   // Local by default.
   if (!source_volume) {
     LOG(ERROR) << "Unable to find source volume (source path filesystem_id: "
                << source_url.filesystem_id() << ")";
-    return SourceType::LOCAL;
+    return std::nullopt;
   }
   // First, look at whether the filesystem is read-only.
   if (source_volume->is_read_only()) {
@@ -163,16 +161,14 @@
                    : SourceType::LOCAL;
       }
     }
-    // Local if unable to find the provided file system.
-    return SourceType::LOCAL;
+    LOG(ERROR) << "Unable to find the provided file system";
+    return std::nullopt;
   }
   // Local by default.
   return SourceType::LOCAL;
 }
 
-UploadType GetUploadType(Profile* profile,
-                         const storage::FileSystemURL& source_url) {
-  SourceType source_type = GetSourceType(profile, source_url);
+UploadType SourceTypeToUploadType(SourceType source_type) {
   return source_type == SourceType::LOCAL ? UploadType::kMove
                                           : UploadType::kCopy;
 }
diff --git a/chrome/browser/ui/webui/ash/cloud_upload/cloud_upload_util.h b/chrome/browser/ui/webui/ash/cloud_upload/cloud_upload_util.h
index 202e1ea..4798de9 100644
--- a/chrome/browser/ui/webui/ash/cloud_upload/cloud_upload_util.h
+++ b/chrome/browser/ui/webui/ash/cloud_upload/cloud_upload_util.h
@@ -189,7 +189,8 @@
   kCancelledAtFallbackAfterOpen = 19,
   kCannotGetFallbackChoiceAfterOpen = 20,
   kFileAlreadyBeingOpened = 21,
-  kMaxValue = kFileAlreadyBeingOpened,
+  kCannotGetSourceType = 22,
+  kMaxValue = kCannotGetSourceType,
 };
 
 // The result of the "Upload to cloud" workflow for Office files.
@@ -350,13 +351,13 @@
 
 // Returns the type of the source location from which the file is getting
 // uploaded (see SourceType values).
-SourceType GetSourceType(Profile* profile,
-                         const storage::FileSystemURL& source_path);
+std::optional<SourceType> GetSourceType(
+    Profile* profile,
+    const storage::FileSystemURL& source_path);
 
 // Returns the upload type (move or copy) for the upload flow based on the
-// source path of the file to upload.
-UploadType GetUploadType(Profile* profile,
-                         const storage::FileSystemURL& source_path);
+// source type.
+UploadType SourceTypeToUploadType(SourceType source_type);
 
 // Request ODFS be mounted. If there is an existing mount, ODFS will unmount
 // that one after authentication of the new mount.
diff --git a/chrome/browser/ui/webui/ash/cloud_upload/drive_upload_handler.cc b/chrome/browser/ui/webui/ash/cloud_upload/drive_upload_handler.cc
index 0800ca2..80d34c1 100644
--- a/chrome/browser/ui/webui/ash/cloud_upload/drive_upload_handler.cc
+++ b/chrome/browser/ui/webui/ash/cloud_upload/drive_upload_handler.cc
@@ -63,6 +63,7 @@
 DriveUploadHandler::DriveUploadHandler(
     Profile* profile,
     const FileSystemURL& source_url,
+    UploadType upload_type,
     UploadCallback callback,
     base::SafeRef<CloudOpenMetrics> cloud_open_metrics)
     : profile_(profile),
@@ -70,7 +71,7 @@
           file_manager::util::GetFileManagerFileSystemContext(profile)),
       drive_integration_service_(
           drive::DriveIntegrationServiceFactory::FindForProfile(profile)),
-      upload_type_(GetUploadType(profile, source_url)),
+      upload_type_(upload_type),
       notification_manager_(
           base::MakeRefCounted<CloudUploadNotificationManager>(
               profile,
diff --git a/chrome/browser/ui/webui/ash/cloud_upload/drive_upload_handler.h b/chrome/browser/ui/webui/ash/cloud_upload/drive_upload_handler.h
index fbf55a1..ff6e778 100644
--- a/chrome/browser/ui/webui/ash/cloud_upload/drive_upload_handler.h
+++ b/chrome/browser/ui/webui/ash/cloud_upload/drive_upload_handler.h
@@ -54,6 +54,7 @@
 
   DriveUploadHandler(Profile* profile,
                      const storage::FileSystemURL& source_url,
+                     UploadType upload_type,
                      UploadCallback callback,
                      base::SafeRef<CloudOpenMetrics> cloud_open_metrics);
   ~DriveUploadHandler() override;
diff --git a/chrome/browser/ui/webui/ash/cloud_upload/drive_upload_handler_browsertest.cc b/chrome/browser/ui/webui/ash/cloud_upload/drive_upload_handler_browsertest.cc
index 98d34701..f4b67a1 100644
--- a/chrome/browser/ui/webui/ash/cloud_upload/drive_upload_handler_browsertest.cc
+++ b/chrome/browser/ui/webui/ash/cloud_upload/drive_upload_handler_browsertest.cc
@@ -410,7 +410,7 @@
       .WillOnce(RunOnceCallback<1>(drive::FileError::FILE_ERROR_OK));
 
   auto drive_upload_handler = std::make_unique<DriveUploadHandler>(
-      profile(), source_file_url,
+      profile(), source_file_url, UploadType::kMove,
       base::BindOnce(&DriveUploadHandlerTest::OnUploadDone,
                      base::Unretained(this)),
       cloud_open_metrics_ref_);
@@ -442,7 +442,7 @@
       .WillOnce(RunOnceCallback<1>(drive::FileError::FILE_ERROR_OK));
 
   auto drive_upload_handler = std::make_unique<DriveUploadHandler>(
-      profile(), source_file_url,
+      profile(), source_file_url, UploadType::kCopy,
       base::BindOnce(&DriveUploadHandlerTest::OnUploadDone,
                      base::Unretained(this)),
       cloud_open_metrics_ref_);
@@ -476,7 +476,7 @@
       .WillOnce(RunOnceCallback<1>(drive::FileError::FILE_ERROR_FAILED));
 
   auto drive_upload_handler = std::make_unique<DriveUploadHandler>(
-      profile(), source_file_url,
+      profile(), source_file_url, UploadType::kMove,
       base::BindOnce(&DriveUploadHandlerTest::OnUploadDone,
                      base::Unretained(this)),
       cloud_open_metrics_ref_);
@@ -514,7 +514,7 @@
                                    std::optional<GURL>(std::nullopt), _))
       .WillOnce(RunClosure(run_loop.QuitClosure()));
   auto drive_upload_handler = std::make_unique<DriveUploadHandler>(
-      profile(), source_file_url, upload_callback.Get(),
+      profile(), source_file_url, UploadType::kMove, upload_callback.Get(),
       cloud_open_metrics_ref_);
   drive_upload_handler->Run();
   run_loop.Run();
@@ -552,7 +552,7 @@
                                    std::optional<GURL>(std::nullopt), _))
       .WillOnce(RunClosure(run_loop.QuitClosure()));
   auto drive_upload_handler = std::make_unique<DriveUploadHandler>(
-      profile(), source_file_url, upload_callback.Get(),
+      profile(), source_file_url, UploadType::kMove, upload_callback.Get(),
       cloud_open_metrics_ref_);
   drive_upload_handler->Run();
   run_loop.Run();
@@ -573,7 +573,8 @@
 
   // Provide a FILE_ERROR_FAILED response.
   auto drive_upload_handler = std::make_unique<DriveUploadHandler>(
-      profile(), source_file_url, base::DoNothing(), cloud_open_metrics_ref_);
+      profile(), source_file_url, UploadType::kMove, base::DoNothing(),
+      cloud_open_metrics_ref_);
   // This should call the OnFailedUpload() immediately since no "upload"
   // actually occurred so there is no need to do any clean up.
   drive_upload_handler->OnGetDriveMetadata(
@@ -602,7 +603,8 @@
   metadata->alternate_url = "invalid";
 
   auto drive_upload_handler = std::make_unique<DriveUploadHandler>(
-      profile(), source_file_url, base::DoNothing(), cloud_open_metrics_ref_);
+      profile(), source_file_url, UploadType::kMove, base::DoNothing(),
+      cloud_open_metrics_ref_);
   // This should call the OnFailedUpload() immediately since no "upload"
   // actually occurred so there is no need to do any clean up.
   drive_upload_handler->OnGetDriveMetadata(
@@ -631,7 +633,8 @@
       "https://unexpected.com/document/d/smalldocxid?rtpof=true&usp=drive_fs";
 
   auto drive_upload_handler = std::make_unique<DriveUploadHandler>(
-      profile(), source_file_url, base::DoNothing(), cloud_open_metrics_ref_);
+      profile(), source_file_url, UploadType::kMove, base::DoNothing(),
+      cloud_open_metrics_ref_);
   // This should call the OnFailedUpload() immediately since no "upload"
   // actually occurred so there is no need to do any clean up.
   drive_upload_handler->OnGetDriveMetadata(
@@ -661,7 +664,8 @@
       "https://drive.google.com/document/d/smalldocxid?rtpof=true&usp=drive_fs";
 
   auto drive_upload_handler = std::make_unique<DriveUploadHandler>(
-      profile(), source_file_url, base::DoNothing(), cloud_open_metrics_ref_);
+      profile(), source_file_url, UploadType::kMove, base::DoNothing(),
+      cloud_open_metrics_ref_);
   // This should call the OnFailedUpload() immediately since no "upload"
   // actually occurred so there is no need to do any clean up.
   drive_upload_handler->OnGetDriveMetadata(
diff --git a/chrome/browser/ui/webui/ash/cloud_upload/one_drive_upload_handler.cc b/chrome/browser/ui/webui/ash/cloud_upload/one_drive_upload_handler.cc
index c299c569..e213d61 100644
--- a/chrome/browser/ui/webui/ash/cloud_upload/one_drive_upload_handler.cc
+++ b/chrome/browser/ui/webui/ash/cloud_upload/one_drive_upload_handler.cc
@@ -38,6 +38,7 @@
 OneDriveUploadHandler::OneDriveUploadHandler(
     Profile* profile,
     const storage::FileSystemURL& source_url,
+    UploadType upload_type,
     UploadCallback callback,
     base::SafeRef<CloudOpenMetrics> cloud_open_metrics)
     : profile_(profile),
@@ -50,8 +51,11 @@
               l10n_util::GetStringUTF8(IDS_OFFICE_FILE_HANDLER_APP_MICROSOFT),
               // TODO(b/242685536) Update when support for multi-files is added.
               /*num_files=*/1,
-              GetUploadType(profile, source_url))),
+              upload_type)),
       source_url_(source_url),
+      operation_type_(upload_type == UploadType::kCopy
+                          ? file_manager::io_task::OperationType::kCopy
+                          : file_manager::io_task::OperationType::kMove),
       callback_(std::move(callback)),
       cloud_open_metrics_(cloud_open_metrics) {
   observed_task_id_ = -1;
@@ -153,9 +157,6 @@
     return;
   }
 
-  operation_type_ = GetUploadType(profile_, source_url_) == UploadType::kCopy
-                        ? file_manager::io_task::OperationType::kCopy
-                        : file_manager::io_task::OperationType::kMove;
   std::vector<FileSystemURL> source_urls{source_url_};
   std::unique_ptr<file_manager::io_task::IOTask> task =
       std::make_unique<file_manager::io_task::CopyOrMoveIOTask>(
diff --git a/chrome/browser/ui/webui/ash/cloud_upload/one_drive_upload_handler.h b/chrome/browser/ui/webui/ash/cloud_upload/one_drive_upload_handler.h
index e739233..fcb21ac 100644
--- a/chrome/browser/ui/webui/ash/cloud_upload/one_drive_upload_handler.h
+++ b/chrome/browser/ui/webui/ash/cloud_upload/one_drive_upload_handler.h
@@ -40,6 +40,7 @@
 
   OneDriveUploadHandler(Profile* profile,
                         const storage::FileSystemURL& source_url,
+                        UploadType upload_type,
                         UploadCallback callback,
                         base::SafeRef<CloudOpenMetrics> cloud_open_metrics);
   ~OneDriveUploadHandler() override;
diff --git a/chrome/browser/ui/webui/ash/cloud_upload/one_drive_upload_handler_browsertest.cc b/chrome/browser/ui/webui/ash/cloud_upload/one_drive_upload_handler_browsertest.cc
index 8f44790f..8162a68 100644
--- a/chrome/browser/ui/webui/ash/cloud_upload/one_drive_upload_handler_browsertest.cc
+++ b/chrome/browser/ui/webui/ash/cloud_upload/one_drive_upload_handler_browsertest.cc
@@ -305,7 +305,7 @@
   // Start the upload workflow and end the test once the upload has completed
   // successfully.
   auto one_drive_upload_handler = std::make_unique<OneDriveUploadHandler>(
-      profile(), source_file_url,
+      profile(), source_file_url, UploadType::kMove,
       base::BindOnce(&OneDriveUploadHandlerTest::OnUploadSuccessful,
                      base::Unretained(this),
                      /*expected_task_result=*/OfficeTaskResult::kMoved),
@@ -334,7 +334,7 @@
   // Start the upload workflow and end the test once the upload has completed
   // successfully.
   auto one_drive_upload_handler = std::make_unique<OneDriveUploadHandler>(
-      profile(), source_file_url,
+      profile(), source_file_url, UploadType::kMove,
       base::BindOnce(&OneDriveUploadHandlerTest::OnUploadSuccessful,
                      base::Unretained(this),
                      /*expected_task_result=*/OfficeTaskResult::kMoved),
@@ -364,7 +364,7 @@
   // Start the upload workflow and end the test once the upload has completed
   // successfully.
   auto one_drive_upload_handler = std::make_unique<OneDriveUploadHandler>(
-      profile(), source_file_url,
+      profile(), source_file_url, UploadType::kCopy,
       base::BindOnce(&OneDriveUploadHandlerTest::OnUploadSuccessful,
                      base::Unretained(this),
                      /*expected_task_result=*/OfficeTaskResult::kCopied),
@@ -417,7 +417,7 @@
       });
   SetOnNotificationDisplayedCallback(std::move(on_notification));
   auto one_drive_upload_handler = std::make_unique<OneDriveUploadHandler>(
-      profile(), source_file_url,
+      profile(), source_file_url, UploadType::kMove,
       base::BindOnce(
           &OneDriveUploadHandlerTest::OnUploadFailedOrAbandoned,
           base::Unretained(this),
@@ -467,7 +467,7 @@
   SetOnNotificationDisplayedCallback(std::move(on_notification));
   SetUpRunLoop(/*conditions_to_end_wait=*/2);
   auto one_drive_upload_handler = std::make_unique<OneDriveUploadHandler>(
-      profile(), source_file_url,
+      profile(), source_file_url, UploadType::kMove,
       base::BindOnce(
           &OneDriveUploadHandlerTest::OnUploadFailedOrAbandoned,
           base::Unretained(this),
@@ -512,7 +512,7 @@
       }));
   SetUpRunLoop(/*conditions_to_end_wait=*/2);
   auto one_drive_upload_handler = std::make_unique<OneDriveUploadHandler>(
-      profile(), source_file_url,
+      profile(), source_file_url, UploadType::kMove,
       base::BindOnce(
           &OneDriveUploadHandlerTest::OnUploadFailedOrAbandoned,
           base::Unretained(this),
@@ -574,7 +574,7 @@
           }));
 
   auto one_drive_upload_handler = std::make_unique<OneDriveUploadHandler>(
-      profile(), source_file_url,
+      profile(), source_file_url, UploadType::kMove,
       base::BindOnce(&OneDriveUploadHandlerTest::OnUploadSuccessful,
                      base::Unretained(this),
                      /*expected_task_result=*/OfficeTaskResult::kMoved),
@@ -627,7 +627,7 @@
   SetOnNotificationDisplayedCallback(std::move(on_notification));
   SetUpRunLoop(/*conditions_to_end_wait=*/2);
   auto one_drive_upload_handler = std::make_unique<OneDriveUploadHandler>(
-      profile(), source_file_url,
+      profile(), source_file_url, UploadType::kMove,
       base::BindOnce(
           &OneDriveUploadHandlerTest::OnUploadFailedOrAbandoned,
           base::Unretained(this),
@@ -676,7 +676,7 @@
       });
   SetOnNotificationDisplayedCallback(std::move(on_notification));
   auto one_drive_upload_handler = std::make_unique<OneDriveUploadHandler>(
-      profile(), source_file_url,
+      profile(), source_file_url, UploadType::kMove,
       base::BindOnce(
           &OneDriveUploadHandlerTest::OnUploadFailedOrAbandoned,
           base::Unretained(this),
@@ -715,7 +715,7 @@
   provided_file_system_->SetCreateFileCallback(
       base::BindLambdaForTesting([&]() {
         one_drive_upload_handler2 = std::make_unique<OneDriveUploadHandler>(
-            profile(), source_file_url2,
+            profile(), source_file_url2, UploadType::kMove,
             base::BindOnce(&OneDriveUploadHandlerTest::OnUploadSuccessful,
                            base::Unretained(this),
                            /*expected_task_result=*/OfficeTaskResult::kMoved),
@@ -725,7 +725,7 @@
 
   // Start the first upload.
   auto one_drive_upload_handler1 = std::make_unique<OneDriveUploadHandler>(
-      profile(), source_file_url1,
+      profile(), source_file_url1, UploadType::kMove,
       base::BindOnce(&OneDriveUploadHandlerTest::OnUploadSuccessful,
                      base::Unretained(this),
                      /*expected_task_result=*/OfficeTaskResult::kMoved),
diff --git a/chrome/browser/ui/webui/ash/settings/pages/device/inputs_section_unittest.cc b/chrome/browser/ui/webui/ash/settings/pages/device/inputs_section_unittest.cc
index 32502f7..e117da0 100644
--- a/chrome/browser/ui/webui/ash/settings/pages/device/inputs_section_unittest.cc
+++ b/chrome/browser/ui/webui/ash/settings/pages/device/inputs_section_unittest.cc
@@ -127,6 +127,7 @@
       });
 
   chromeos::test::FakeMagicBoostState magic_boost_state;
+  magic_boost_state.SetAvailability(true);
   base::CommandLine::ForCurrentProcess()->AppendSwitch(
       chromeos::switches::kMahiRestrictionsOverride);
 
diff --git a/chrome/browser/ui/webui/ash/settings/pages/search/search_section_unittest.cc b/chrome/browser/ui/webui/ash/settings/pages/search/search_section_unittest.cc
index 1633b120..1ec49f4 100644
--- a/chrome/browser/ui/webui/ash/settings/pages/search/search_section_unittest.cc
+++ b/chrome/browser/ui/webui/ash/settings/pages/search/search_section_unittest.cc
@@ -91,6 +91,7 @@
       content::TestWebUIDataSource::Create("test-search-section");
 
   chromeos::test::FakeMagicBoostState magic_boost_state;
+  magic_boost_state.SetAvailability(true);
 
   search_section_->AddLoadTimeData(html_source->GetWebUIDataSource());
 
@@ -101,7 +102,7 @@
 
 // MagicBoost availability check requires an async operation. There is a short
 // period where `MagicBoostState` returns false for its availability even if a
-// user/device is eligible.
+// user/device is eligible, and magic boost is enabled.
 TEST_F(SearchSectionTest,
        QuickAnswersSearchConceptsRemovedIfItBecomesUnavailable) {
   const std::string quick_answers_result_id = base::StrCat(
@@ -109,7 +110,8 @@
        ",", base::ToString(IDS_OS_SETTINGS_TAG_QUICK_ANSWERS)});
 
   chromeos::test::FakeMagicBoostState magic_boost_state;
-  magic_boost_state.SetMagicBoostAvailability(false);
+  magic_boost_state.SetAvailability(false);
+  magic_boost_state.SetMagicBoostEnabled(true);
   FakeQuickAnswersState quick_answers_state;
   quick_answers_state.SetApplicationLocale("en");
   ASSERT_EQ(QuickAnswersState::FeatureType::kQuickAnswers,
@@ -127,9 +129,8 @@
          "to kQuickAnswers";
 
   // Simulate that MagicBoost availability check async operation has been
-  // completed and a user has went through MagicBoost consent flow.
-  magic_boost_state.SetMagicBoostAvailability(true);
-  magic_boost_state.SetMagicBoostEnabled(true);
+  // completed.
+  magic_boost_state.SetAvailability(true);
   ASSERT_EQ(QuickAnswersState::FeatureType::kHmr,
             QuickAnswersState::GetFeatureType());
 
@@ -143,6 +144,7 @@
  public:
   void SetUp() override {
     SearchSectionTest::SetUp();
+    magic_boost_state_.SetAvailability(true);
     feature_list_.InitWithFeatures(
         /*enable_features=*/{ash::features::kLobster,
                              ash::features::kFeatureManagementLobster},
@@ -257,6 +259,7 @@
   std::unique_ptr<content::TestWebUIDataSource> html_source =
       content::TestWebUIDataSource::Create("test-search-section");
   chromeos::test::FakeMagicBoostState magic_boost_state;
+  magic_boost_state.SetAvailability(true);
 
   search_section->AddLoadTimeData(html_source->GetWebUIDataSource());
 
@@ -281,6 +284,7 @@
   std::unique_ptr<content::TestWebUIDataSource> html_source =
       content::TestWebUIDataSource::Create("test-search-section");
   chromeos::test::FakeMagicBoostState magic_boost_state;
+  magic_boost_state.SetAvailability(true);
 
   search_section->AddLoadTimeData(html_source->GetWebUIDataSource());
 
@@ -301,6 +305,7 @@
   std::unique_ptr<content::TestWebUIDataSource> html_source =
       content::TestWebUIDataSource::Create("test-search-section");
   chromeos::test::FakeMagicBoostState magic_boost_state;
+  magic_boost_state.SetAvailability(true);
 
   search_section->AddLoadTimeData(html_source->GetWebUIDataSource());
 
diff --git a/chrome/browser/ui/webui/on_device_internals/on_device_internals_page_handler.cc b/chrome/browser/ui/webui/on_device_internals/on_device_internals_page_handler.cc
index 2dc647f..16bcf4b 100644
--- a/chrome/browser/ui/webui/on_device_internals/on_device_internals_page_handler.cc
+++ b/chrome/browser/ui/webui/on_device_internals/on_device_internals_page_handler.cc
@@ -49,6 +49,11 @@
     model_paths.weights = model_path;
   }
 
+  if (optimization_guide::features::ForceCpuBackendForOnDeviceModel()) {
+    model_paths.cache =
+        model_paths.weights.AddExtension(FILE_PATH_LITERAL("cache"));
+  }
+
   return on_device_model::LoadModelAssets(model_paths);
 }
 #endif
diff --git a/chrome/browser/ui/webui/searchbox/searchbox_handler.cc b/chrome/browser/ui/webui/searchbox/searchbox_handler.cc
index 82d0650b..4c67f7a 100644
--- a/chrome/browser/ui/webui/searchbox/searchbox_handler.cc
+++ b/chrome/browser/ui/webui/searchbox/searchbox_handler.cc
@@ -605,11 +605,7 @@
       icon.name == omnibox::kJourneysChromeRefreshIcon.name) {
     return kJourneysIconResourceName;
   }
-  if (icon.name == omnibox::kPageSparkIcon.name ||
-      icon.name == omnibox::kArrowUpChromeRefreshIcon.name ||
-      icon.name == omnibox::kVerticalBarIcon.name) {
-    // TODO(crbug.com/415327679): Either select an SVG or update webui for
-    //  vertical bars on contextual search matches once the omnibox UI settles.
+  if (icon.name == omnibox::kPageSparkIcon.name) {
     return kPageSparkIconResourceName;
   }
   if (icon.name == omnibox::kPedalIcon.name ||
diff --git a/chrome/browser/ui/webui/signin/history_sync_optin/history_sync_optin_ui.cc b/chrome/browser/ui/webui/signin/history_sync_optin/history_sync_optin_ui.cc
index 7c31cb7..7c1117a 100644
--- a/chrome/browser/ui/webui/signin/history_sync_optin/history_sync_optin_ui.cc
+++ b/chrome/browser/ui/webui/signin/history_sync_optin/history_sync_optin_ui.cc
@@ -5,8 +5,10 @@
 #include "chrome/browser/ui/webui/signin/history_sync_optin/history_sync_optin_ui.h"
 
 #include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/profiles/profile_avatar_icon_util.h"
 #include "chrome/browser/ui/webui/signin/history_sync_optin/history_sync_optin_handler.h"
 #include "chrome/common/webui_url_constants.h"
+#include "chrome/grit/generated_resources.h"
 #include "chrome/grit/signin_history_sync_optin_resources.h"
 #include "chrome/grit/signin_history_sync_optin_resources_map.h"
 #include "components/signin/public/base/signin_switches.h"
@@ -31,6 +33,19 @@
   webui::SetupWebUIDataSource(
       source, base::span(kSigninHistorySyncOptinResources),
       IDR_SIGNIN_HISTORY_SYNC_OPTIN_HISTORY_SYNC_OPTIN_HTML);
+
+  static constexpr webui::LocalizedString kLocalizedStrings[] = {
+      {"historySyncOptInTitle", IDS_HISTORY_SYNC_OPT_IN_TITLE},
+      {"historySyncOptInSubtitle", IDS_HISTORY_SYNC_OPT_IN_SUBTITLE},
+      {"historySyncOptInAcceptButtonLabel",
+       IDS_HISTORY_SYNC_OPT_IN_ACCEPT_BUTTON},
+      {"historySyncOptInCancelButtonLabel",
+       IDS_HISTORY_SYNC_OPT_IN_CANCEL_BUTTON},
+      {"historySyncOptInDescription", IDS_HISTORY_SYNC_OPT_IN_DESCRIPTION}};
+  source->AddLocalizedStrings(kLocalizedStrings);
+  // Add avatar fallback value.
+  source->AddString("accountPictureUrl",
+                    profiles::GetPlaceholderAvatarIconUrl());
 }
 
 HistorySyncOptinUI::~HistorySyncOptinUI() = default;
diff --git a/chrome/browser/ui/webui/tab_search/tab_search_page_handler.cc b/chrome/browser/ui/webui/tab_search/tab_search_page_handler.cc
index 459db732c..bbf9fc4 100644
--- a/chrome/browser/ui/webui/tab_search/tab_search_page_handler.cc
+++ b/chrome/browser/ui/webui/tab_search/tab_search_page_handler.cc
@@ -1567,13 +1567,13 @@
   page_->TabUpdated(std::move(tab_update_info));
 }
 
-void TabSearchPageHandler::OnSplitTabRemoved(
-    std::vector<std::pair<tabs::TabInterface*, int>> tabs,
-    split_tabs::SplitTabId split_id,
-    TabStripModelObserver::SplitTabRemoveReason reason) {
+void TabSearchPageHandler::OnSplitTabChanged(const SplitTabChange& change) {
   if (!base::FeatureList::IsEnabled(features::kSideBySide)) {
     return;
   }
+  if (change.type != SplitTabChange::Type::kRemoved) {
+    return;
+  }
   GURL url = web_ui_->GetWebContents()->GetURL();
   if (url.spec() != chrome::kChromeUISplitViewNewTabPageURL) {
     return;
diff --git a/chrome/browser/ui/webui/tab_search/tab_search_page_handler.h b/chrome/browser/ui/webui/tab_search/tab_search_page_handler.h
index dfab3b1..4c2e03b4f 100644
--- a/chrome/browser/ui/webui/tab_search/tab_search_page_handler.h
+++ b/chrome/browser/ui/webui/tab_search/tab_search_page_handler.h
@@ -150,10 +150,7 @@
   void TabChangedAt(content::WebContents* contents,
                     int index,
                     TabChangeType change_type) override;
-  void OnSplitTabRemoved(
-      std::vector<std::pair<tabs::TabInterface*, int>> tabs,
-      split_tabs::SplitTabId split_id,
-      TabStripModelObserver::SplitTabRemoveReason reason) override;
+  void OnSplitTabChanged(const SplitTabChange& change) override;
 
   // TabDeclutterObserver:
   void OnUnusedTabsProcessed(
diff --git a/chrome/browser/ui/webui/tab_strip/tab_strip_ui.cc b/chrome/browser/ui/webui/tab_strip/tab_strip_ui.cc
index 55163459..8a09f0f 100644
--- a/chrome/browser/ui/webui/tab_strip/tab_strip_ui.cc
+++ b/chrome/browser/ui/webui/tab_strip/tab_strip_ui.cc
@@ -9,7 +9,9 @@
 #include "chrome/browser/themes/theme_service.h"
 #include "chrome/browser/themes/theme_service_factory.h"
 #include "chrome/browser/ui/browser.h"
+#include "chrome/browser/ui/browser_window/public/browser_window_features.h"
 #include "chrome/browser/ui/color/chrome_color_id.h"
+#include "chrome/browser/ui/tabs/tab_strip_api/tab_strip_service_register.h"
 #include "chrome/browser/ui/ui_features.h"
 #include "chrome/browser/ui/views/frame/browser_view.h"
 #include "chrome/browser/ui/webui/favicon_source.h"
@@ -100,6 +102,14 @@
       web_ui()->GetWebContents(), std::move(receiver));
 }
 
+void TabStripUI::BindInterface(
+    mojo::PendingReceiver<tabs_api::mojom::TabStripService> receiver) {
+  if (auto* tab_strip_service =
+          browser_->browser_window_features()->tab_strip_service()) {
+    tab_strip_service->Accept(std::move(receiver));
+  }
+}
+
 void TabStripUI::CreatePageHandler(
     mojo::PendingRemote<tab_strip::mojom::Page> page,
     mojo::PendingReceiver<tab_strip::mojom::PageHandler> receiver) {
diff --git a/chrome/browser/ui/webui/tab_strip/tab_strip_ui.h b/chrome/browser/ui/webui/tab_strip/tab_strip_ui.h
index 69046ce..91964698 100644
--- a/chrome/browser/ui/webui/tab_strip/tab_strip_ui.h
+++ b/chrome/browser/ui/webui/tab_strip/tab_strip_ui.h
@@ -6,6 +6,7 @@
 #define CHROME_BROWSER_UI_WEBUI_TAB_STRIP_TAB_STRIP_UI_H_
 
 #include "base/memory/raw_ptr.h"
+#include "chrome/browser/ui/tabs/tab_strip_api/tab_strip_api.mojom.h"
 #include "chrome/browser/ui/webui/tab_strip/tab_strip.mojom.h"
 #include "chrome/browser/ui/webui/tab_strip/thumbnail_tracker.h"
 #include "chrome/browser/ui/webui/webui_load_timer.h"
@@ -65,6 +66,11 @@
       mojo::PendingReceiver<color_change_listener::mojom::PageHandler>
           receiver);
 
+  // Instantiates the implementor of the mojom::TabStripService mojo
+  // interface passing the pending receiver that will be internally bound.
+  void BindInterface(
+      mojo::PendingReceiver<tabs_api::mojom::TabStripService> receiver);
+
   // Initialize TabStripUI with its embedder and the Browser it's running in.
   // Must be called exactly once. The WebUI won't work until this is called.
   // |Deinitialize| is called during |embedder|'s destructor. It release the
diff --git a/chrome/browser_exposed_mojom_targets.gni b/chrome/browser_exposed_mojom_targets.gni
index 29039d22..2443127 100644
--- a/chrome/browser_exposed_mojom_targets.gni
+++ b/chrome/browser_exposed_mojom_targets.gni
@@ -22,6 +22,7 @@
   "//chrome/browser/new_tab_page/modules/v2/calendar:mojo_bindings",
   "//chrome/browser/new_tab_page/modules/v2/most_relevant_tab_resumption:mojo_bindings",
   "//chrome/browser/resource_coordinator:mojo_bindings",
+  "//chrome/browser/ui/tabs/tab_strip_api:mojom",
   "//chrome/browser/ui/webui/access_code_cast:mojo_bindings",
   "//chrome/browser/ui/webui/app_home:mojo_bindings",
   "//chrome/browser/ui/webui/app_service_internals:mojo_bindings",
diff --git a/chrome/build/android-arm32.pgo.txt b/chrome/build/android-arm32.pgo.txt
index 3b1a669..0b44c9f 100644
--- a/chrome/build/android-arm32.pgo.txt
+++ b/chrome/build/android-arm32.pgo.txt
@@ -1 +1 @@
-chrome-android32-main-1747072790-77523477ce79b5d0135799afaef83228736489c0-3e3a70932389e905ada855eaffe5580962c75b77.profdata
+chrome-android32-main-1747094145-8f652eb0e1e9b4791125a2d246b16f28edb9c197-209588d432611f8cc26421bf48c42a09ceab9742.profdata
diff --git a/chrome/build/android-arm64.pgo.txt b/chrome/build/android-arm64.pgo.txt
index 2f52023..24c98f3 100644
--- a/chrome/build/android-arm64.pgo.txt
+++ b/chrome/build/android-arm64.pgo.txt
@@ -1 +1 @@
-chrome-android64-main-1747068766-8256e4d82055a01e90f3a720d630b4b9c33ccdf2-8c7c1a2089552525ca4e9660f405e109f48ca754.profdata
+chrome-android64-main-1747109191-51d45c99a07cf92d2efc89b7e47754a39ec391dd-808e7d0e55827e67e1b8ac4fedf2421924286870.profdata
diff --git a/chrome/build/linux.pgo.txt b/chrome/build/linux.pgo.txt
index e2ffa91..388102f2 100644
--- a/chrome/build/linux.pgo.txt
+++ b/chrome/build/linux.pgo.txt
@@ -1 +1 @@
-chrome-linux-main-1747050864-5fc854516f698623d76b1ba1481b97d55768ba85-a4324bea8a2487ce1c7bf4d446062f09d67452ae.profdata
+chrome-linux-main-1747094145-fdce6231849408c62885c64314d37b1b3e4ad7be-209588d432611f8cc26421bf48c42a09ceab9742.profdata
diff --git a/chrome/build/mac-arm.pgo.txt b/chrome/build/mac-arm.pgo.txt
index 5599915..fb2fb11 100644
--- a/chrome/build/mac-arm.pgo.txt
+++ b/chrome/build/mac-arm.pgo.txt
@@ -1 +1 @@
-chrome-mac-arm-main-1747072790-05a6ec1f362dc2fff93ddf2f8c8367cb47b9e7a0-3e3a70932389e905ada855eaffe5580962c75b77.profdata
+chrome-mac-arm-main-1747108348-c8fc5cc54f66c2ff3e579d4e1460f808581ba0d2-170aba132ccbc47ed73b59df164cd5f8c98876dd.profdata
diff --git a/chrome/build/mac.pgo.txt b/chrome/build/mac.pgo.txt
index 1fc4ed0..66d8d1a 100644
--- a/chrome/build/mac.pgo.txt
+++ b/chrome/build/mac.pgo.txt
@@ -1 +1 @@
-chrome-mac-main-1747029142-e2474329ecfaff735404a5e7de6aa2c31ca2c90a-6e9453de1be37ab16eb07df5f5381a853f5d5390.profdata
+chrome-mac-main-1747094145-fd7a62aa350711bb2fb5316bf7c7673c8c0769fe-209588d432611f8cc26421bf48c42a09ceab9742.profdata
diff --git a/chrome/build/win-arm64.pgo.txt b/chrome/build/win-arm64.pgo.txt
index dd5c0d6..c794eef 100644
--- a/chrome/build/win-arm64.pgo.txt
+++ b/chrome/build/win-arm64.pgo.txt
@@ -1 +1 @@
-chrome-win-arm64-main-1747050864-d5ce350b7bc17fabb6c6ba6b936cb5452cb95777-a4324bea8a2487ce1c7bf4d446062f09d67452ae.profdata
+chrome-win-arm64-main-1747094145-e6da7e174a41773946cc319a8bd4c312e9c7b2ad-209588d432611f8cc26421bf48c42a09ceab9742.profdata
diff --git a/chrome/build/win32.pgo.txt b/chrome/build/win32.pgo.txt
index bada14e..52c3ef3 100644
--- a/chrome/build/win32.pgo.txt
+++ b/chrome/build/win32.pgo.txt
@@ -1 +1 @@
-chrome-win32-main-1747040350-8be01855e5d9f9285a7c17ed3030db292748512c-ef48e4d60cc0f3033c242531ed4d0cdec161e84c.profdata
+chrome-win32-main-1747094145-91fdc4a66a639fcf28cfdc8ade8437f45a1d4ac8-209588d432611f8cc26421bf48c42a09ceab9742.profdata
diff --git a/chrome/build/win64.pgo.txt b/chrome/build/win64.pgo.txt
index c77af76..7f8f121 100644
--- a/chrome/build/win64.pgo.txt
+++ b/chrome/build/win64.pgo.txt
@@ -1 +1 @@
-chrome-win64-main-1747029142-d9fe33f075cef2acd862287b657d0c42e47c0b18-6e9453de1be37ab16eb07df5f5381a853f5d5390.profdata
+chrome-win64-main-1747072790-18acbccf4e3e885a0f8078d6455f3a45dcd8ea9b-3e3a70932389e905ada855eaffe5580962c75b77.profdata
diff --git a/chrome/common/chrome_features.cc b/chrome/common/chrome_features.cc
index 6040589..6a440e1 100644
--- a/chrome/common/chrome_features.cc
+++ b/chrome/common/chrome_features.cc
@@ -1741,6 +1741,11 @@
 BASE_FEATURE(kK12AgeClassificationMetricsProvider,
              "K12AgeClassificationMetricsProvider",
              base::FEATURE_DISABLED_BY_DEFAULT);
+
+// A feature to enable periodic log class management enabled policy.
+BASE_FEATURE(kClassManagementEnabledMetricsProvider,
+             "ClassManagementEnabledMetricsProvider",
+             base::FEATURE_DISABLED_BY_DEFAULT);
 #endif  // BUILDFLAG(IS_CHROMEOS)
 
 // A feature to disable shortcut creation from the Chrome UI, and instead use
diff --git a/chrome/common/chrome_features.h b/chrome/common/chrome_features.h
index 9b66d42..ab5cf00 100644
--- a/chrome/common/chrome_features.h
+++ b/chrome/common/chrome_features.h
@@ -1077,6 +1077,8 @@
 bool IsK12AgeClassificationMetricsProviderEnabled();
 COMPONENT_EXPORT(CHROME_FEATURES)
 BASE_DECLARE_FEATURE(kK12AgeClassificationMetricsProvider);
+COMPONENT_EXPORT(CHROME_FEATURES)
+BASE_DECLARE_FEATURE(kClassManagementEnabledMetricsProvider);
 #endif  // BUILDFLAG(IS_CHROMEOS)
 
 bool PrefServiceEnabled();
diff --git a/chrome/common/chrome_result_codes.cc b/chrome/common/chrome_result_codes.cc
index 22145147..18d4131 100644
--- a/chrome/common/chrome_result_codes.cc
+++ b/chrome/common/chrome_result_codes.cc
@@ -11,7 +11,8 @@
   // here.
   if (code == CHROME_RESULT_CODE_NORMAL_EXIT_UPGRADE_RELAUNCHED ||
       code == CHROME_RESULT_CODE_NORMAL_EXIT_PACK_EXTENSION_SUCCESS ||
-      code == CHROME_RESULT_CODE_NORMAL_EXIT_PROCESS_NOTIFIED) {
+      code == CHROME_RESULT_CODE_NORMAL_EXIT_PROCESS_NOTIFIED ||
+      code == CHROME_RESULT_CODE_NORMAL_EXIT_AUTO_DE_ELEVATED) {
     return true;
   }
 
diff --git a/chrome/common/chrome_result_codes.h b/chrome/common/chrome_result_codes.h
index a0c60d46..40ecb55 100644
--- a/chrome/common/chrome_result_codes.h
+++ b/chrome/common/chrome_result_codes.h
@@ -124,11 +124,14 @@
   // system state can't be recovered and will be unstable.
   CHROME_RESULT_CODE_SYSTEM_RESOURCE_EXHAUSTED,
 
+  // The browser process exited because it was re-launched without elevation.
+  CHROME_RESULT_CODE_NORMAL_EXIT_AUTO_DE_ELEVATED,
+
   // Last return code (keep this last).
   CHROME_RESULT_CODE_CHROME_LAST_CODE
 };
 
-static_assert(CHROME_RESULT_CODE_CHROME_LAST_CODE == 38,
+static_assert(CHROME_RESULT_CODE_CHROME_LAST_CODE == 39,
               "Please make sure the enum values are in sync with enums.xml");
 
 // Returns true if the result code should be treated as a normal exit code i.e.
diff --git a/chrome/common/chrome_switches.cc b/chrome/common/chrome_switches.cc
index d2e2f5c3..e741783 100644
--- a/chrome/common/chrome_switches.cc
+++ b/chrome/common/chrome_switches.cc
@@ -232,6 +232,10 @@
 // Forces the maximum disk space to be used by the disk cache, in bytes.
 const char kDiskCacheSize[] = "disk-cache-size";
 
+// Do not de-elevate the browser on launch. Used after de-elevating to prevent
+// infinite loops.
+const char kDoNotDeElevateOnLaunch[] = "do-not-de-elevate";
+
 // Requests that a running browser process dump its collected histograms to a
 // given file. The file is overwritten if it exists.
 const char kDumpBrowserHistograms[] = "dump-browser-histograms";
diff --git a/chrome/common/chrome_switches.h b/chrome/common/chrome_switches.h
index d615165..86dc798f 100644
--- a/chrome/common/chrome_switches.h
+++ b/chrome/common/chrome_switches.h
@@ -85,6 +85,7 @@
 extern const char kDisableZeroBrowsersOpenForTests[];
 extern const char kDiskCacheDir[];
 extern const char kDiskCacheSize[];
+extern const char kDoNotDeElevateOnLaunch[];
 extern const char kDumpBrowserHistograms[];
 extern const char kEnableAudioDebugRecordingsFromExtension[];
 extern const char kEnableBookmarkUndo[];
diff --git a/chrome/common/pref_names.h b/chrome/common/pref_names.h
index 959e78c6..3bcca667 100644
--- a/chrome/common/pref_names.h
+++ b/chrome/common/pref_names.h
@@ -1370,11 +1370,10 @@
 
 // SkColor used to theme the browser for Chrome Refresh. The value
 // SK_ColorTRANSPARENT means the user color has not been set.
-// Note: In the process of migration. Please use `GetThemePrefNameInMigration()`
-// instead. See crbug.com/356148174.
-inline constexpr char kUserColorDoNotUse[] = "browser.theme.user_color";
-inline constexpr char kNonSyncingUserColorDoNotUse[] =
-    "browser.theme.user_color2";
+// Use `kUserColor` only.
+inline constexpr char kDeprecatedUserColorDoNotUse[] =
+    "browser.theme.user_color";
+inline constexpr char kUserColor[] = "browser.theme.user_color2";
 
 // Enum tracking the color variant preference for the browser.
 // Note: In the process of migration. Please use `GetThemePrefNameInMigration()`
@@ -4225,11 +4224,11 @@
 inline constexpr char kNTPFooterManagementNoticeEnabled[] =
     "ntp_footer.settings.management_notice";
 
-// Boolean value that determine whether the NTP theme attribution on the NTP
-// footer is enabled. This is false when disabled by the
-// `NTPFooterThemeAttributionEnabled` policy.
-inline constexpr char kNTPFooterThemeAttributionEnabled[] =
-    "ntp_footer.settings.theme_attribution";
+// Boolean value that determines whether the NTP extension attribution on the
+// NTP footer is enabled. This is false when disabled by the
+// `NTPFooterExtensionAttributionEnabled` policy.
+inline constexpr char kNTPFooterExtensionAttributionEnabled[] =
+    "ntp_footer.settings.extension_attribution";
 
 #if BUILDFLAG(IS_ANDROID)
 // An integer count of how many account-level breached credentials were
diff --git a/chrome/test/BUILD.gn b/chrome/test/BUILD.gn
index a4c23b7..90d540e5 100644
--- a/chrome/test/BUILD.gn
+++ b/chrome/test/BUILD.gn
@@ -4638,6 +4638,12 @@
         "//chrome/browser/extensions/api/braille_display_private:controller",
         "//chrome/browser/hid",
         "//chrome/browser/prefetch",
+
+        # TODO(417228688): Remove when the following tests get modularized:
+        # - chrome_serial_browsertest.cc
+        # - controlled_frame_disabled_permission_browsertest.cc
+        # - web_view_browsertest.cc
+        "//chrome/browser/serial",
         "//chrome/browser/ui/global_error:test_support",
         "//chrome/browser/ui/hid",
         "//chrome/browser/ui/javascript_dialogs",
@@ -4978,6 +4984,7 @@
         "../browser/file_system_access/cloud_identifier/cloud_identifier_util_ash_browsertest.cc",
         "../browser/metrics/chromeos_family_link_user_metrics_provider_browsertest.cc",
         "../browser/metrics/chromeos_metrics_provider_browsertest.cc",
+        "../browser/metrics/class_management_enabled_metrics_provider_browsertest.cc",
         "../browser/metrics/family_user_metrics_provider_browsertest.cc",
         "../browser/metrics/k12_age_classification_metrics_provider_browsertest.cc",
         "../browser/metrics/majority_age_user_metrics_provider_browsertest.cc",
@@ -6324,7 +6331,6 @@
     "../browser/ui/passwords/display_account_info_unittest.cc",
     "../browser/ui/passwords/manage_passwords_state_unittest.cc",
     "../browser/ui/passwords/settings/password_manager_porter_unittest.cc",
-    "../browser/ui/serial/serial_chooser_controller_unittest.cc",
     "../browser/ui/sync/tab_contents_synced_tab_delegate_unittest.cc",
     "../browser/ui/webui/chrome_urls/chrome_urls_handler_unittest.cc",
     "../browser/ui/webui/fileicon_source_unittest.cc",
@@ -6650,6 +6656,9 @@
     "//chrome/browser/search_engine_choice",
     "//chrome/browser/search_engines",
     "//chrome/browser/segmentation_platform:test_utils",
+
+    # TODO(417228688): Remove this dependency when serial_chooser_context_unittest.cc gets modularized.
+    "//chrome/browser/serial",
     "//chrome/browser/share",
     "//chrome/browser/status_icons:status_icons",
     "//chrome/browser/storage_access_api",
@@ -6676,7 +6685,7 @@
     "//chrome/browser/ui/plus_addresses:unit_tests",
     "//chrome/browser/ui/safety_hub:test_support",
     "//chrome/browser/ui/search_engines:unit_tests",
-    "//chrome/browser/ui/serial:test_support",
+    "//chrome/browser/ui/serial:unit_tests",
     "//chrome/browser/ui/webui",
     "//chrome/browser/ui/webui/internal_debug_pages_disabled",
     "//chrome/browser/updates/announcement_notification:unit_tests",
@@ -7575,6 +7584,8 @@
       "//chrome/browser/ui/autofill/payments:test_support",
       "//chrome/browser/ui/fast_checkout",
       "//chrome/browser/ui/fast_checkout:unit_tests",
+      "//chrome/browser/ui/serial",
+      "//chrome/browser/ui/serial:test_support",
       "//chrome/browser/ui/tabs:tab_enums",
       "//chrome/common/wallet:mojo_bindings",
       "//chrome/services/media_gallery_util:unit_tests",
diff --git a/chrome/test/android/BUILD.gn b/chrome/test/android/BUILD.gn
index c6f34ce..8a20024 100644
--- a/chrome/test/android/BUILD.gn
+++ b/chrome/test/android/BUILD.gn
@@ -219,6 +219,7 @@
     "javatests/src/org/chromium/chrome/test/transit/hub/ArchiveMessageCardFacility.java",
     "javatests/src/org/chromium/chrome/test/transit/hub/ArchivedTabsDialogStation.java",
     "javatests/src/org/chromium/chrome/test/transit/hub/CardAtPositionCondition.java",
+    "javatests/src/org/chromium/chrome/test/transit/hub/HistoryPaneStation.java",
     "javatests/src/org/chromium/chrome/test/transit/hub/HubBaseStation.java",
     "javatests/src/org/chromium/chrome/test/transit/hub/HubStationUtils.java",
     "javatests/src/org/chromium/chrome/test/transit/hub/IncognitoTabSwitcherStation.java",
diff --git a/chrome/test/android/javatests/src/org/chromium/chrome/test/transit/hub/HistoryPaneStation.java b/chrome/test/android/javatests/src/org/chromium/chrome/test/transit/hub/HistoryPaneStation.java
new file mode 100644
index 0000000..fe39aeb
--- /dev/null
+++ b/chrome/test/android/javatests/src/org/chromium/chrome/test/transit/hub/HistoryPaneStation.java
@@ -0,0 +1,132 @@
+// Copyright 2025 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.test.transit.hub;
+
+import static androidx.test.espresso.Espresso.onView;
+import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist;
+import static androidx.test.espresso.matcher.ViewMatchers.withId;
+import static androidx.test.espresso.matcher.ViewMatchers.withText;
+
+import static org.hamcrest.CoreMatchers.instanceOf;
+
+import static org.chromium.base.test.transit.ViewSpec.viewSpec;
+
+import android.view.View;
+import android.widget.EditText;
+
+import org.chromium.base.test.transit.Facility;
+import org.chromium.base.test.transit.ViewElement;
+import org.chromium.chrome.browser.history.HistoryItemView;
+import org.chromium.chrome.browser.hub.PaneId;
+import org.chromium.chrome.test.R;
+import org.chromium.chrome.test.transit.page.PageStation;
+import org.chromium.chrome.test.transit.page.WebPageStation;
+
+/** The History pane station. */
+public class HistoryPaneStation extends HubBaseStation {
+    public HistoryPaneStation(boolean regularTabsExist, boolean incognitoTabsExist) {
+        super(regularTabsExist, incognitoTabsExist, /* hasMenuButton= */ false);
+    }
+
+    @Override
+    public @PaneId int getPaneId() {
+        return PaneId.HISTORY;
+    }
+
+    /** Expect history entries to be displayed in the history pane. */
+    public HistoryWithEntriesFacility expectEntries() {
+        return enterFacilitySync(new HistoryWithEntriesFacility(), /* trigger= */ null);
+    }
+
+    /** Expect no history to be displayed in the history pane. */
+    public EmptyHistoryFacility expectEmptyState() {
+        return enterFacilitySync(new EmptyHistoryFacility(), /* trigger= */ null);
+    }
+
+    /** Empty state of the history pane. */
+    public static class EmptyHistoryFacility extends Facility<HistoryPaneStation> {
+        public EmptyHistoryFacility() {
+            declareView(viewSpec(withText("You’ll find your history here")));
+            declareView(
+                    viewSpec(
+                            withText(
+                                    "You can see the pages you’ve visited or delete them from your"
+                                            + " history")));
+            declareNoView(viewSpec(withId(R.id.history_page_recycler_view)));
+        }
+    }
+
+    /** Non-empty state of the history pane. */
+    public class HistoryWithEntriesFacility extends Facility<HistoryPaneStation> {
+        public final ViewElement<View> recyclerViewElement;
+        public final ViewElement<View> searchButtonElement;
+
+        public HistoryWithEntriesFacility() {
+            recyclerViewElement = declareView(viewSpec(withId(R.id.history_page_recycler_view)));
+            searchButtonElement = declareView(viewSpec(withId(R.id.search_menu_id)));
+        }
+
+        /** Expect an entry to be displayed in the history pane. */
+        public HistoryEntryFacility expectEntry(String text) {
+            return mHostStation.enterFacilitySync(
+                    new HistoryEntryFacility(this, text), /* trigger= */ null);
+        }
+
+        /** Expect an entry to be not displayed in the history pane. */
+        public void expectNoEntry(String text) {
+            onView(withText(text)).check(doesNotExist());
+        }
+
+        /** Open the history search. */
+        public HistorySearchFacility openSearch() {
+            return enterFacilitySync(
+                    new HistorySearchFacility(), searchButtonElement.getClickTrigger());
+        }
+    }
+
+    /** One history entry in the history pane. */
+    public class HistoryEntryFacility extends Facility<HistoryPaneStation> {
+        public final ViewElement<HistoryItemView> itemElement;
+        public final ViewElement<View> titleElement;
+        public final ViewElement<View> iconElement;
+        public final ViewElement<View> removeButtonElement;
+
+        public HistoryEntryFacility(HistoryWithEntriesFacility resultsFacility, String text) {
+            titleElement =
+                    declareView(
+                            resultsFacility.recyclerViewElement.descendant(
+                                    withText(text), withId(R.id.title)));
+            itemElement =
+                    declareView(
+                            titleElement.ancestor(
+                                    HistoryItemView.class, instanceOf(HistoryItemView.class)));
+            iconElement = declareView(itemElement.descendant(withId(R.id.start_icon)));
+            removeButtonElement = declareView(itemElement.descendant(withId(R.id.end_button)));
+        }
+
+        /** Select the entry to open. */
+        public WebPageStation selectToOpenWebPage(PageStation previousPage, String url) {
+            return travelToSync(
+                    WebPageStation.newBuilder()
+                            .initFrom(previousPage)
+                            .withExpectedUrlSubstring(url)
+                            .build(),
+                    itemElement.getClickTrigger());
+        }
+    }
+
+    /** Search state in the history pane. */
+    public static class HistorySearchFacility extends Facility<HistoryPaneStation> {
+        public final ViewElement<EditText> editTextElement;
+
+        public HistorySearchFacility() {
+            editTextElement = declareView(viewSpec(EditText.class, withId(R.id.search_text)));
+        }
+
+        public void typeSearchTerm(String text) {
+            editTextElement.getTypeTextTrigger(text).triggerTransition();
+        }
+    }
+}
diff --git a/chrome/test/android/javatests/src/org/chromium/chrome/test/transit/hub/HubBaseStation.java b/chrome/test/android/javatests/src/org/chromium/chrome/test/transit/hub/HubBaseStation.java
index 412e80ff..9a408438 100644
--- a/chrome/test/android/javatests/src/org/chromium/chrome/test/transit/hub/HubBaseStation.java
+++ b/chrome/test/android/javatests/src/org/chromium/chrome/test/transit/hub/HubBaseStation.java
@@ -123,15 +123,25 @@
     }
 
     /** Convenience method to select the Regular Tab Switcher pane. */
-    public RegularTabSwitcherStation selectRegularTabList() {
+    public RegularTabSwitcherStation selectRegularTabsPane() {
         return selectPane(PaneId.TAB_SWITCHER, RegularTabSwitcherStation.class);
     }
 
     /** Convenience method to select the Incognito Tab Switcher pane. */
-    public IncognitoTabSwitcherStation selectIncognitoTabList() {
+    public IncognitoTabSwitcherStation selectIncognitoTabsPane() {
         return selectPane(PaneId.INCOGNITO_TAB_SWITCHER, IncognitoTabSwitcherStation.class);
     }
 
+    /** Convenience method to select the Tab Groups pane. */
+    public TabGroupPaneStation selectTabGroupsPane() {
+        return selectPane(PaneId.TAB_GROUPS, TabGroupPaneStation.class);
+    }
+
+    /** Convenience method to select the History pane. */
+    public HistoryPaneStation selectHistoryPane() {
+        return selectPane(PaneId.HISTORY, HistoryPaneStation.class);
+    }
+
     public class SwitchPaneButtonFacility extends Facility<HubBaseStation> {
         public final ViewElement<View> buttonElement;
 
diff --git a/chrome/test/android/javatests/src/org/chromium/chrome/test/transit/hub/HubStationUtils.java b/chrome/test/android/javatests/src/org/chromium/chrome/test/transit/hub/HubStationUtils.java
index f56002775..190b1c44 100644
--- a/chrome/test/android/javatests/src/org/chromium/chrome/test/transit/hub/HubStationUtils.java
+++ b/chrome/test/android/javatests/src/org/chromium/chrome/test/transit/hub/HubStationUtils.java
@@ -18,7 +18,8 @@
             Map.ofEntries(
                     Map.entry(PaneId.TAB_SWITCHER, "standard tab"),
                     Map.entry(PaneId.INCOGNITO_TAB_SWITCHER, "Incognito tabs"),
-                    Map.entry(PaneId.TAB_GROUPS, "Tab groups"));
+                    Map.entry(PaneId.TAB_GROUPS, "Tab groups"),
+                    Map.entry(PaneId.HISTORY, "History"));
 
     /**
      * @param paneId The pane to get the content description of.
@@ -45,6 +46,8 @@
                 return new IncognitoTabSwitcherStation(regularTabsExist, incognitoTabsExist);
             case PaneId.TAB_GROUPS:
                 return new TabGroupPaneStation(regularTabsExist, incognitoTabsExist);
+            case PaneId.HISTORY:
+                return new HistoryPaneStation(regularTabsExist, incognitoTabsExist);
             default:
                 throw new IllegalArgumentException("No hub station is available for " + paneId);
         }
diff --git a/chrome/test/data/pdf/ink2_manager_test.ts b/chrome/test/data/pdf/ink2_manager_test.ts
index 8753340..9ccbfef 100644
--- a/chrome/test/data/pdf/ink2_manager_test.ts
+++ b/chrome/test/data/pdf/ink2_manager_test.ts
@@ -7,12 +7,12 @@
 import {assert} from 'chrome://resources/js/assert.js';
 import {eventToPromise} from 'chrome://webui-test/test_util.js';
 
-import {assertAnnotationBrush, assertDeepEquals, setGetAnnotationBrushReply, setupTestViewportAndMockPluginForInk} from './test_util.js';
+import {assertAnnotationBrush, assertDeepEquals, MockDocumentDimensions, setGetAnnotationBrushReply, setupTestViewportAndMockPluginForInk} from './test_util.js';
 
 const {viewport, mockPlugin} = setupTestViewportAndMockPluginForInk();
 const manager = Ink2Manager.getInstance();
 
-function getTestAnnotation(): TextAnnotation {
+function getTestAnnotation(id: number): TextAnnotation {
   return {
     textAttributes: {
       typeface: TextTypeface.SANS_SERIF,
@@ -25,17 +25,51 @@
       },
     },
     text: 'Hello World',
-    id: 0,
+    id: id,
     pageNumber: 0,
     textBoxRect: {
-      height: 50,
-      locationX: 15,
+      height: 35,
+      locationX: 20,
       locationY: 25,
       width: 50,
     },
+    textOrientation: 0,
   };
 }
 
+// Verifies that the plugin received a startTextAnnotation message for
+// annotation with id 0.
+function verifyStartTextAnnotationMessage(expected: boolean) {
+  const startTextAnnotationMessage =
+      mockPlugin.findMessage('startTextAnnotation');
+  chrome.test.assertEq(expected, startTextAnnotationMessage !== undefined);
+  if (expected) {
+    chrome.test.assertEq(
+        'startTextAnnotation', startTextAnnotationMessage.type);
+    chrome.test.assertEq(0, startTextAnnotationMessage.data);
+  }
+}
+
+// Simulates the way the viewport is rotated from the plugin by setting updated
+// DocumentDimensions. Assumes a non-rotated pageWidth of 80 and pageHeight of
+// 100.
+function rotateViewport(orientation: number) {
+  const rotatedDocumentDimensions = new MockDocumentDimensions(0, 0);
+  // When the plugin notifies the viewport of new dimensions for a rotation,
+  // it swaps the width and height if the page is oriented sideways.
+  if (orientation === 0 || orientation === 2) {
+    rotatedDocumentDimensions.addPage(80, 100);
+  } else {
+    rotatedDocumentDimensions.addPage(100, 80);
+  }
+  rotatedDocumentDimensions.layoutOptions = {
+    defaultPageOrientation: orientation,  // 90 degree CCW rotation
+    direction: 2,                         // LTR
+    twoUpViewEnabled: false,
+  };
+  viewport.setDocumentDimensions(rotatedDocumentDimensions);
+}
+
 chrome.test.runTests([
   async function testInitializeBrush() {
     chrome.test.assertFalse(manager.isInitializationStarted());
@@ -207,9 +241,15 @@
   },
 
   async function testInitializeTextBox() {
+    // Create a new mock document dimensions that has different width from
+    // height. This is relevant for testing different rotations.
+    const documentDimensions = new MockDocumentDimensions(0, 0);
+    documentDimensions.addPage(80, 100);
+    viewport.setDocumentDimensions(documentDimensions);
+
     // Add listeners for the expected events that fire in response to an
     // initializeTextAnnotation call.
-    const eventsDispatched: Array<{name: string, detail: any}> = [];
+    let eventsDispatched: Array<{name: string, detail: any}> = [];
     ['initialize-text-box', 'attributes-changed'].forEach(eventName => {
       manager.addEventListener(eventName, e => {
         eventsDispatched.push(
@@ -218,52 +258,65 @@
     });
 
     const attributes = manager.getCurrentTextAttributes();
-    const whenUpdateEvent = eventToPromise('initialize-text-box', manager);
-    Ink2Manager.getInstance().initializeTextAnnotation({x: 20, y: 23});
-    await whenUpdateEvent;
-    chrome.test.assertEq(2, eventsDispatched.length);
-    chrome.test.assertEq('initialize-text-box', eventsDispatched[0]!.name);
-    const initData = eventsDispatched[0]!.detail as TextBoxInit;
-    chrome.test.assertEq('', initData.annotation.text);
-    assertDeepEquals(attributes, initData.annotation.textAttributes);
-    chrome.test.assertEq(
-        DEFAULT_TEXTBOX_HEIGHT, initData.annotation.textBoxRect.height);
-    chrome.test.assertEq(20, initData.annotation.textBoxRect.locationX);
-    chrome.test.assertEq(23, initData.annotation.textBoxRect.locationY);
-    chrome.test.assertEq(
-        DEFAULT_TEXTBOX_WIDTH, initData.annotation.textBoxRect.width);
-    chrome.test.assertEq(0, initData.annotation.pageNumber);
-    chrome.test.assertEq(0, initData.annotation.id);
-    // Placeholder viewport has a 90x90 page and 100x100 window. This creates
-    // pageX and pageY offsets of 10px = (100 - 90)/2 + 5px and 3px
-    // respectively.
-    chrome.test.assertEq(10, initData.pageCoordinates.x);
-    chrome.test.assertEq(3, initData.pageCoordinates.y);
-    chrome.test.assertEq('attributes-changed', eventsDispatched[1]!.name);
-    assertDeepEquals(attributes, eventsDispatched[1]!.detail);
+    async function verifyTextboxInit(
+        x: number, y: number, rotation: number, id: number) {
+      const whenUpdateEvent = eventToPromise('initialize-text-box', manager);
+      Ink2Manager.getInstance().initializeTextAnnotation({x, y});
+      await whenUpdateEvent;
+      chrome.test.assertEq(2, eventsDispatched.length);
+      chrome.test.assertEq('initialize-text-box', eventsDispatched[0]!.name);
+      const initData = eventsDispatched[0]!.detail as TextBoxInit;
+      chrome.test.assertEq('', initData.annotation.text);
+      assertDeepEquals(attributes, initData.annotation.textAttributes);
+      chrome.test.assertEq(
+          DEFAULT_TEXTBOX_HEIGHT, initData.annotation.textBoxRect.height);
+      chrome.test.assertEq(x, initData.annotation.textBoxRect.locationX);
+      chrome.test.assertEq(y, initData.annotation.textBoxRect.locationY);
+      chrome.test.assertEq(
+          DEFAULT_TEXTBOX_WIDTH, initData.annotation.textBoxRect.width);
+      chrome.test.assertEq(0, initData.annotation.pageNumber);
+      chrome.test.assertEq(id, initData.annotation.id);
+      chrome.test.assertEq(rotation, initData.annotation.textOrientation);
+      // Placeholder viewport has a 80x100 page and 100x100 window.
+      // The y offset is always 3px, because the page is always positioned
+      // 3px from the top. When the page is oriented vertically, it is centered
+      // in the viewport with an additional 5px margin in x, creating pageX =
+      // (100 - 80)/2 + 5 = 15px offset. When the page is oriented horizontally,
+      // it is as wide as the viewport, so it uses the minimum 5px margin for
+      // pageX.
+      chrome.test.assertEq(
+          rotation % 2 === 0 ? 15 : 5, initData.pageCoordinates.x);
+      chrome.test.assertEq(3, initData.pageCoordinates.y);
+      chrome.test.assertEq('attributes-changed', eventsDispatched[1]!.name);
+      assertDeepEquals(attributes, eventsDispatched[1]!.detail);
+      eventsDispatched = [];
 
-    // Since this is a new annotation, it shouldn't have sent a message to the
-    // plugin.
-    const startTextAnnotationMessage =
-        mockPlugin.findMessage('startTextAnnotation');
-    chrome.test.assertEq(undefined, startTextAnnotationMessage);
+      // Since this is a new annotation, it shouldn't have sent a message to the
+      // plugin.
+      verifyStartTextAnnotationMessage(false);
+    }
+
+    // Test initialization in different positions and different viewport
+    // rotations. id should increment with each new textbox.
+    rotateViewport(/* clockwiseRotations= */ 3);
+    await verifyTextboxInit(/* x= */ 15, /* y= */ 10, /* rotations= */ 1,
+                            /* id= */ 0);
+    rotateViewport(/* clockwiseRotations= */ 2);
+    await verifyTextboxInit(/* x= */ 50, /* y= */ 60, /* rotations= */ 2,
+                            /* id= */ 1);
+    rotateViewport(/* clockwiseRotations= */ 1);
+    await verifyTextboxInit(/* x= */ 80, /* y = */ 20, /* rotations= */ 3,
+                            /* id= */ 2);
+    rotateViewport(/* clockwiseRotations= */ 0);
+    await verifyTextboxInit(/* x= */ 20, /* y= */ 23, /* rotations= */ 0,
+                            /* id= */ 3);
+
     chrome.test.succeed();
   },
 
   function testCommitTextAnnotation() {
-    // Listen for PluginControllerEventType.FINISH_INK_STROKE events. The
-    // manager dispatches these on PluginController's eventTarget.
-    let finishInkStrokeEvents = 0;
-    PluginController.getInstance().getEventTarget().addEventListener(
-        PluginControllerEventType.FINISH_INK_STROKE, () => {
-          finishInkStrokeEvents++;
-        });
-
-    const annotationPageCoords = getTestAnnotation();
-    // Adjust by the x and y offsets to get to page coordinates.
-    annotationPageCoords.textBoxRect.locationX = 5;
-    annotationPageCoords.textBoxRect.locationY = 22;
-    function verifyFinishTextAnnotationMessage() {
+    function verifyFinishTextAnnotationMessage(
+        annotationPageCoords: TextAnnotation) {
       const finishTextAnnotationMessage =
           mockPlugin.findMessage('finishTextAnnotation');
       chrome.test.assertTrue(finishTextAnnotationMessage !== undefined);
@@ -272,17 +325,103 @@
       assertDeepEquals(annotationPageCoords, finishTextAnnotationMessage.data);
     }
 
-    // Committing with edited = true should fire an event.
-    manager.commitTextAnnotation(getTestAnnotation(), true);
-    chrome.test.assertEq(1, finishInkStrokeEvents);
-    verifyFinishTextAnnotationMessage();
+    function testCommitAnnotation(
+        annotationScreenCoords: TextAnnotation,
+        annotationPageCoords: TextAnnotation) {
+      // Listen for PluginControllerEventType.FINISH_INK_STROKE events. The
+      // manager dispatches these on PluginController's eventTarget.
+      let finishInkStrokeEvents = 0;
+      PluginController.getInstance().getEventTarget().addEventListener(
+          PluginControllerEventType.FINISH_INK_STROKE, () => {
+            finishInkStrokeEvents++;
+          });
+
+      // Committing with edited = true should fire an event.
+      // Use structuredClone since the manager edits the object in place,
+      // and we want to reuse this below.
+      manager.commitTextAnnotation(
+          structuredClone(annotationScreenCoords), true);
+      chrome.test.assertEq(1, finishInkStrokeEvents);
+      verifyFinishTextAnnotationMessage(annotationPageCoords);
+      mockPlugin.clearMessages();
+
+      // Committing with edited = false should not fire an event.
+      manager.commitTextAnnotation(
+          structuredClone(annotationScreenCoords), false);
+      chrome.test.assertEq(1, finishInkStrokeEvents);
+      verifyFinishTextAnnotationMessage(annotationPageCoords);
+      mockPlugin.clearMessages();
+    }
+
+    // Test committing annotations at different rotations to ensure the
+    // conversion back to page coordinates works correctly. Note that the
+    // page screen rectangle will be 70x90 since there are 10px of page
+    // shadow.
+
+    // 90 degrees CCW
+    rotateViewport(/* clockwiseRotations= */ 3);
+    let annotationScreenCoords = getTestAnnotation(3);
+    let annotationPageCoords = getTestAnnotation(3);
+    annotationPageCoords.textBoxRect = {
+      height: 50,
+      width: 35,
+      locationX: 13,
+      locationY: 15,
+    };
+    testCommitAnnotation(annotationScreenCoords, annotationPageCoords);
+    // Delete to clear state.
+    annotationScreenCoords.text = '';
+    manager.commitTextAnnotation(annotationScreenCoords, true);
     mockPlugin.clearMessages();
 
-    // Committing with edited = false should not fire an event.
-    manager.commitTextAnnotation(getTestAnnotation(), false);
-    chrome.test.assertEq(1, finishInkStrokeEvents);
-    verifyFinishTextAnnotationMessage();
+    // 180 degrees
+    rotateViewport(/* clockwiseRotations= */ 2);
+    annotationScreenCoords = getTestAnnotation(2);
+    annotationPageCoords = getTestAnnotation(2);
+    // Adjust by the x and y offsets to get to page coordinates.
+    annotationPageCoords.textBoxRect = {
+      height: 35,
+      width: 50,
+      locationX: 15,
+      locationY: 33,
+    };
+    testCommitAnnotation(annotationScreenCoords, annotationPageCoords);
+    // Delete to clear state.
+    annotationScreenCoords.text = '';
+    manager.commitTextAnnotation(annotationScreenCoords, true);
+    mockPlugin.clearMessages();
 
+    // 90 degrees CW
+    rotateViewport(/* clockwiseRotations= */ 1);
+    annotationScreenCoords = getTestAnnotation(1);
+    annotationPageCoords = getTestAnnotation(1);
+    // Adjust by the x and y offsets to get to page coordinates.
+    annotationPageCoords.textBoxRect = {
+      height: 50,
+      width: 35,
+      locationX: 22,
+      locationY: 25,
+    };
+    testCommitAnnotation(annotationScreenCoords, annotationPageCoords);
+    // Delete to clear state.
+    annotationScreenCoords.text = '';
+    manager.commitTextAnnotation(annotationScreenCoords, true);
+    mockPlugin.clearMessages();
+
+    // Normal orientation (0 degrees).
+    rotateViewport(/* clockwiseRotations= */ 0);
+    annotationScreenCoords = getTestAnnotation(0);
+    annotationPageCoords = getTestAnnotation(0);
+    // Adjust by the x and y offsets to get to page coordinates.
+    annotationPageCoords.textBoxRect = {
+      height: 35,
+      width: 50,
+      locationX: 5,
+      locationY: 22,
+    };
+    testCommitAnnotation(annotationScreenCoords, annotationPageCoords);
+    // Note: not deleting since we re-activate this annotation in the next
+    // test.
     chrome.test.succeed();
   },
 
@@ -304,9 +443,10 @@
     chrome.test.assertEq(2, eventsDispatched.length);
     chrome.test.assertEq('initialize-text-box', eventsDispatched[0]!.name);
     const initData = eventsDispatched[0]!.detail as TextBoxInit;
-    const testAnnotation = getTestAnnotation();
+    const testAnnotation = getTestAnnotation(0);
     assertDeepEquals(testAnnotation, initData.annotation);
-    chrome.test.assertEq(10, initData.pageCoordinates.x);
+    // Still using the 80x100 page from the previous test.
+    chrome.test.assertEq(15, initData.pageCoordinates.x);
     chrome.test.assertEq(3, initData.pageCoordinates.y);
     chrome.test.assertEq('attributes-changed', eventsDispatched[1]!.name);
     assertDeepEquals(
@@ -314,12 +454,7 @@
 
     // Since this is an existing annotation, it should send a start message to
     // the plugin.
-    const startTextAnnotationMessage =
-        mockPlugin.findMessage('startTextAnnotation');
-    chrome.test.assertTrue(startTextAnnotationMessage !== undefined);
-    chrome.test.assertEq(
-        'startTextAnnotation', startTextAnnotationMessage.type);
-    chrome.test.assertEq(0, startTextAnnotationMessage.data);
+    verifyStartTextAnnotationMessage(true);
     chrome.test.succeed();
   },
 
@@ -327,10 +462,26 @@
     const initialParams = manager.getViewportParams();
     chrome.test.assertEq(1.0, initialParams.zoom);
     // pageMarginY * zoom = 3 * 1
-    chrome.test.assertEq(3, initialParams.pageY);
+    chrome.test.assertEq(3, initialParams.pageDimensions.y);
     // (windowWidth - docWidth * zoom)/2 + pageMarginX * zoom =
-    // (100 - 90 * 1)/2 + 5 * 1
-    chrome.test.assertEq(10, initialParams.pageX);
+    // (100 - 80 * 1)/2 + 5 * 1
+    chrome.test.assertEq(15, initialParams.pageDimensions.x);
+    // 10px of width are taken up by PAGE_SHADOW.
+    chrome.test.assertEq(70, initialParams.pageDimensions.width);
+    // 20px of height are also taken up by PAGE_SHADOW.
+    chrome.test.assertEq(90, initialParams.pageDimensions.height);
+    chrome.test.assertEq(0, initialParams.clockwiseRotations);
+
+    // In this new layout, the existing 50x35 annotation at page coordinate
+    // 5, 22 has its top left corner at 20, 25 in screen coordinates. Make
+    // sure clicking there creates the box, and clicking just outside of this
+    // does not.
+    mockPlugin.clearMessages();
+    Ink2Manager.getInstance().initializeTextAnnotation({x: 20, y: 25});
+    verifyStartTextAnnotationMessage(true);
+    mockPlugin.clearMessages();
+    Ink2Manager.getInstance().initializeTextAnnotation({x: 19, y: 24});
+    verifyStartTextAnnotationMessage(false);
 
     // Zoom out should fire an event.
     let whenViewportChanged = eventToPromise('viewport-changed', manager);
@@ -338,10 +489,22 @@
     let changedEvent = await whenViewportChanged;
     chrome.test.assertEq(0.5, changedEvent.detail.zoom);
     // pageMarginY * zoom = 3 * .5
-    chrome.test.assertEq(1.5, changedEvent.detail.pageY);
+    chrome.test.assertEq(1.5, changedEvent.detail.pageDimensions.y);
     // (windowWidth - docWidth * zoom)/2 + pageMarginX * zoom =
-    // (100 - 90 * .5)/2 + 5 * .5
-    chrome.test.assertEq(30, changedEvent.detail.pageX);
+    // (100 - 80 * .5)/2 + 5 * .5
+    chrome.test.assertEq(32.5, changedEvent.detail.pageDimensions.x);
+    chrome.test.assertEq(35, changedEvent.detail.pageDimensions.width);
+    chrome.test.assertEq(45, changedEvent.detail.pageDimensions.height);
+    chrome.test.assertEq(0, changedEvent.detail.clockwiseRotations);
+
+    // In this new layout, the existing 50x35 annotation at page coordinate
+    // 5, 22 has its top left corner at 35, 12.5 in screen coordinates.
+    mockPlugin.clearMessages();
+    Ink2Manager.getInstance().initializeTextAnnotation({x: 35, y: 13});
+    verifyStartTextAnnotationMessage(true);
+    mockPlugin.clearMessages();
+    Ink2Manager.getInstance().initializeTextAnnotation({x: 34, y: 12});
+    verifyStartTextAnnotationMessage(false);
 
     // Zoom in should fire an event.
     whenViewportChanged = eventToPromise('viewport-changed', manager);
@@ -349,9 +512,21 @@
     changedEvent = await whenViewportChanged;
     chrome.test.assertEq(2, changedEvent.detail.zoom);
     // pageMarginY * zoom = 3 * 2
-    chrome.test.assertEq(6, changedEvent.detail.pageY);
+    chrome.test.assertEq(6, changedEvent.detail.pageDimensions.y);
     // docWidth * zoom > windowWidth, so this is now pageMarginX * zoom = 5 * 2
-    chrome.test.assertEq(10, changedEvent.detail.pageX);
+    chrome.test.assertEq(10, changedEvent.detail.pageDimensions.x);
+    chrome.test.assertEq(140, changedEvent.detail.pageDimensions.width);
+    chrome.test.assertEq(180, changedEvent.detail.pageDimensions.height);
+    chrome.test.assertEq(0, changedEvent.detail.clockwiseRotations);
+
+    // In this new layout, the existing 50x35 annotation at page coordinate
+    // 5, 22 has its top left corner at 25, 50 in screen coordinates.
+    mockPlugin.clearMessages();
+    Ink2Manager.getInstance().initializeTextAnnotation({x: 25, y: 50});
+    verifyStartTextAnnotationMessage(true);
+    mockPlugin.clearMessages();
+    Ink2Manager.getInstance().initializeTextAnnotation({x: 24, y: 49});
+    verifyStartTextAnnotationMessage(false);
 
     // Translation.
     whenViewportChanged = eventToPromise('viewport-changed', manager);
@@ -359,9 +534,48 @@
     changedEvent = await whenViewportChanged;
     chrome.test.assertEq(2, changedEvent.detail.zoom);
     // Shifts by -20 * zoom = -40 from previous position.
-    chrome.test.assertEq(-34, changedEvent.detail.pageY);
+    chrome.test.assertEq(-34, changedEvent.detail.pageDimensions.y);
     // Shifts by -20 * zoom = -40 from previous position.
-    chrome.test.assertEq(-30, changedEvent.detail.pageX);
+    chrome.test.assertEq(-30, changedEvent.detail.pageDimensions.x);
+    chrome.test.assertEq(140, changedEvent.detail.pageDimensions.width);
+    chrome.test.assertEq(180, changedEvent.detail.pageDimensions.height);
+    chrome.test.assertEq(0, changedEvent.detail.clockwiseRotations);
+
+    // In this new layout, the existing 50x35 annotation at page coordinate
+    // 5, 22 has its top left corner at -15, 10 in screen coordinates.
+    // It has width 100 and height 70 so (0, 81) should be just outside the box
+    // and (0, 80) just inside.
+    mockPlugin.clearMessages();
+    Ink2Manager.getInstance().initializeTextAnnotation({x: 0, y: 80});
+    verifyStartTextAnnotationMessage(true);
+    mockPlugin.clearMessages();
+    Ink2Manager.getInstance().initializeTextAnnotation({x: 0, y: 81});
+    verifyStartTextAnnotationMessage(false);
+
+    // Rotation
+    whenViewportChanged = eventToPromise('viewport-changed', manager);
+    rotateViewport(/* clockwiseRotations= */ 3);  // 90 degree CCW rotation.
+    changedEvent = await whenViewportChanged;
+    chrome.test.assertEq(2, changedEvent.detail.zoom);
+    chrome.test.assertEq(-34, changedEvent.detail.pageDimensions.y);
+    chrome.test.assertEq(-30, changedEvent.detail.pageDimensions.x);
+    // Width and height are switched.
+    chrome.test.assertEq(180, changedEvent.detail.pageDimensions.width);
+    chrome.test.assertEq(140, changedEvent.detail.pageDimensions.height);
+    // Rotations now non-zero.
+    chrome.test.assertEq(3, changedEvent.detail.clockwiseRotations);
+
+    // In this new layout, the existing 50x35 annotation at page coordinate
+    // 5, 22 has its top left corner at 14, -4 in screen coordinates.
+    // It has width 70 and height 100 so (85, 0) should be just outside the box
+    // and (84, 0) just inside.
+    mockPlugin.clearMessages();
+    Ink2Manager.getInstance().initializeTextAnnotation({x: 84, y: 0});
+    verifyStartTextAnnotationMessage(true);
+    mockPlugin.clearMessages();
+    Ink2Manager.getInstance().initializeTextAnnotation({x: 85, y: 0});
+    verifyStartTextAnnotationMessage(false);
+
     chrome.test.succeed();
   },
 ]);
diff --git a/chrome/test/data/pdf/ink2_text_box_test.ts b/chrome/test/data/pdf/ink2_text_box_test.ts
index f52418e..cacbc04b 100644
--- a/chrome/test/data/pdf/ink2_text_box_test.ts
+++ b/chrome/test/data/pdf/ink2_text_box_test.ts
@@ -15,7 +15,8 @@
 document.body.appendChild(textbox);
 
 function initializeBox(
-    width: number, height: number, x: number, y: number, existing?: boolean) {
+    width: number, height: number, x: number, y: number, existing?: boolean,
+    orientation?: number) {
   manager.dispatchEvent(new CustomEvent('initialize-text-box', {
     detail: {
       annotation: {
@@ -31,6 +32,7 @@
           color: hexToColor(TEXT_COLORS[0]!.color),
         },
         textBoxRect: {height, locationX: x, locationY: y, width},
+        textOrientation: orientation ? orientation : 0,
         id: 0,
         pageNumber: 0,
       },
@@ -325,8 +327,13 @@
 
     // Simulate a zoom change to 0.5. This also comes with x and y changes
     // simulating production.
-    manager.dispatchEvent(new CustomEvent(
-        'viewport-changed', {detail: {pageX: 30, pageY: 1.5, zoom: 0.5}}));
+    manager.dispatchEvent(new CustomEvent('viewport-changed', {
+      detail: {
+        clockwiseRotations: 0,
+        pageDimensions: {x: 30, y: 1.5, width: 45, height: 45},
+        zoom: 0.5,
+      },
+    }));
     await microtasksFinished();
     assertPositionAndSize(textbox, '50px', '50px', '230px', '151.5px');
     chrome.test.assertEq(
@@ -335,8 +342,13 @@
 
     // Simulate a zoom change to 2.0. This also comes with x and y changes
     // simulating production.
-    manager.dispatchEvent(new CustomEvent(
-        'viewport-changed', {detail: {pageX: 10, pageY: 6, zoom: 2.0}}));
+    manager.dispatchEvent(new CustomEvent('viewport-changed', {
+      detail: {
+        clockwiseRotations: 0,
+        pageDimensions: {x: 10, y: 6, width: 180, height: 180},
+        zoom: 2.0,
+      },
+    }));
     await microtasksFinished();
     assertPositionAndSize(textbox, '200px', '200px', '810px', '606px');
     chrome.test.assertEq(
@@ -344,8 +356,13 @@
         getComputedStyle(textbox.$.textbox).getPropertyValue('font-size'));
 
     // Simulate a scroll + resetting zoom to 1.0.
-    manager.dispatchEvent(new CustomEvent(
-        'viewport-changed', {detail: {pageX: 100, pageY: 100, zoom: 1.0}}));
+    manager.dispatchEvent(new CustomEvent('viewport-changed', {
+      detail: {
+        clockwiseRotations: 0,
+        pageDimensions: {x: 100, y: 100, width: 90, height: 90},
+        zoom: 1.0,
+      },
+    }));
     await microtasksFinished();
     assertPositionAndSize(textbox, '100px', '100px', '500px', '400px');
     chrome.test.assertEq(
@@ -353,8 +370,13 @@
         getComputedStyle(textbox.$.textbox).getPropertyValue('font-size'));
 
     // Scroll where start of page is no longer in the viewport.
-    manager.dispatchEvent(new CustomEvent(
-        'viewport-changed', {detail: {pageX: -100, pageY: -100, zoom: 1.0}}));
+    manager.dispatchEvent(new CustomEvent('viewport-changed', {
+      detail: {
+        clockwiseRotations: 0,
+        pageDimensions: {x: -100, y: -100, width: 90, height: 90},
+        zoom: 1.0,
+      },
+    }));
     await microtasksFinished();
     assertPositionAndSize(textbox, '100px', '100px', '300px', '200px');
     chrome.test.assertEq(
@@ -362,8 +384,13 @@
         getComputedStyle(textbox.$.textbox).getPropertyValue('font-size'));
 
     // Scroll where textbox ends up off screen.
-    manager.dispatchEvent(new CustomEvent(
-        'viewport-changed', {detail: {pageX: -500, pageY: -500, zoom: 1.0}}));
+    manager.dispatchEvent(new CustomEvent('viewport-changed', {
+      detail: {
+        clockwiseRotations: 0,
+        pageDimensions: {x: -500, y: -500, width: 90, height: 90},
+        zoom: 1.0,
+      },
+    }));
     await microtasksFinished();
     assertPositionAndSize(textbox, '100px', '100px', '-100px', '-200px');
     chrome.test.assertEq(
@@ -372,6 +399,134 @@
     chrome.test.succeed();
   },
 
+  async function testViewportRotationChanges() {
+    // Custom init with different x offsets to simulate a rectangular page with
+    // rotations.
+    function initializeBoxWithOrientation(
+        width: number, height: number, x: number, y: number,
+        orientation: number) {
+      manager.dispatchEvent(new CustomEvent('initialize-text-box', {
+        detail: {
+          annotation: {
+            text: '',
+            textAttributes: {
+              size: 12,
+              typeface: TextTypeface.SANS_SERIF,
+              styles: {
+                [TextStyle.BOLD]: false,
+                [TextStyle.ITALIC]: false,
+              },
+              alignment: TextAlignment.LEFT,
+              color: hexToColor(TEXT_COLORS[0]!.color),
+            },
+            textBoxRect: {height, locationX: x, locationY: y, width},
+            textOrientation: orientation,
+            id: 0,
+            pageNumber: 0,
+          },
+          pageCoordinates: orientation % 2 === 0 ? {x: 15, y: 3} : {x: 5, y: 3},
+        },
+      }));
+    }
+
+    // Helper to update the viewport to the specified number of clockwise
+    // rotations.
+    function updateViewportWithClockwiseRotations(rotations: number):
+        Promise<void> {
+      // Simulating real viewport changes. The x offset reduces when the
+      // page is flipped horizontally, since it takes the whole window.
+      // width and height flip when the page is horizontal.
+      const x = rotations % 2 === 0 ? 15 : 5;
+      const width = rotations % 2 === 0 ? 80 : 100;
+      const height = rotations % 2 === 0 ? 100 : 80;
+      manager.dispatchEvent(new CustomEvent('viewport-changed', {
+        detail: {
+          clockwiseRotations: rotations,
+          pageDimensions: {x, y: 3, width, height},
+          zoom: 1.0,
+        },
+      }));
+      return microtasksFinished();
+    }
+
+    // Helper to check that the textbox styles match the expected rotation of
+    // the text (in number of 90 degree clockwise rotations).
+    function assertTextboxStyles(expectedTextRotation: number) {
+      const expectedTransform =
+          expectedTextRotation === 2 ? 'matrix(-1, 0, 0, -1, 0, 0)' : 'none';
+      let expectedWritingMode = 'horizontal-tb';
+      if (expectedTextRotation === 1) {
+        expectedWritingMode = 'vertical-rl';
+      } else if (expectedTextRotation === 3) {
+        expectedWritingMode = 'sideways-lr';
+      }
+      const styles = getComputedStyle(textbox.$.textbox);
+      chrome.test.assertEq(
+          expectedTransform, styles.getPropertyValue('transform'));
+      chrome.test.assertEq(
+          expectedWritingMode, styles.getPropertyValue('writing-mode'));
+    }
+
+    // Initialize to a 50x48 box at 20, 30 + page offsets. Make box rotated
+    // by 90 degrees clockwise compared to the PDF. This happens when the
+    // viewport is rotated by 90 degrees CCW and the user creates a new
+    // annotation, so simulate that scenario here.
+    await updateViewportWithClockwiseRotations(3);
+    initializeBoxWithOrientation(50, 48, 25, 33, 1);
+    await microtasksFinished();
+    // Position and size are in viewport coordinates, so the box is 50x48 in
+    // the rotated viewport.
+    assertPositionAndSize(textbox, '50px', '48px', '25px', '33px');
+    // Textbox is non-rotated relative to the current viewport orientation.
+    assertTextboxStyles(0);
+
+    await updateViewportWithClockwiseRotations(0);
+    assertPositionAndSize(textbox, '48px', '50px', '17px', '23px');
+    assertTextboxStyles(1);
+
+    await updateViewportWithClockwiseRotations(1);
+    assertPositionAndSize(textbox, '50px', '48px', '35px', '5px');
+    assertTextboxStyles(2);
+
+    await updateViewportWithClockwiseRotations(2);
+    assertPositionAndSize(textbox, '48px', '50px', '45px', '33px');
+    assertTextboxStyles(3);
+
+    // Back to the original position, size and style since we've now rotated
+    // all the way around.
+    await updateViewportWithClockwiseRotations(3);
+    assertPositionAndSize(textbox, '50px', '48px', '25px', '33px');
+    assertTextboxStyles(0);
+
+    // Now initialize a box with no rotation relative to the PDF, at the same
+    // location. This happens when the viewport has no rotation when the box is
+    // created.
+    await updateViewportWithClockwiseRotations(0);
+    initializeBoxWithOrientation(50, 48, 35, 33, 0);
+    await microtasksFinished();
+    assertPositionAndSize(textbox, '50px', '48px', '35px', '33px');
+    assertTextboxStyles(0);
+
+    await updateViewportWithClockwiseRotations(1);
+    assertPositionAndSize(textbox, '48px', '50px', '27px', '23px');
+    assertTextboxStyles(1);
+
+    await updateViewportWithClockwiseRotations(2);
+    assertPositionAndSize(textbox, '50px', '48px', '25px', '25px');
+    assertTextboxStyles(2);
+
+    await updateViewportWithClockwiseRotations(3);
+    assertPositionAndSize(textbox, '48px', '50px', '35px', '13px');
+    assertTextboxStyles(3);
+
+    // Back to 0 rotation should get us back to the original location and style.
+    await updateViewportWithClockwiseRotations(0);
+    assertPositionAndSize(textbox, '50px', '48px', '35px', '33px');
+    assertTextboxStyles(0);
+
+    chrome.test.succeed();
+  },
+
   async function testCommit() {
     // Initialize to a 100x100 box at 400, 300.
     initializeBox(100, 100, 400, 300);
@@ -411,6 +566,7 @@
       },
       // Messages to the backend are in page coordinates.
       textBoxRect: {locationX: 195, locationY: 147, height: 50, width: 50},
+      textOrientation: 0,
     };
 
     function startNewAnnotationAndVerifyMessage(existing: boolean = false) {
diff --git a/chrome/test/data/webui/glic/test_client/index.html b/chrome/test/data/webui/glic/test_client/index.html
index 4d7510f..47bffaa 100644
--- a/chrome/test/data/webui/glic/test_client/index.html
+++ b/chrome/test/data/webui/glic/test_client/index.html
@@ -193,6 +193,7 @@
   <h1>Test Web Client</h1>
   <button id="refreshbn">🔄</button>
   <button id="closebn">❌</button>
+  <button id="shutdownbn">⏻</button>
 </div>
 <div id="content">
 <div class="section">
diff --git a/chrome/test/data/webui/glic/test_client/page_element_types.ts b/chrome/test/data/webui/glic/test_client/page_element_types.ts
index 6953c37..a9cd1fcb 100644
--- a/chrome/test/data/webui/glic/test_client/page_element_types.ts
+++ b/chrome/test/data/webui/glic/test_client/page_element_types.ts
@@ -53,6 +53,7 @@
   permissionSelect: HTMLSelectElement;
   enabledSelect: HTMLSelectElement;
   closebn: HTMLButtonElement;
+  shutdownbn: HTMLButtonElement;
   attachpanelbn: HTMLButtonElement;
   detachpanelbn: HTMLButtonElement;
   refreshbn: HTMLButtonElement;
diff --git a/chrome/test/data/webui/glic/test_client/test_client.ts b/chrome/test/data/webui/glic/test_client/test_client.ts
index 269fbd6..1b414a4 100644
--- a/chrome/test/data/webui/glic/test_client/test_client.ts
+++ b/chrome/test/data/webui/glic/test_client/test_client.ts
@@ -113,6 +113,9 @@
 $.closebn.addEventListener('click', () => {
   getBrowser()!.closePanel!();
 });
+$.shutdownbn.addEventListener('click', () => {
+  getBrowser()!.closePanelAndShutdown!();
+});
 $.attachpanelbn.addEventListener('click', () => {
   getBrowser()!.attachPanel!();
 });
diff --git a/chrome/test/data/webui/print_preview/invalid_settings_test.ts b/chrome/test/data/webui/print_preview/invalid_settings_test.ts
index 240857f..fcafa47 100644
--- a/chrome/test/data/webui/print_preview/invalid_settings_test.ts
+++ b/chrome/test/data/webui/print_preview/invalid_settings_test.ts
@@ -85,7 +85,7 @@
         previewAreaEl.shadowRoot.querySelector('.preview-area-overlay-layer')!;
     const messageEl =
         previewAreaEl.shadowRoot.querySelector('.preview-area-message')!;
-    const sidebar = page.shadowRoot!.querySelector('print-preview-sidebar')!;
+    const sidebar = page.shadowRoot.querySelector('print-preview-sidebar')!;
     let printButton: CrButtonElement;
     const destinationSettings =
         sidebar.shadowRoot.querySelector('print-preview-destination-settings')!;
diff --git a/chrome/test/data/webui/print_preview/key_event_test.ts b/chrome/test/data/webui/print_preview/key_event_test.ts
index 639b4d9..ff11705 100644
--- a/chrome/test/data/webui/print_preview/key_event_test.ts
+++ b/chrome/test/data/webui/print_preview/key_event_test.ts
@@ -70,7 +70,7 @@
   test('EnterOnInputTriggersPrint', function() {
     const whenPrintCalled = nativeLayer.whenCalled('doPrint');
     keyEventOn(
-        page.shadowRoot!.querySelector('print-preview-sidebar')!.shadowRoot
+        page.shadowRoot.querySelector('print-preview-sidebar')!.shadowRoot
             .querySelector('print-preview-copies-settings')!.shadowRoot
             .querySelector('print-preview-number-settings-section')!.shadowRoot
             .querySelector('cr-input')!.inputElement,
@@ -84,7 +84,7 @@
       'EnterOnDropdownDoesNotPrint', function() {
         const whenKeyEventFired = eventToPromise('keydown', page);
         keyEventOn(
-            page.shadowRoot!.querySelector('print-preview-sidebar')!.shadowRoot
+            page.shadowRoot.querySelector('print-preview-sidebar')!.shadowRoot
                 .querySelector('print-preview-layout-settings')!.shadowRoot
                 .querySelector<HTMLSelectElement>('.md-select')!,
             'keydown', 0, [], 'Enter');
@@ -96,11 +96,11 @@
   // comes from a button.
   test('EnterOnButtonDoesNotPrint', async () => {
     const moreSettingsElement =
-        page.shadowRoot!.querySelector('print-preview-sidebar')!.shadowRoot
+        page.shadowRoot.querySelector('print-preview-sidebar')!.shadowRoot
             .querySelector('print-preview-more-settings')!;
     moreSettingsElement.$.label.click();
     const button =
-        page.shadowRoot!.querySelector('print-preview-sidebar')!.shadowRoot
+        page.shadowRoot.querySelector('print-preview-sidebar')!.shadowRoot
             .querySelector('print-preview-advanced-options-settings')!
             .shadowRoot!.querySelector('cr-button')!;
     const whenKeyEventFired = eventToPromise('keydown', button);
@@ -115,12 +115,12 @@
   test(
       'EnterOnCheckboxDoesNotPrint', function() {
         const moreSettingsElement =
-            page.shadowRoot!.querySelector('print-preview-sidebar')!.shadowRoot
+            page.shadowRoot.querySelector('print-preview-sidebar')!.shadowRoot
                 .querySelector('print-preview-more-settings')!;
         moreSettingsElement.$.label.click();
         const whenKeyEventFired = eventToPromise('keydown', page);
         keyEventOn(
-            page.shadowRoot!.querySelector('print-preview-sidebar')!.shadowRoot
+            page.shadowRoot.querySelector('print-preview-sidebar')!.shadowRoot
                 .querySelector('print-preview-other-options-settings')!
                 .shadowRoot.querySelector('cr-checkbox')!,
             'keydown', 0, [], 'Enter');
diff --git a/chrome/test/data/webui/print_preview/policy_test.ts b/chrome/test/data/webui/print_preview/policy_test.ts
index b724033..3f1ca710 100644
--- a/chrome/test/data/webui/print_preview/policy_test.ts
+++ b/chrome/test/data/webui/print_preview/policy_test.ts
@@ -99,13 +99,13 @@
 
   function toggleMoreSettings() {
     const moreSettingsElement =
-        page.shadowRoot!.querySelector('print-preview-sidebar')!.shadowRoot
+        page.shadowRoot.querySelector('print-preview-sidebar')!.shadowRoot
             .querySelector('print-preview-more-settings')!;
     moreSettingsElement.$.label.click();
   }
 
   function getCheckbox(settingName: string): CrCheckboxElement {
-    return page.shadowRoot!.querySelector('print-preview-sidebar')!.shadowRoot
+    return page.shadowRoot.querySelector('print-preview-sidebar')!.shadowRoot
         .querySelector('print-preview-other-options-settings')!.shadowRoot
         .querySelector<CrCheckboxElement>(`#${settingName}`)!;
   }
@@ -246,7 +246,7 @@
       }]);
       toggleMoreSettings();
       const mediaSettingsSelect =
-          page.shadowRoot!.querySelector('print-preview-sidebar')!.shadowRoot
+          page.shadowRoot.querySelector('print-preview-sidebar')!.shadowRoot
               .querySelector('print-preview-media-size-settings')!.shadowRoot
               .querySelector('print-preview-settings-select')!.shadowRoot
               .querySelector('select')!;
diff --git a/chrome/test/data/webui/print_preview/preview_generation_test.ts b/chrome/test/data/webui/print_preview/preview_generation_test.ts
index 7443e06..ae528e6 100644
--- a/chrome/test/data/webui/print_preview/preview_generation_test.ts
+++ b/chrome/test/data/webui/print_preview/preview_generation_test.ts
@@ -5,6 +5,7 @@
 import type {NativeInitialSettings, PreviewTicket, PrintPreviewAppElement, PrintPreviewDestinationSettingsElement, Range, Settings} from 'chrome://print/print_preview.js';
 import {ColorMode, CustomMarginsOrientation, Destination, DestinationOrigin, Margins, MarginsType, NativeLayerImpl, PluginProxyImpl, ScalingType} from 'chrome://print/print_preview.js';
 import {assertEquals, assertFalse, assertTrue} from 'chrome://webui-test/chai_assert.js';
+import {microtasksFinished} from 'chrome://webui-test/test_util.js';
 
 import {NativeLayerStub} from './native_layer_stub.js';
 import {getCddTemplate, getDefaultInitialSettings} from './print_preview_test_utils.js';
@@ -150,7 +151,7 @@
    */
   test('CustomMargins', function() {
     return initialize()
-        .then(function(args) {
+        .then(async function(args) {
           const originalTicket: PreviewTicket = JSON.parse(args.printTicket);
           assertEquals(MarginsType.DEFAULT, originalTicket.marginsType);
           // Custom margins should not be set in the ticket.
@@ -159,10 +160,13 @@
 
           // This should do nothing.
           page.setSetting('margins', MarginsType.CUSTOM);
+          await microtasksFinished();
           // Sets only 1 side, not valid.
           page.setSetting('customMargins', {marginTop: 25});
+          await microtasksFinished();
           // 2 sides, still not valid.
           page.setSetting('customMargins', {marginTop: 25, marginRight: 40});
+          await microtasksFinished();
           // This should trigger a preview.
           nativeLayer.resetResolver('getPreview');
           page.setSetting('customMargins', {
@@ -173,7 +177,7 @@
           });
           return nativeLayer.whenCalled('getPreview');
         })
-        .then(function(args) {
+        .then(async function(args) {
           const ticket: PreviewTicket = JSON.parse(args.printTicket);
           assertEquals(MarginsType.CUSTOM, ticket.marginsType);
           assertEquals(25, ticket.marginsCustom!.marginTop);
@@ -184,7 +188,9 @@
           page.setSetting('margins', MarginsType.DEFAULT);
           // Set setting to something invalid and then set margins to CUSTOM.
           page.setSetting('customMargins', {marginTop: 25, marginRight: 40});
+          await microtasksFinished();
           page.setSetting('margins', MarginsType.CUSTOM);
+          await microtasksFinished();
           nativeLayer.resetResolver('getPreview');
           page.setSetting('customMargins', {
             marginTop: 25,
@@ -561,9 +567,8 @@
         .then(function(args) {
           const originalTicket: PreviewTicket = JSON.parse(args.printTicket);
           destinationSettings =
-              page.shadowRoot!.querySelector('print-preview-sidebar')!
-                  .shadowRoot.querySelector(
-                      'print-preview-destination-settings')!;
+              page.shadowRoot.querySelector('print-preview-sidebar')!.shadowRoot
+                  .querySelector('print-preview-destination-settings')!;
           assertTrue(!!destinationSettings.destination);
           assertEquals('FooDevice', destinationSettings.destination.id);
           assertEquals('FooDevice', originalTicket.deviceName);
@@ -636,7 +641,9 @@
         assertMarginsFooter(ticket, 0, MarginsType.DEFAULT, true);
 
         // After getting the new layout, a second request should have been
-        // sent.
+        // sent. Need to wait for a cycle since the 2nd request is issued
+        // asynchronously in app.ts.
+        await microtasksFinished();
         assertEquals(2, nativeLayer.getCallCount('getPreview'));
         assertEquals(MarginsType.DEFAULT, page.getSettingValue('margins'));
         assertFalse(page.getSettingValue('headerFooter'));
@@ -645,7 +652,7 @@
         // have the same settings as the original (headers and footers
         // should have been turned off).
         const previewArea =
-            page.shadowRoot!.querySelector('print-preview-preview-area')!;
+            page.shadowRoot.querySelector('print-preview-preview-area')!;
         assertMarginsFooter(
             previewArea.getLastTicketForTest()!, 1, MarginsType.DEFAULT, false);
         nativeLayer.resetResolver('getPreview');
diff --git a/chrome/test/data/webui/print_preview/print_button_test.ts b/chrome/test/data/webui/print_preview/print_button_test.ts
index e1240fe..b547be0 100644
--- a/chrome/test/data/webui/print_preview/print_button_test.ts
+++ b/chrome/test/data/webui/print_preview/print_button_test.ts
@@ -5,7 +5,6 @@
 import type {CrButtonElement, NativeInitialSettings, PrintPreviewAppElement, PrintTicket} from 'chrome://print/print_preview.js';
 import {
   NativeLayerImpl, PluginProxyImpl, State} from 'chrome://print/print_preview.js';
-import {flush} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
 import {assertDeepEquals, assertEquals, assertFalse, assertTrue} from 'chrome://webui-test/chai_assert.js';
 
 import {NativeLayerStub} from './native_layer_stub.js';
@@ -67,7 +66,6 @@
         printButton.click();
       }
       if (cancelBeforePreviewReady) {
-        flush();
         const cancelButton =
             buttonStrip.shadowRoot.querySelector<CrButtonElement>(
                 '.cancel-button');
diff --git a/chrome/test/data/webui/print_preview/print_preview_app_test.ts b/chrome/test/data/webui/print_preview/print_preview_app_test.ts
index d350e97..12540218 100644
--- a/chrome/test/data/webui/print_preview/print_preview_app_test.ts
+++ b/chrome/test/data/webui/print_preview/print_preview_app_test.ts
@@ -62,8 +62,8 @@
 
   test('PrintPresets', async () => {
     await initialize();
-    assertEquals(1, page.settings.copies.value);
-    assertFalse(page.settings.duplex.value);
+    assertEquals(1, page.getSettingValue('copies'));
+    assertFalse(page.getSettingValue('duplex'));
 
     // Send preset values of duplex LONG_EDGE and 2 copies.
     const copies = 2;
@@ -78,21 +78,21 @@
   test('DestinationsManaged', async () => {
     initialSettings.destinationsManaged = true;
     await initialize();
-    const sidebar = page.shadowRoot!.querySelector('print-preview-sidebar')!;
+    const sidebar = page.shadowRoot.querySelector('print-preview-sidebar')!;
     assertTrue(sidebar.controlsManaged);
   });
 
   test('HeaderFooterManaged', async () => {
     initialSettings.policies = {headerFooter: {allowedMode: true}};
     await initialize();
-    const sidebar = page.shadowRoot!.querySelector('print-preview-sidebar')!;
+    const sidebar = page.shadowRoot.querySelector('print-preview-sidebar')!;
     assertTrue(sidebar.controlsManaged);
   });
 
   test('CssBackgroundManaged', async () => {
     initialSettings.policies = {cssBackground: {allowedMode: 1}};
     await initialize();
-    const sidebar = page.shadowRoot!.querySelector('print-preview-sidebar')!;
+    const sidebar = page.shadowRoot.querySelector('print-preview-sidebar')!;
     assertTrue(sidebar.controlsManaged);
   });
 });
diff --git a/chrome/test/data/webui/print_preview/restore_state_test.ts b/chrome/test/data/webui/print_preview/restore_state_test.ts
index 58bd9dc..7ed889d 100644
--- a/chrome/test/data/webui/print_preview/restore_state_test.ts
+++ b/chrome/test/data/webui/print_preview/restore_state_test.ts
@@ -29,23 +29,25 @@
   function verifyStickySettingsApplied(stickySettings: SerializedSettings) {
     assertEquals(
         stickySettings.dpi!.horizontal_dpi,
-        page.settings.dpi.value.horizontal_dpi);
+        page.getSetting('dpi').value.horizontal_dpi);
     assertEquals(
-        stickySettings.dpi!.vertical_dpi, page.settings.dpi.value.vertical_dpi);
+        stickySettings.dpi!.vertical_dpi,
+        page.getSetting('dpi').value.vertical_dpi);
     assertEquals(
-        stickySettings.mediaSize!.name, page.settings.mediaSize.value.name);
+        stickySettings.mediaSize!.name,
+        page.getSetting('mediaSize').value.name);
     assertEquals(
         stickySettings.mediaSize!.height_microns,
-        page.settings.mediaSize.value.height_microns);
+        page.getSetting('mediaSize').value.height_microns);
     assertEquals(
         stickySettings.mediaSize!.width_microns,
-        page.settings.mediaSize.value.width_microns);
+        page.getSetting('mediaSize').value.width_microns);
     assertEquals(
         (stickySettings.vendorOptions! as {[key: string]: any})['paperType'],
-        page.settings.vendorItems.value.paperType);
+        page.getSetting('vendorItems').value.paperType);
     assertEquals(
         (stickySettings.vendorOptions! as {[key: string]: any})['printArea'],
-        page.settings.vendorItems.value.printArea);
+        page.getSetting('vendorItems').value.printArea);
 
     ([
       ['margins', 'marginsType'],
@@ -61,7 +63,7 @@
         .forEach(keys => {
           assertEquals(
               (stickySettings as {[key: string]: any})[keys[1]],
-              page.settings[keys[0]].value);
+              page.getSetting(keys[0]).value);
         });
   }
 
@@ -302,7 +304,7 @@
       // production, just use the model instead of creating the dialog.
       const element = testValue.settingName === 'vendorItems' ?
           getInstance() :
-          page.shadowRoot!.querySelector('print-preview-sidebar')!.shadowRoot
+          page.shadowRoot.querySelector('print-preview-sidebar')!.shadowRoot
               .querySelector<SettingsMixinInterface&HTMLElement>(
                   testValue.section)!;
       element.setSetting(testValue.settingName, testValue.value);
diff --git a/chrome/test/data/webui/print_preview/system_dialog_test.ts b/chrome/test/data/webui/print_preview/system_dialog_test.ts
index 2a0426d8..954b8ca 100644
--- a/chrome/test/data/webui/print_preview/system_dialog_test.ts
+++ b/chrome/test/data/webui/print_preview/system_dialog_test.ts
@@ -42,7 +42,7 @@
 
     const page = document.createElement('print-preview-app');
     document.body.appendChild(page);
-    sidebar = page.shadowRoot!.querySelector('print-preview-sidebar')!;
+    sidebar = page.shadowRoot.querySelector('print-preview-sidebar')!;
     return Promise
         .all([
           waitBeforeNextRender(page),
diff --git a/chrome/test/data/webui/side_panel/read_anything/BUILD.gn b/chrome/test/data/webui/side_panel/read_anything/BUILD.gn
index a8dad42..13aa0895 100644
--- a/chrome/test/data/webui/side_panel/read_anything/BUILD.gn
+++ b/chrome/test/data/webui/side_panel/read_anything/BUILD.gn
@@ -39,7 +39,6 @@
     "node_store_test.ts",
     "phrase_highlighting_test.ts",
     "play_pause_test.ts",
-    "prefs_test.ts",
     "rate_selection_test.ts",
     "read_aloud_flag_test.ts",
     "read_aloud_highlighting_test.ts",
diff --git a/chrome/test/data/webui/side_panel/read_anything/app_receives_toolbar_changes_test.ts b/chrome/test/data/webui/side_panel/read_anything/app_receives_toolbar_changes_test.ts
index 6284bd2..6820576 100644
--- a/chrome/test/data/webui/side_panel/read_anything/app_receives_toolbar_changes_test.ts
+++ b/chrome/test/data/webui/side_panel/read_anything/app_receives_toolbar_changes_test.ts
@@ -225,12 +225,7 @@
   });
 
   suite('play/pause', () => {
-    let propagatedActiveState: boolean;
-
     setup(() => {
-      chrome.readingMode.onSpeechPlayingStateChanged = isSpeechActive => {
-        propagatedActiveState = isSpeechActive;
-      };
       app.updateContent();
       return microtasksFinished();
     });
@@ -240,22 +235,11 @@
       return microtasksFinished();
     }
 
-    test('by default is paused', () => {
-      assertFalse(speechController.isSpeechActive());
-      assertFalse(!!propagatedActiveState);
-      assertFalse(speechController.hasSpeechBeenTriggered());
-
-      // isSpeechTreeInitialized is set in updateContent
-      assertTrue(speechController.isSpeechTreeInitialized());
-    });
-
-
     test('on first click starts speech', async () => {
       await emitPlayPause();
       assertTrue(speechController.isSpeechActive());
       assertTrue(speechController.isSpeechTreeInitialized());
       assertTrue(speechController.hasSpeechBeenTriggered());
-      assertTrue(propagatedActiveState);
     });
 
     test('on second click stops speech', async () => {
@@ -265,7 +249,6 @@
       assertFalse(speechController.isSpeechActive());
       assertTrue(speechController.isSpeechTreeInitialized());
       assertTrue(speechController.hasSpeechBeenTriggered());
-      assertFalse(propagatedActiveState);
     });
 
     suite('on keyboard k pressed', () => {
@@ -280,7 +263,6 @@
         await microtasksFinished();
 
         assertTrue(speechController.isSpeechActive());
-        assertTrue(propagatedActiveState);
         assertEquals(0, metrics.getCallCount('recordSpeechStopSource'));
       });
 
@@ -290,7 +272,6 @@
         await microtasksFinished();
 
         assertFalse(speechController.isSpeechActive());
-        assertFalse(propagatedActiveState);
         assertEquals(
             chrome.readingMode.keyboardShortcutStopSource,
             await metrics.whenCalled('recordSpeechStopSource'));
@@ -398,45 +379,15 @@
       app.updateContent();
     });
 
-    function emitNextGranularity() {
-      emitEvent(app, ToolbarEvent.NEXT_GRANULARITY);
-    }
-
-    function emitPreviousGranularity() {
-      emitEvent(app, ToolbarEvent.PREVIOUS_GRANULARITY);
-    }
-
-    test('next propagates change', () => {
-      let movedToNext = false;
-      chrome.readingMode.movePositionToNextGranularity = () => {
-        movedToNext = true;
-      };
-
-      emitNextGranularity();
-
-      assertTrue(movedToNext);
-    });
-
     test('next highlights text', () => {
-      emitNextGranularity();
+      emitEvent(app, ToolbarEvent.NEXT_GRANULARITY);
       const currentHighlight =
           app.$.container.querySelector('.current-read-highlight');
       assertTrue(!!currentHighlight!.textContent);
     });
 
-    test('previous propagates change', () => {
-      let movedToPrevious: boolean = false;
-      chrome.readingMode.movePositionToPreviousGranularity = () => {
-        movedToPrevious = true;
-      };
-
-      emitPreviousGranularity();
-
-      assertTrue(movedToPrevious);
-    });
-
     test('previous highlights text', () => {
-      emitPreviousGranularity();
+      emitEvent(app, ToolbarEvent.PREVIOUS_GRANULARITY);
       const currentHighlight =
           app.$.container.querySelector('.current-read-highlight');
       assertTrue(!!currentHighlight!.textContent);
diff --git a/chrome/test/data/webui/side_panel/read_anything/common_test.ts b/chrome/test/data/webui/side_panel/read_anything/common_test.ts
index bf295a3..762dde4 100644
--- a/chrome/test/data/webui/side_panel/read_anything/common_test.ts
+++ b/chrome/test/data/webui/side_panel/read_anything/common_test.ts
@@ -2,8 +2,8 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import {getCurrentSpeechRate} from 'chrome-untrusted://read-anything-side-panel.top-chrome/read_anything.js';
-import {assertEquals} from 'chrome-untrusted://webui-test/chai_assert.js';
+import {getCurrentSpeechRate, isRectVisible} from 'chrome-untrusted://read-anything-side-panel.top-chrome/read_anything.js';
+import {assertEquals, assertFalse, assertTrue} from 'chrome-untrusted://webui-test/chai_assert.js';
 
 import {FakeReadingMode} from './fake_reading_mode.js';
 
@@ -13,16 +13,59 @@
     chrome.readingMode = readingMode as unknown as typeof chrome.readingMode;
   });
 
-  suite('getCurrentSpeechRate', () => {
-    test('rounds value to 1 decimal', () => {
-      chrome.readingMode.speechRate = 1.1234567890;
-      assertEquals(1.1, getCurrentSpeechRate());
+  test('getCurrentSpeechRate rounds value to 1 decimal', () => {
+    chrome.readingMode.speechRate = 1.1234567890;
+    assertEquals(1.1, getCurrentSpeechRate());
 
-      chrome.readingMode.speechRate = 0.912345678;
-      assertEquals(0.9, getCurrentSpeechRate());
+    chrome.readingMode.speechRate = 0.912345678;
+    assertEquals(0.9, getCurrentSpeechRate());
 
-      chrome.readingMode.speechRate = 1.199999999;
-      assertEquals(1.2, getCurrentSpeechRate());
+    chrome.readingMode.speechRate = 1.199999999;
+    assertEquals(1.2, getCurrentSpeechRate());
+  });
+
+  suite('isRectVisible', () => {
+    let windowHeight: number;
+    let halfHeight: number;
+
+    setup(() => {
+      windowHeight = document.documentElement.clientHeight;
+      halfHeight = windowHeight / 2;
+      window.innerHeight = windowHeight;
+    });
+
+    test('fully inside window returns true', () => {
+      const rect = new DOMRect(0, 0, halfHeight, halfHeight);
+      assertTrue(isRectVisible(rect));
+    });
+
+    test('bottom inside window returns true', () => {
+      const rect =
+          new DOMRect(-halfHeight, -halfHeight, windowHeight, windowHeight);
+      assertTrue(isRectVisible(rect));
+    });
+
+    test('top inside window returns true', () => {
+      const rect =
+          new DOMRect(halfHeight, halfHeight, windowHeight, windowHeight);
+      assertTrue(isRectVisible(rect));
+    });
+
+    test('bigger than window returns true', () => {
+      const rect = new DOMRect(
+          -halfHeight, -halfHeight, windowHeight * 2, windowHeight * 2);
+      assertTrue(isRectVisible(rect));
+    });
+
+    test('fully above window returns false', () => {
+      const rect = new DOMRect(-halfHeight, -halfHeight, -1, -1);
+      assertFalse(isRectVisible(rect));
+    });
+
+    test('fully below window returns false', () => {
+      const rect = new DOMRect(
+          windowHeight + 1, windowHeight + 1, halfHeight, halfHeight);
+      assertFalse(isRectVisible(rect));
     });
   });
 });
diff --git a/chrome/test/data/webui/side_panel/read_anything/highlighter_test.ts b/chrome/test/data/webui/side_panel/read_anything/highlighter_test.ts
index 28844881..f4367e6 100644
--- a/chrome/test/data/webui/side_panel/read_anything/highlighter_test.ts
+++ b/chrome/test/data/webui/side_panel/read_anything/highlighter_test.ts
@@ -17,6 +17,7 @@
   function assertFullNodeIsHighlighted(id: number, text: string) {
     assertEquals(
         '<span class="current-read-highlight">' + text + '</span>',
+        (nodeStore.getDomNode(id) as Element).innerHTML,
         (nodeStore.getDomNode(id) as Element).innerHTML);
   }
 
@@ -111,6 +112,27 @@
     assertFullNodeIsHighlighted(id2, text2);
   });
 
+  test('with auto highlighting and rate of 2, sentence highlight used', () => {
+    chrome.readingMode.onHighlightGranularityChanged(
+        chrome.readingMode.autoHighlighting);
+    chrome.readingMode.onSpeechRateChange(2);
+
+    const id = 10;
+    const sentence = document.createElement('p');
+    const text = 'Woke up today, feeling the way I always do. ';
+    sentence.appendChild(document.createTextNode(text));
+    nodeStore.setDomNode(sentence, id);
+    chrome.readingMode.getCurrentTextStartIndex = () => 0;
+    chrome.readingMode.getCurrentTextEndIndex = () => text.length;
+
+    highlighter.highlightCurrentGranularity(
+        [id], /*scrollIntoView=*/ false,
+        /*shouldUpdateSentenceHighlight=*/ true);
+
+    assertTrue(highlighter.hasCurrentHighlights());
+    assertFullNodeIsHighlighted(id, text);
+  });
+
   test('word highlight', () => {
     chrome.readingMode.onHighlightGranularityChanged(
         chrome.readingMode.wordHighlighting);
@@ -229,6 +251,32 @@
         id2);
   });
 
+  test(
+      'with auto highlighting and rate of 1, word/phrase highlight used',
+      () => {
+        chrome.readingMode.onHighlightGranularityChanged(
+            chrome.readingMode.autoHighlighting);
+        chrome.readingMode.onSpeechRateChange(1);
+        wordBoundaries.updateBoundary(0);
+        const id = 10;
+        chrome.readingMode.getHighlightForCurrentSegmentIndex =
+            () => [{nodeId: id, start: 0, length: 20}];
+        const sentence = document.createElement('p');
+        sentence.appendChild(document.createTextNode(
+            'Hungry for something that I can\'t eat. '));
+        nodeStore.setDomNode(sentence, id);
+
+        highlighter.highlightCurrentGranularity(
+            [id], /*scrollIntoView=*/ false,
+            /*shouldUpdateSentenceHighlight=*/ true);
+
+        assertTrue(highlighter.hasCurrentHighlights());
+        assertHtml(
+            '<span class="current-read-highlight">Hungry for something</span>' +
+                ' that I can\'t eat. ',
+            id);
+      });
+
   test('remove current highlight', () => {
     chrome.readingMode.onHighlightGranularityChanged(
         chrome.readingMode.sentenceHighlighting);
diff --git a/chrome/test/data/webui/side_panel/read_anything/language_change_test.ts b/chrome/test/data/webui/side_panel/read_anything/language_change_test.ts
index 8b2c4558..f5da52d 100644
--- a/chrome/test/data/webui/side_panel/read_anything/language_change_test.ts
+++ b/chrome/test/data/webui/side_panel/read_anything/language_change_test.ts
@@ -7,62 +7,19 @@
 
 import 'chrome-untrusted://read-anything-side-panel.top-chrome/read_anything.js';
 
-import {BrowserProxy, ToolbarEvent} from 'chrome-untrusted://read-anything-side-panel.top-chrome/read_anything.js';
 import type {AppElement} from 'chrome-untrusted://read-anything-side-panel.top-chrome/read_anything.js';
-import {AVAILABLE_GOOGLE_TTS_LOCALES, convertLangOrLocaleForVoicePackManager, PACK_MANAGER_SUPPORTED_LANGS_AND_LOCALES, SpeechBrowserProxyImpl, VoicePackController} from 'chrome-untrusted://read-anything-side-panel.top-chrome/read_anything.js';
-import {assertEquals, assertFalse, assertTrue} from 'chrome-untrusted://webui-test/chai_assert.js';
+import {BrowserProxy, VoicePackController} from 'chrome-untrusted://read-anything-side-panel.top-chrome/read_anything.js';
+import {assertEquals, assertTrue} from 'chrome-untrusted://webui-test/chai_assert.js';
 import {microtasksFinished} from 'chrome-untrusted://webui-test/test_util.js';
 
-import {createApp, createSpeechSynthesisVoice, emitEvent, setVoices} from './common.js';
+import {createApp} from './common.js';
 import {FakeReadingMode} from './fake_reading_mode.js';
 import {TestColorUpdaterBrowserProxy} from './test_color_updater_browser_proxy.js';
-import {TestSpeechBrowserProxy} from './test_speech_browser_proxy.js';
 
 suite('LanguageChanged', () => {
-  const langForDefaultVoice = 'en';
-  const lang1 = 'zh';
-  const lang2 = 'tr';
-  const lang3 = 'pt-br';
-  const langWithNoVoices = 'elvish';
-
-  const defaultVoice = createSpeechSynthesisVoice({
-    lang: langForDefaultVoice,
-    name: 'Google Kristi',
-    default: true,
-  });
-  const firstVoiceWithLang1 =
-      createSpeechSynthesisVoice({lang: lang1, name: 'Google Lauren'});
-  const defaultVoiceWithLang1 = createSpeechSynthesisVoice(
-      {lang: lang1, name: 'Google Eitan', default: true});
-  const firstVoiceWithLang2 =
-      createSpeechSynthesisVoice({lang: lang2, name: 'Google Yu'});
-  const secondVoiceWithLang2 =
-      createSpeechSynthesisVoice({lang: lang2, name: 'Google Xiang'});
-  const firstVoiceWithLang3 =
-      createSpeechSynthesisVoice({lang: lang3, name: 'Google Kristi'});
-  const naturalVoiceWithLang3 = createSpeechSynthesisVoice(
-      {lang: lang3, name: 'Google Kristi (Natural)'});
-  const otherVoice =
-      createSpeechSynthesisVoice({lang: 'it', name: 'Google Shari'});
-  const voices = [
-    defaultVoice,
-    firstVoiceWithLang1,
-    defaultVoiceWithLang1,
-    otherVoice,
-    firstVoiceWithLang2,
-    secondVoiceWithLang2,
-    firstVoiceWithLang3,
-    naturalVoiceWithLang3,
-  ];
-
   let app: AppElement;
-  let speech: TestSpeechBrowserProxy;
   let voicePackController: VoicePackController;
 
-  function setInstalled(lang: string) {
-    voicePackController.updateVoicePackStatus(lang, 'kInstalled');
-  }
-
   setup(async () => {
     // Clearing the DOM should always be done first.
     document.body.innerHTML = window.trustedTypes!.emptyHTML;
@@ -70,18 +27,9 @@
     const readingMode = new FakeReadingMode();
     chrome.readingMode = readingMode as unknown as typeof chrome.readingMode;
     chrome.readingMode.isReadAloudEnabled = true;
-    speech = new TestSpeechBrowserProxy();
-    SpeechBrowserProxyImpl.setInstance(speech);
-    speech.setVoices(voices);
     voicePackController = new VoicePackController();
     VoicePackController.setInstance(voicePackController);
-
     app = await createApp();
-    for (const v of voices) {
-      setInstalled(v.lang);
-      voicePackController.enableLang(v.lang);
-    }
-    return microtasksFinished();
   });
 
   test('updates toolbar fonts', async () => {
@@ -96,250 +44,21 @@
     assertTrue(updatedFontsOnToolbar);
   });
 
-  test('to the stored voice for this language if there is one', async () => {
-    chrome.readingMode.getStoredVoice = () => otherVoice.name;
-    chrome.readingMode.baseLanguageForSpeech = otherVoice.lang;
+  test('sets current language', () => {
+    const lang1 = 'vi';
+    const lang2 = 'zh';
+    const lang3 = 'tr';
 
+    chrome.readingMode.baseLanguageForSpeech = lang1;
     app.languageChanged();
-    await microtasksFinished();
+    assertEquals(lang1, voicePackController.getCurrentLanguage());
 
-    assertEquals(otherVoice, voicePackController.getCurrentVoice());
-  });
-
-  test('enables the stored voice language', async () => {
-    const voice = createSpeechSynthesisVoice({lang: 'es-us', name: 'Mush'});
-    chrome.readingMode.getStoredVoice = () => voice.name;
-    chrome.readingMode.baseLanguageForSpeech = 'es';
-    setInstalled(voice.lang);
-    setVoices(speech, [voice]);
-    assertFalse(voicePackController.isLangEnabled(voice.lang));
-
+    chrome.readingMode.baseLanguageForSpeech = lang2;
     app.languageChanged();
-    await microtasksFinished();
+    assertEquals(lang2, voicePackController.getCurrentLanguage());
 
-    assertTrue(voicePackController.isLangEnabled(voice.lang));
-    assertEquals(voice, voicePackController.getCurrentVoice());
-  });
-
-  suite('when there is no stored voice for this language', () => {
-    setup(() => {
-      chrome.readingMode.getStoredVoice = () => '';
-    });
-
-    suite('and no voices at all for this language', () => {
-      setup(() => {
-        chrome.readingMode.baseLanguageForSpeech = langWithNoVoices;
-      });
-
-      test('to the current voice if there is one', async () => {
-        emitEvent(
-            app, ToolbarEvent.VOICE, {detail: {selectedVoice: otherVoice}});
-        app.languageChanged();
-        await microtasksFinished();
-        assertEquals(otherVoice, voicePackController.getCurrentVoice());
-      });
-
-      test('to a natural voice if there\'s no current voice', async () => {
-        app.languageChanged();
-        await microtasksFinished();
-        assertEquals(
-            naturalVoiceWithLang3, voicePackController.getCurrentVoice());
-      });
-
-      test('to the device default if there\'s no natural', () => {
-        setVoices(speech, voices.filter(v => v !== naturalVoiceWithLang3));
-        app.languageChanged();
-        assertEquals(defaultVoice, voicePackController.getCurrentVoice());
-      });
-    });
-
-    test('to a voice in the enabled locale for this base language', () => {
-      const voice =
-          createSpeechSynthesisVoice({lang: 'es-us', name: 'Spanish'});
-      voicePackController.enableLang('es-us');
-      setVoices(speech, [voice]);
-      chrome.readingMode.baseLanguageForSpeech = 'es';
-
-      app.languageChanged();
-
-      assertEquals(voice, voicePackController.getCurrentVoice());
-    });
-
-    test('to a voice in the available locale for this base language', () => {
-      const voice =
-          createSpeechSynthesisVoice({lang: 'en-au', name: 'Australian'});
-      setVoices(speech, [voice]);
-      setInstalled('en-au');
-      chrome.readingMode.baseLanguageForSpeech = 'en';
-
-      app.languageChanged();
-
-      assertEquals(voice, voicePackController.getCurrentVoice());
-    });
-
-    suite('and this locale is enabled', () => {
-      test('to a natural voice for this language', () => {
-        chrome.readingMode.baseLanguageForSpeech = lang3;
-        app.languageChanged();
-        assertEquals(
-            naturalVoiceWithLang3, voicePackController.getCurrentVoice());
-      });
-
-      test(
-          'to the default voice for this language if there\'s no natural voice',
-          () => {
-            chrome.readingMode.baseLanguageForSpeech = lang1;
-            app.languageChanged();
-            assertEquals(
-                defaultVoiceWithLang1, voicePackController.getCurrentVoice());
-          });
-
-      test(
-          'to the first listed voice for this language if there\'s no default',
-          () => {
-            chrome.readingMode.baseLanguageForSpeech = lang2;
-            app.languageChanged();
-            assertEquals(
-                firstVoiceWithLang2, voicePackController.getCurrentVoice());
-          });
-    });
-
-    suite('and this locale is disabled', () => {
-      test('and it enables pack manager locale', () => {
-        chrome.readingMode.baseLanguageForSpeech = lang3;
-
-        app.languageChanged();
-
-        assertTrue(voicePackController.isLangEnabled(lang3));
-        assertEquals(
-            naturalVoiceWithLang3, voicePackController.getCurrentVoice());
-      });
-
-      test(
-          'and it enables other locale if not supported by pack manager',
-          () => {
-            chrome.readingMode.baseLanguageForSpeech = lang1;
-
-            app.languageChanged();
-
-            assertTrue(voicePackController.isLangEnabled(lang1));
-            assertEquals(
-                defaultVoiceWithLang1, voicePackController.getCurrentVoice());
-          });
-
-
-      test('to voice in different locale and same language', () => {
-        const voice = createSpeechSynthesisVoice(
-            {lang: 'en-GB', name: 'British', default: true});
-        voicePackController.enableLang('en-gb');
-        setVoices(speech, [voice]);
-        setInstalled('en-gb');
-        setInstalled('en-us');
-        chrome.readingMode.baseLanguageForSpeech = 'en-US';
-
-        app.languageChanged();
-
-        assertEquals(voice, voicePackController.getCurrentVoice());
-      });
-
-      test('to natural enabled voice if no same locale', () => {
-        voicePackController.enableLang(lang3);
-        setVoices(speech, [naturalVoiceWithLang3]);
-        chrome.readingMode.baseLanguageForSpeech = lang2;
-
-        app.languageChanged();
-
-        assertEquals(
-            naturalVoiceWithLang3, voicePackController.getCurrentVoice());
-      });
-
-      test('to default enabled voice if no natural voice', () => {
-        voicePackController.enableLang(lang1);
-        setVoices(speech, [defaultVoiceWithLang1]);
-        chrome.readingMode.baseLanguageForSpeech = lang2;
-
-        app.languageChanged();
-
-        assertEquals(
-            defaultVoiceWithLang1, voicePackController.getCurrentVoice());
-      });
-
-      test('to null if no enabled languages', () => {
-        chrome.readingMode.baseLanguageForSpeech = lang2;
-        for (const lang of voicePackController.getEnabledLangs()) {
-          voicePackController.onLanguageToggle(lang);
-        }
-
-        app.languageChanged();
-
-        assertFalse(!!voicePackController.getCurrentVoice());
-      });
-    });
-  });
-
-  suite('tries to install voice pack', () => {
-    let sentRequest: boolean;
-
-    setup(() => {
-      sentRequest = false;
-      chrome.readingMode.sendGetVoicePackInfoRequest = () => {
-        sentRequest = true;
-      };
-    });
-
-    test('but doesn\'t if the language is unsupported', () => {
-      chrome.readingMode.baseLanguageForSpeech = 'zh';
-
-      app.languageChanged();
-
-      // Use this check to ensure this stays updated if the supported
-      // languages changes.
-      assertFalse(PACK_MANAGER_SUPPORTED_LANGS_AND_LOCALES.has(
-          chrome.readingMode.baseLanguageForSpeech));
-      assertFalse(sentRequest);
-    });
-
-    test('if the language is unsupported but has valid voice pack code', () => {
-      chrome.readingMode.baseLanguageForSpeech = 'bn';
-
-      app.languageChanged();
-
-      // Use this check to ensure this stays updated if the supported
-      // languages changes.
-      assertTrue(PACK_MANAGER_SUPPORTED_LANGS_AND_LOCALES.has(
-          chrome.readingMode.baseLanguageForSpeech));
-      assertFalse(AVAILABLE_GOOGLE_TTS_LOCALES.has(
-          chrome.readingMode.baseLanguageForSpeech));
-      assertTrue(sentRequest);
-    });
-
-    test('but doesn\'t if the language is already installing', () => {
-      const lang = 'bn-bd';
-      const voicePackLang = convertLangOrLocaleForVoicePackManager(lang);
-      assertTrue(!!voicePackLang);
-
-      voicePackController.updateVoicePackStatus(lang, 'kInstalling');
-      app.languageChanged();
-
-      assertFalse(sentRequest);
-    });
-
-    test('and gets voice pack info if no status yet', () => {
-      const lang = 'bn-bd';
-      chrome.readingMode.baseLanguageForSpeech = lang;
-
-      app.languageChanged();
-
-      assertTrue(sentRequest);
-    });
-
-    test('and gets voice pack info if we know it exists', () => {
-      const lang = 'de-de';
-      chrome.readingMode.baseLanguageForSpeech = lang;
-
-      app.languageChanged();
-
-      assertTrue(sentRequest);
-    });
+    chrome.readingMode.baseLanguageForSpeech = lang3;
+    app.languageChanged();
+    assertEquals(lang3, voicePackController.getCurrentLanguage());
   });
 });
diff --git a/chrome/test/data/webui/side_panel/read_anything/phrase_highlighting_test.ts b/chrome/test/data/webui/side_panel/read_anything/phrase_highlighting_test.ts
index 91b8ec75..c4a622a 100644
--- a/chrome/test/data/webui/side_panel/read_anything/phrase_highlighting_test.ts
+++ b/chrome/test/data/webui/side_panel/read_anything/phrase_highlighting_test.ts
@@ -2,18 +2,14 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import type {CrIconButtonElement} from '//resources/cr_elements/cr_icon_button/cr_icon_button.js';
-import type {AppElement, ReadAnythingToolbarElement} from 'chrome-untrusted://read-anything-side-panel.top-chrome/read_anything.js';
+import type {AppElement} from 'chrome-untrusted://read-anything-side-panel.top-chrome/read_anything.js';
 import {ReadAloudHighlighter, SpeechController, VoicePackController, WordBoundaries} from 'chrome-untrusted://read-anything-side-panel.top-chrome/read_anything.js';
 import {assertEquals, assertTrue} from 'chrome-untrusted://webui-test/chai_assert.js';
-import {microtasksFinished} from 'chrome-untrusted://webui-test/test_util.js';
 
-import {createApp, mockMetrics, stubAnimationFrame} from './common.js';
-import type {TestMetricsBrowserProxy} from './test_metrics_browser_proxy.js';
+import {createApp} from './common.js';
 
 suite('PhraseHighlighting', () => {
   let app: AppElement;
-  let metrics: TestMetricsBrowserProxy;
   let wordBoundaries: WordBoundaries;
 
   // root htmlTag='#document' id=1
@@ -69,7 +65,6 @@
     wordBoundaries = new WordBoundaries();
     WordBoundaries.setInstance(wordBoundaries);
     ReadAloudHighlighter.setInstance(new ReadAloudHighlighter());
-    metrics = mockMetrics();
     SpeechController.setInstance(new SpeechController());
     app = await createApp();
 
@@ -79,100 +74,55 @@
     chrome.readingMode.setContentForTesting(axTree, [2, 4]);
   });
 
-  function computeStyle(style: string) {
-    return window.getComputedStyle(app.$.container).getPropertyValue(style);
-  }
+  test('with word highlighting on, word is highlighted', () => {
+    chrome.readingMode.onHighlightGranularityChanged(
+        chrome.readingMode.wordHighlighting);
 
-  suite('changing the highlight from the menu', () => {
-    let toolbar: ReadAnythingToolbarElement;
-    let highlightButton: CrIconButtonElement;
-    let options: HTMLButtonElement[];
+    wordBoundaries.updateBoundary(0);
+    app.playSpeech();
+    const currentHighlight =
+        app.$.container.querySelector('.current-read-highlight');
+    assertTrue(currentHighlight !== undefined);
+    assertEquals(currentHighlight!.textContent!, 'This ');
+  });
 
-    setup(async () => {
-      toolbar = app.$.toolbar;
-      highlightButton =
-          toolbar.$.toolbarContainer.querySelector<CrIconButtonElement>(
-              '#highlight')!;
-      stubAnimationFrame();
-      highlightButton.click();
+  test('with phrase highlighting on, phrase is highlighted', () => {
+    chrome.readingMode.onHighlightGranularityChanged(
+        chrome.readingMode.phraseHighlighting);
 
-      await microtasksFinished();
+    wordBoundaries.updateBoundary(0);
+    app.playSpeech();
 
-      const menu = toolbar.$.highlightMenu.$.menu.$.lazyMenu.get();
-      assertTrue(menu.open);
-      options = Array.from(
-          menu.querySelectorAll<HTMLButtonElement>('.dropdown-item'));
-    });
+    const currentHighlight =
+        app.$.container.querySelector('.current-read-highlight');
+    assertTrue(currentHighlight !== undefined);
+    assertEquals(currentHighlight!.textContent!, 'This is a ');
+  });
 
-    test('with word highlighting on, word is highlighted', async () => {
-      options[1]!.click();
-      await microtasksFinished();
-      assertEquals(
-          chrome.readingMode.highlightGranularity,
-          chrome.readingMode.wordHighlighting);
+  test('with sentence highlighting on, sentence is highlighted', () => {
+    chrome.readingMode.onHighlightGranularityChanged(
+        chrome.readingMode.sentenceHighlighting);
 
-      wordBoundaries.updateBoundary(0);
-      app.playSpeech();
-      const currentHighlight =
-          app.$.container.querySelector('.current-read-highlight');
-      assertTrue(currentHighlight !== undefined);
-      assertEquals(currentHighlight!.textContent!, 'This ');
+    wordBoundaries.updateBoundary(0);
+    app.playSpeech();
 
-      assertEquals(2, await metrics.whenCalled('recordHighlightGranularity'));
-      assertEquals(1, metrics.getCallCount('recordHighlightGranularity'));
-    });
+    const currentHighlight =
+        app.$.container.querySelector('.current-read-highlight');
+    assertTrue(currentHighlight !== undefined);
+    assertEquals(currentHighlight!.textContent!, 'This is a link.');
+  });
 
-    test('with phrase highlighting on, phrase is highlighted', async () => {
-      options[2]!.click();
-      await microtasksFinished();
+  test('with highlighting off, sentence is highlighted', () => {
+    chrome.readingMode.onHighlightGranularityChanged(
+        chrome.readingMode.noHighlighting);
 
-      assertEquals(
-          chrome.readingMode.highlightGranularity,
-          chrome.readingMode.phraseHighlighting);
+    wordBoundaries.updateBoundary(0);
+    app.playSpeech();
 
-      wordBoundaries.updateBoundary(0);
-      app.playSpeech();
-      const currentHighlight =
-          app.$.container.querySelector('.current-read-highlight');
-      assertTrue(currentHighlight !== undefined);
-      assertEquals(currentHighlight!.textContent!, 'This is a ');
-      assertEquals(3, await metrics.whenCalled('recordHighlightGranularity'));
-      assertEquals(1, metrics.getCallCount('recordHighlightGranularity'));
-    });
-
-    test('with sentence highlighting on, sentence is highlighted', async () => {
-      options[3]!.click();
-      await microtasksFinished();
-      assertEquals(
-          chrome.readingMode.highlightGranularity,
-          chrome.readingMode.sentenceHighlighting);
-
-      wordBoundaries.updateBoundary(0);
-      app.playSpeech();
-      const currentHighlight =
-          app.$.container.querySelector('.current-read-highlight');
-      assertTrue(currentHighlight !== undefined);
-      assertEquals(currentHighlight!.textContent!, 'This is a link.');
-      assertEquals(4, await metrics.whenCalled('recordHighlightGranularity'));
-      assertEquals(1, metrics.getCallCount('recordHighlightGranularity'));
-    });
-
-    test('with highlighting off, highlight is invisible', async () => {
-      options[4]!.click();
-      await microtasksFinished();
-      assertEquals(
-          chrome.readingMode.highlightGranularity,
-          chrome.readingMode.noHighlighting);
-
-      wordBoundaries.updateBoundary(0);
-      app.playSpeech();
-      const currentHighlight =
-          app.$.container.querySelector('.current-read-highlight');
-      assertTrue(currentHighlight !== undefined);
-      assertEquals('transparent', computeStyle('--current-highlight-bg-color'));
-      assertEquals(1, await metrics.whenCalled('recordHighlightGranularity'));
-      assertEquals(1, metrics.getCallCount('recordHighlightGranularity'));
-    });
+    const currentHighlight =
+        app.$.container.querySelector('.current-read-highlight');
+    assertTrue(currentHighlight !== undefined);
+    assertEquals(currentHighlight!.textContent!, 'This is a link.');
   });
 
   suite('after a word boundary', () => {
@@ -212,40 +162,6 @@
       assertEquals(currentHighlight!.textContent!, 'link.');
     });
 
-    // Tests for checking correct handling of auto granularity.
-    test(
-        'with auto highlighting and rate of 2, sentence highlight used', () => {
-          chrome.readingMode.onHighlightGranularityChanged(
-              chrome.readingMode.sentenceHighlighting);
-          app.playSpeech();
-          const currentHighlight =
-              app.$.container.querySelector('.current-read-highlight');
-          assertTrue(currentHighlight !== undefined);
-          assertEquals('This is a link.', currentHighlight!.textContent);
-        });
-
-    test('with auto highlighting and rate of 1, phrase highlight used', () => {
-      chrome.readingMode.onHighlightGranularityChanged(
-          chrome.readingMode.autoHighlighting);
-      chrome.readingMode.onSpeechRateChange(1);
-      app.playSpeech();
-      const currentHighlight =
-          app.$.container.querySelector('.current-read-highlight');
-      assertTrue(currentHighlight !== undefined);
-      assertEquals('This is a ', currentHighlight!.textContent);
-    });
-
-    test('with auto highlighting and rate of 0.5, word highlight used', () => {
-      chrome.readingMode.onHighlightGranularityChanged(
-          chrome.readingMode.autoHighlighting);
-      chrome.readingMode.onSpeechRateChange(0.5);
-      app.playSpeech();
-      const currentHighlight =
-          app.$.container.querySelector('.current-read-highlight');
-      assertTrue(currentHighlight !== undefined);
-      assertEquals('This ', currentHighlight!.textContent);
-    });
-
     // TODO(b/364327601): Add tests for unsupported language handling.
   });
 });
diff --git a/chrome/test/data/webui/side_panel/read_anything/prefs_test.ts b/chrome/test/data/webui/side_panel/read_anything/prefs_test.ts
deleted file mode 100644
index 88849f8..0000000
--- a/chrome/test/data/webui/side_panel/read_anything/prefs_test.ts
+++ /dev/null
@@ -1,236 +0,0 @@
-// Copyright 2024 The Chromium Authors
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-import {BrowserProxy, SpeechBrowserProxyImpl, ToolbarEvent, VoicePackController} from 'chrome-untrusted://read-anything-side-panel.top-chrome/read_anything.js';
-import type {AppElement} from 'chrome-untrusted://read-anything-side-panel.top-chrome/read_anything.js';
-import {assertArrayEquals, assertEquals, assertFalse, assertTrue} from 'chrome-untrusted://webui-test/chai_assert.js';
-
-import {createAndSetVoices, createApp, createSpeechSynthesisVoice, emitEvent, setupBasicSpeech, setVoices} from './common.js';
-import {FakeReadingMode} from './fake_reading_mode.js';
-import {TestColorUpdaterBrowserProxy} from './test_color_updater_browser_proxy.js';
-import {TestSpeechBrowserProxy} from './test_speech_browser_proxy.js';
-
-// TODO: b/40927698 - Add more tests.
-suite('PrefsTest', () => {
-  let app: AppElement;
-  let speech: TestSpeechBrowserProxy;
-  let voicePackController: VoicePackController;
-
-  setup(async () => {
-    // Clearing the DOM should always be done first.
-    document.body.innerHTML = window.trustedTypes!.emptyHTML;
-    BrowserProxy.setInstance(new TestColorUpdaterBrowserProxy());
-    const readingMode = new FakeReadingMode();
-    chrome.readingMode = readingMode as unknown as typeof chrome.readingMode;
-    chrome.readingMode.isReadAloudEnabled = true;
-    speech = new TestSpeechBrowserProxy();
-    SpeechBrowserProxyImpl.setInstance(speech);
-    voicePackController = new VoicePackController();
-    VoicePackController.setInstance(voicePackController);
-    app = await createApp();
-  });
-
-
-  suite('on restore settings from prefs', () => {
-    setup(() => {
-      // We are not testing the toolbar for this suite.
-      app.$.toolbar.restoreSettingsFromPrefs = () => {};
-    });
-
-    test('removes unavailable languages from prefs', () => {
-      const previouslyAvailableLang = 'pt-pt';
-      chrome.readingMode.onLanguagePrefChange(previouslyAvailableLang, true);
-      setupBasicSpeech(speech);
-
-      app.restoreSettingsFromPrefs();
-
-      assertFalse(voicePackController.isLangEnabled(previouslyAvailableLang));
-      assertFalse(chrome.readingMode.getLanguagesEnabledInPref().includes(
-          previouslyAvailableLang));
-    });
-
-    test('adds initially populated languages to prefs', () => {
-      const previouslyAvailableLang = 'pt-pt';
-      const availableLang = 'pt-br';
-      chrome.readingMode.onLanguagePrefChange(previouslyAvailableLang, true);
-      createAndSetVoices(speech, [
-        {lang: availableLang, name: 'Google Galinda'},
-      ]);
-
-      app.restoreSettingsFromPrefs();
-
-      assertFalse(voicePackController.isLangEnabled(previouslyAvailableLang));
-      assertFalse(chrome.readingMode.getLanguagesEnabledInPref().includes(
-          previouslyAvailableLang));
-      assertTrue(voicePackController.isLangEnabled(availableLang));
-      assertTrue(chrome.readingMode.getLanguagesEnabledInPref().includes(
-          availableLang));
-    });
-
-    // <if expr="not is_chromeos">
-    test('adds unavailable language to prefs once available', () => {
-      const previouslyAvailableLang = 'da-dk';
-      chrome.readingMode.onLanguagePrefChange(previouslyAvailableLang, true);
-      createAndSetVoices(speech, [
-        {lang: 'en-us', name: 'Google Fiyero'},
-      ]);
-
-      app.restoreSettingsFromPrefs();
-
-      assertFalse(voicePackController.isLangEnabled(previouslyAvailableLang));
-      assertFalse(chrome.readingMode.getLanguagesEnabledInPref().includes(
-          previouslyAvailableLang));
-
-      // The previously unavailable language is now available.
-      createAndSetVoices(speech, [
-        {lang: 'en-us', name: 'Google Fiyero'},
-        {lang: 'da-dk', name: 'Doctor Dillamond'},
-      ]);
-
-      assertTrue(voicePackController.isLangEnabled(previouslyAvailableLang));
-      assertTrue(chrome.readingMode.getLanguagesEnabledInPref().includes(
-          previouslyAvailableLang));
-    });
-    // </if>
-
-    suite('with no initial voices', () => {
-      setup(() => {
-        chrome.readingMode.baseLanguageForSpeech = 'en';
-
-        // Set synthesis to have no available voices
-        setVoices(speech, []);
-        voicePackController.setCurrentVoice(null);
-      });
-
-      test('with no settings, voice selected in onVoicesChanged', () => {
-        chrome.readingMode.getStoredVoice = () => '';
-
-        // When there's no voices available, there shouldn't be a speech
-        // synthesis voice selected.
-        app.restoreSettingsFromPrefs();
-        assertFalse(!!voicePackController.getCurrentVoice());
-
-        // Update the speech synthesis engine with voices.
-        setupBasicSpeech(speech);
-
-        // Once voices are available, settings should be restored.
-        assertTrue(!!voicePackController.getCurrentVoice());
-      });
-
-      test('with no settings, dfferent language voice selected', () => {
-        chrome.readingMode.getStoredVoice = () => '';
-
-        // When there's no voices available, there shouldn't be a speech
-        // synthesis voice selected.
-        app.restoreSettingsFromPrefs();
-        assertFalse(!!voicePackController.getCurrentVoice());
-
-        // Update the speech synthesis engine with voices.
-        setupBasicSpeech(speech);
-
-        // Once voices are available, settings should be restored.
-        assertTrue(!!voicePackController.getCurrentVoice());
-      });
-
-      test(
-          'with no initial voices and previously selected voice, correct ' +
-              'voice selected after onVoicesChanged',
-          () => {
-            chrome.readingMode.getStoredVoice = () => 'Google Kristi';
-
-            // When there's no voices available, there shouldn't be a speech
-            // synthesis voice selected.
-            app.restoreSettingsFromPrefs();
-            assertFalse(!!voicePackController.getCurrentVoice());
-
-            // Update the speech synthesis engine with voices.
-            createAndSetVoices(speech, [
-              {lang: 'en', name: 'Google Lauren'},
-              {lang: 'en', name: 'Google Eitan'},
-              {lang: 'en-uk', name: 'Google Kristi'},
-            ]);
-
-            // Once voices are available, settings should be restored.
-            const selectedVoice = voicePackController.getCurrentVoice();
-            assertTrue(!!selectedVoice);
-            assertEquals('Google Kristi', selectedVoice.name);
-          });
-
-      test(
-          'onVoicesChanged after settings restored, settings aren\'t updated',
-          () => {
-            chrome.readingMode.getStoredVoice = () => 'Google Shari';
-
-            // When there's no voices available, there shouldn't be a speech
-            // synthesis voice selected.
-            app.restoreSettingsFromPrefs();
-            assertFalse(!!voicePackController.getCurrentVoice());
-
-            const futureSelectedVoice =
-                createSpeechSynthesisVoice({lang: 'en', name: 'Google Kristi'});
-
-            // Update the speech synthesis engine with voices.
-            setVoices(speech, [
-              createSpeechSynthesisVoice({lang: 'en', name: 'Google Lauren'}),
-              createSpeechSynthesisVoice({lang: 'en', name: 'Google Shari'}),
-              futureSelectedVoice,
-            ]);
-
-            // Once voices are available, settings should be restored.
-            let selectedVoice = voicePackController.getCurrentVoice();
-            assertTrue(!!selectedVoice);
-            assertEquals('Google Shari', selectedVoice.name);
-
-            emitEvent(
-                app, ToolbarEvent.VOICE,
-                {detail: {selectedVoice: futureSelectedVoice}});
-            selectedVoice = voicePackController.getCurrentVoice();
-            assertTrue(!!selectedVoice);
-            assertEquals('Google Kristi', selectedVoice.name);
-
-            // We have to update the stored voice so onVoicesChanged recognizes
-            // a user chosen voice.
-            chrome.readingMode.getStoredVoice = () => 'Google Kristi';
-            voicePackController.onVoicesChanged();
-
-            // After onVoicesChanged, the most recently selected voice should
-            // be used.
-            selectedVoice = voicePackController.getCurrentVoice();
-            assertTrue(!!selectedVoice);
-            assertEquals('Google Kristi', selectedVoice.name);
-          });
-    });
-
-    suite('populates enabled languages', () => {
-      const langs = ['si', 'km', 'th'];
-      const locales = ['si-lk', 'km-kh', 'th-th'];
-
-      setup(() => {
-        createAndSetVoices(speech, [
-          {lang: langs[0], name: 'Google Frodo'},
-          {lang: langs[1], name: 'Google Merry'},
-          {lang: langs[2], name: 'Google Pippin'},
-        ]);
-      });
-
-      test('with langs stored in prefs', () => {
-        chrome.readingMode.getLanguagesEnabledInPref = () => langs;
-
-        app.restoreSettingsFromPrefs();
-
-        assertArrayEquals(
-            langs.concat(locales), voicePackController.getEnabledLangs());
-      });
-
-      test('with browser lang', () => {
-        chrome.readingMode.baseLanguageForSpeech = langs[1]!;
-
-        app.restoreSettingsFromPrefs();
-
-        assertArrayEquals(
-            [langs[1], locales[1]], voicePackController.getEnabledLangs());
-      });
-    });
-  });
-});
diff --git a/chrome/test/data/webui/side_panel/read_anything/read_anything_browsertest.cc b/chrome/test/data/webui/side_panel/read_anything/read_anything_browsertest.cc
index aac41f6..afa8f52 100644
--- a/chrome/test/data/webui/side_panel/read_anything/read_anything_browsertest.cc
+++ b/chrome/test/data/webui/side_panel/read_anything/read_anything_browsertest.cc
@@ -191,10 +191,6 @@
                    "mocha.run()");
 }
 
-IN_PROC_BROWSER_TEST_F(ReadAnythingMochaTest, Prefs) {
-  RunSidePanelTest("side_panel/read_anything/prefs_test.js", "mocha.run()");
-}
-
 #if BUILDFLAG(IS_CHROMEOS)
 IN_PROC_BROWSER_TEST_F(ReadAnythingMochaTest, DownloadNotification) {
   RunSidePanelTest("side_panel/read_anything/download_notification_test.js",
diff --git a/chrome/test/data/webui/side_panel/read_anything/speech_controller_test.ts b/chrome/test/data/webui/side_panel/read_anything/speech_controller_test.ts
index b2223ca1..b9dbade4b 100644
--- a/chrome/test/data/webui/side_panel/read_anything/speech_controller_test.ts
+++ b/chrome/test/data/webui/side_panel/read_anything/speech_controller_test.ts
@@ -358,6 +358,7 @@
         chrome.readingMode.engineInterruptStopSource,
         await metrics.whenCalled('recordSpeechStopSource'));
   });
+
   test('onSpeechFinished', () => {
     speechController.onPlayPauseToggle(null, 'New phone who dis?');
 
@@ -368,4 +369,44 @@
     assertEquals(1, metrics.getCallCount('recordSpeechStopSource'));
     assertFalse(speechController.isSpeechActive());
   });
+
+  test('playNextGranularity propagates change', () => {
+    let movedToNext = false;
+    chrome.readingMode.getCurrentText = () => [];
+    chrome.readingMode.movePositionToNextGranularity = () => {
+      movedToNext = true;
+    };
+
+    speechController.playNextGranularity();
+
+    assertTrue(movedToNext);
+  });
+
+  test('playPreviousGranularity propagates change', () => {
+    let movedToPrevious: boolean = false;
+    chrome.readingMode.getCurrentText = () => [];
+    chrome.readingMode.movePositionToPreviousGranularity = () => {
+      movedToPrevious = true;
+    };
+
+    speechController.playPreviousGranularity();
+
+    assertTrue(movedToPrevious);
+  });
+
+  test('onHighlightGranularityChange', async () => {
+    const granularity1 = chrome.readingMode.noHighlighting;
+    const granularity2 = chrome.readingMode.wordHighlighting;
+
+    speechController.onHighlightGranularityChange(granularity1);
+    assertEquals(granularity1, chrome.readingMode.highlightGranularity);
+    assertEquals(
+        granularity1, await metrics.whenCalled('recordHighlightGranularity'));
+
+    metrics.reset();
+    speechController.onHighlightGranularityChange(granularity2);
+    assertEquals(granularity2, chrome.readingMode.highlightGranularity);
+    assertEquals(
+        granularity2, await metrics.whenCalled('recordHighlightGranularity'));
+  });
 });
diff --git a/chrome/test/data/webui/side_panel/read_anything/voice_pack_controller_test.ts b/chrome/test/data/webui/side_panel/read_anything/voice_pack_controller_test.ts
index 33b52db6..a54fea4 100644
--- a/chrome/test/data/webui/side_panel/read_anything/voice_pack_controller_test.ts
+++ b/chrome/test/data/webui/side_panel/read_anything/voice_pack_controller_test.ts
@@ -4,12 +4,12 @@
 
 import 'chrome-untrusted://read-anything-side-panel.top-chrome/read_anything.js';
 
-import {BrowserProxy, EXTENSION_RESPONSE_TIMEOUT_MS, mojoVoicePackStatusToVoicePackStatusEnum, NotificationType, SpeechBrowserProxyImpl, VoiceClientSideStatusCode, VoiceNotificationManager, VoicePackController, VoicePackServerStatusErrorCode, VoicePackServerStatusSuccessCode} from 'chrome-untrusted://read-anything-side-panel.top-chrome/read_anything.js';
+import {AVAILABLE_GOOGLE_TTS_LOCALES, BrowserProxy, EXTENSION_RESPONSE_TIMEOUT_MS, mojoVoicePackStatusToVoicePackStatusEnum, NotificationType, PACK_MANAGER_SUPPORTED_LANGS_AND_LOCALES, SpeechBrowserProxyImpl, VoiceClientSideStatusCode, VoiceNotificationManager, VoicePackController, VoicePackServerStatusErrorCode, VoicePackServerStatusSuccessCode} from 'chrome-untrusted://read-anything-side-panel.top-chrome/read_anything.js';
 import type {VoiceLanguageListener, VoiceNotificationListener} from 'chrome-untrusted://read-anything-side-panel.top-chrome/read_anything.js';
 import {assertArrayEquals, assertEquals, assertFalse, assertTrue} from 'chrome-untrusted://webui-test/chai_assert.js';
 import {MockTimer} from 'chrome-untrusted://webui-test/mock_timer.js';
 
-import {createAndSetVoices, createSpeechSynthesisVoice, setVoices} from './common.js';
+import {createAndSetVoices, createSpeechSynthesisVoice, setupBasicSpeech, setVoices} from './common.js';
 import {FakeReadingMode} from './fake_reading_mode.js';
 import {TestColorUpdaterBrowserProxy} from './test_color_updater_browser_proxy.js';
 import {TestSpeechBrowserProxy} from './test_speech_browser_proxy.js';
@@ -26,6 +26,42 @@
   let requestInfoLangs: string[];
   let notificationType: NotificationType|null;
 
+  const langForDefaultVoice = 'en';
+  const lang1 = 'zh';
+  const lang2 = 'tr';
+  const lang3 = 'pt-br';
+  const langWithNoVoices = 'elvish';
+
+  const defaultVoice = createSpeechSynthesisVoice({
+    lang: langForDefaultVoice,
+    name: 'Google Penny',
+    default: true,
+  });
+  const firstVoiceWithLang1 =
+      createSpeechSynthesisVoice({lang: lang1, name: 'Google Nickel'});
+  const defaultVoiceWithLang1 = createSpeechSynthesisVoice(
+      {lang: lang1, name: 'Google Dime', default: true});
+  const firstVoiceWithLang2 =
+      createSpeechSynthesisVoice({lang: lang2, name: 'Google Quarter'});
+  const secondVoiceWithLang2 =
+      createSpeechSynthesisVoice({lang: lang2, name: 'Google Dollar'});
+  const firstVoiceWithLang3 =
+      createSpeechSynthesisVoice({lang: lang3, name: 'Google Penny'});
+  const naturalVoiceWithLang3 =
+      createSpeechSynthesisVoice({lang: lang3, name: 'Google Penny (Natural)'});
+  const otherVoice =
+      createSpeechSynthesisVoice({lang: 'it', name: 'Google Bill'});
+  const voices = [
+    defaultVoice,
+    firstVoiceWithLang1,
+    defaultVoiceWithLang1,
+    otherVoice,
+    firstVoiceWithLang2,
+    secondVoiceWithLang2,
+    firstVoiceWithLang3,
+    naturalVoiceWithLang3,
+  ];
+
   setup(() => {
     // Clearing the DOM should always be done first.
     document.body.innerHTML = window.trustedTypes!.emptyHTML;
@@ -182,124 +218,116 @@
     assertEquals(voice.lang, sentLang);
   });
 
-  suite('restoreFromPrefs', () => {
-    const langForDefaultVoice = 'en';
-    const lang1 = 'zh';
-    const lang2 = 'tr';
-    const langWithNoVoices = 'elvish';
+  test('restoreFromPrefs removes unavailable languages from prefs', () => {
+    const previouslyAvailableLang = 'pt-pt';
+    chrome.readingMode.onLanguagePrefChange(previouslyAvailableLang, true);
+    setupBasicSpeech(speech);
 
-    const defaultVoice = createSpeechSynthesisVoice({
-      lang: langForDefaultVoice,
-      name: 'Google Kristi',
-      default: true,
-    });
-    const firstVoiceWithLang1 =
-        createSpeechSynthesisVoice({lang: lang1, name: 'Google Monkey'});
-    const defaultVoiceWithLang1 = createSpeechSynthesisVoice({
-      lang: lang1,
-      name: 'Google Llama',
-      default: true,
-    });
-    const firstVoiceWithLang2 =
-        createSpeechSynthesisVoice({lang: lang2, name: 'Google Parrot'});
-    const secondVoiceWithLang2 =
-        createSpeechSynthesisVoice({lang: lang2, name: 'Google Panda'});
-    const otherVoice =
-        createSpeechSynthesisVoice({lang: 'it', name: 'Google Elephant'});
-    const voices = [
-      defaultVoice,
-      firstVoiceWithLang1,
-      defaultVoiceWithLang1,
-      otherVoice,
-      firstVoiceWithLang2,
-      secondVoiceWithLang2,
-    ];
+    voicePackController.restoreFromPrefs();
+
+    assertArrayEquals([], chrome.readingMode.getLanguagesEnabledInPref());
+  });
+
+  test('restoreFromPrefs adds initially populated languages to prefs', () => {
+    const previouslyAvailableLang = 'pt-pt';
+    const availableLang = 'pt-br';
+    chrome.readingMode.onLanguagePrefChange(previouslyAvailableLang, true);
+    voicePackController.enableLang(availableLang);
+    createAndSetVoices(speech, [
+      {lang: availableLang, name: 'Google Galinda'},
+    ]);
+
+    voicePackController.restoreFromPrefs();
+
+    assertArrayEquals(
+        [availableLang], chrome.readingMode.getLanguagesEnabledInPref());
+  });
+
+  // <if expr="not is_chromeos">
+  test(
+      'restoreFromPrefs adds unavailable language to prefs once available',
+      () => {
+        const previouslyAvailableLang = 'da-dk';
+        chrome.readingMode.onLanguagePrefChange(previouslyAvailableLang, true);
+        createAndSetVoices(speech, [
+          {lang: 'en-us', name: 'Google Fiyero'},
+        ]);
+        voicePackController.restoreFromPrefs();
+
+        assertArrayEquals([], chrome.readingMode.getLanguagesEnabledInPref());
+
+        // The previously unavailable language is now available.
+        voicePackController.enableLang(previouslyAvailableLang);
+        createAndSetVoices(speech, [
+          {lang: 'en-us', name: 'Google Fiyero'},
+          {lang: 'da-dk', name: 'Doctor Dillamond'},
+        ]);
+        voicePackController.restoreFromPrefs();
+
+        assertArrayEquals(
+            [previouslyAvailableLang],
+            chrome.readingMode.getLanguagesEnabledInPref());
+      });
+  // </if>
+
+  suite('restoreFromPrefs populates enabled languages', () => {
+    const langs = ['si', 'km', 'th'];
+    const locales = ['si-lk', 'km-kh', 'th-th'];
 
     setup(() => {
-      speech.setVoices(voices);
-    });
-
-    test('enables stored languages', () => {
-      const lang1 = 'en-gb';
-      const lang2 = 'fr';
-      const lang3 = 'bd';
-      chrome.readingMode.onLanguagePrefChange(lang1, true);
-      chrome.readingMode.onLanguagePrefChange(lang2, true);
-      chrome.readingMode.onLanguagePrefChange(lang3, true);
-      chrome.readingMode.baseLanguageForSpeech = 'en';
-      speech.setVoices([
-        createSpeechSynthesisVoice({lang: lang1, name: 'Henry'}),
-        createSpeechSynthesisVoice({lang: lang2, name: 'Google Thomas'}),
-        createSpeechSynthesisVoice({lang: lang3, name: 'Google Matt'}),
+      createAndSetVoices(speech, [
+        {lang: langs[0], name: 'Google Frodo'},
+        {lang: langs[1], name: 'Google Merry'},
+        {lang: langs[2], name: 'Google Pippin'},
       ]);
+    });
+
+    test('with langs stored in prefs', () => {
+      chrome.readingMode.getLanguagesEnabledInPref = () => langs;
 
       voicePackController.restoreFromPrefs();
 
-      assertArrayEquals(
-          [lang1, lang2, lang3], voicePackController.getEnabledLangs());
-      assertEquals('en', voicePackController.getCurrentLanguage());
-      assertTrue(voicePackController.isLangEnabled(lang1));
-      assertTrue(voicePackController.isLangEnabled(lang2));
-      assertTrue(voicePackController.isLangEnabled(lang3));
       assertTrue(onEnabledLangsChange);
+      assertArrayEquals(
+          langs.concat(locales), voicePackController.getEnabledLangs());
     });
 
-    test('enables the lang for the preferred voice', () => {
-      chrome.readingMode.getStoredVoice = () => otherVoice.name;
-      voicePackController.restoreFromPrefs();
-      assertTrue(voicePackController.isLangEnabled(otherVoice.lang));
-    });
-
-    test('uses the stored voice for this language if there is one', () => {
-      chrome.readingMode.getStoredVoice = () => otherVoice.name;
+    test('with browser lang', () => {
+      chrome.readingMode.baseLanguageForSpeech = langs[1]!;
 
       voicePackController.restoreFromPrefs();
 
-      assertTrue(onCurrentVoiceChange);
-      assertEquals(otherVoice, voicePackController.getCurrentVoice());
+      assertTrue(onEnabledLangsChange);
+      assertArrayEquals(
+          [langs[1], locales[1]], voicePackController.getEnabledLangs());
     });
+  });
 
-    test('uses the default voice if the stored voice is invalid', () => {
-      chrome.readingMode.getStoredVoice = () => 'Matt';
-      voicePackController.enableLang(langForDefaultVoice);
+  test('restoreFromPrefs enables the lang for the preferred voice', () => {
+    speech.setVoices(voices);
+    chrome.readingMode.getStoredVoice = () => otherVoice.name;
 
-      voicePackController.restoreFromPrefs();
+    voicePackController.restoreFromPrefs();
 
-      assertTrue(onCurrentVoiceChange);
-      assertEquals(defaultVoice, voicePackController.getCurrentVoice());
-    });
+    assertTrue(voicePackController.isLangEnabled(otherVoice.lang));
+  });
 
-    suite('when there is no stored voice for this language', () => {
-      setup(() => {
-        chrome.readingMode.getStoredVoice = () => '';
-      });
+  test('restoreFromPrefs uses the stored voice for this language', () => {
+    speech.setVoices(voices);
+    chrome.readingMode.getStoredVoice = () => otherVoice.name;
 
-      test('uses the default voice for this language', () => {
-        voicePackController.enableLang(lang1);
-        voicePackController.setCurrentLanguage(lang1);
+    voicePackController.restoreFromPrefs();
 
-        voicePackController.restoreFromPrefs();
+    assertTrue(onCurrentVoiceChange);
+    assertEquals(otherVoice, voicePackController.getCurrentVoice());
+  });
 
-        assertTrue(onCurrentVoiceChange);
-        assertEquals(
-            defaultVoiceWithLang1, voicePackController.getCurrentVoice());
-      });
-
-      test('uses current voice if there\'s none for this language', () => {
-        voicePackController.setCurrentLanguage(langWithNoVoices);
-        voicePackController.setCurrentVoice(otherVoice);
-        voicePackController.enableLang(otherVoice.lang);
-
-        voicePackController.restoreFromPrefs();
-
-        assertTrue(onCurrentVoiceChange);
-        assertEquals(otherVoice, voicePackController.getCurrentVoice());
-      });
-
-      test('uses the device default if there\'s no current voice', () => {
-        voicePackController.setCurrentLanguage(langWithNoVoices);
+  test(
+      'restoreFromPrefs uses the default voice if the stored voice is invalid',
+      () => {
+        speech.setVoices(voices);
+        chrome.readingMode.getStoredVoice = () => 'Matt';
         voicePackController.enableLang(langForDefaultVoice);
-        voicePackController.enableLang(otherVoice.lang);
 
         voicePackController.restoreFromPrefs();
 
@@ -307,19 +335,27 @@
         assertEquals(defaultVoice, voicePackController.getCurrentVoice());
       });
 
-      test(
-          'uses the first voice for this language if there\'s no default',
-          () => {
-            voicePackController.enableLang(lang2);
-            voicePackController.setCurrentLanguage(lang2);
+  test('restoreFromPrefs installs exactly matching enabled langs', () => {
+    const lang1 = 'km';
+    const lang1Exact = 'km-kh';
+    const lang2 = 'de';
+    const lang3 = 'cs';
+    voicePackController.enableLang(lang1Exact);
+    voicePackController.enableLang(lang2);
+    voicePackController.enableLang(lang3);
 
-            voicePackController.restoreFromPrefs();
+    voicePackController.restoreFromPrefs();
+    assertArrayEquals([lang1], requestInfoLangs);
 
-            assertTrue(onCurrentVoiceChange);
-            assertEquals(
-                firstVoiceWithLang2, voicePackController.getCurrentVoice());
-          });
-    });
+    voicePackController.updateVoicePackStatus(lang1, 'kNotInstalled');
+    voicePackController.updateVoicePackStatus(lang2, 'kNotInstalled');
+    voicePackController.updateVoicePackStatus(lang3, 'kNotInstalled');
+    assertEquals(
+        VoiceClientSideStatusCode.SENT_INSTALL_REQUEST,
+        voicePackController.getLocalStatus(lang1));
+    assertFalse(!!voicePackController.getLocalStatus(lang2));
+    assertFalse(!!voicePackController.getLocalStatus(lang3));
+    assertArrayEquals([lang1], installedLangs);
   });
 
   test('onLanguageToggle enabled languages are added', () => {
@@ -407,22 +443,21 @@
         assertArrayEquals([], installedLangs);
       });
 
-  test(
-      'onVoicesChanged with auto selected voice, switches to a Natural voice',
-      () => {
-        chrome.readingMode.getStoredVoice = () => '';
-        const voice =
-            createSpeechSynthesisVoice({lang: 'ja', name: 'Google Eagle'});
-        const naturalVoice = createSpeechSynthesisVoice(
-            {lang: 'ja', name: 'Google Horse (Natural)'});
-        speech.setVoices([voice, naturalVoice]);
-        voicePackController.setCurrentVoice(voice);
-        voicePackController.setCurrentLanguage(voice.lang);
+  test('onVoicesChanged with auto selected voice, uses a Natural voice', () => {
+    chrome.readingMode.getStoredVoice = () => '';
+    const voice =
+        createSpeechSynthesisVoice({lang: 'ja', name: 'Google Eagle'});
+    const naturalVoice = createSpeechSynthesisVoice(
+        {lang: 'ja', name: 'Google Horse (Natural)'});
+    speech.setVoices([voice, naturalVoice]);
+    voicePackController.setCurrentVoice(voice);
+    voicePackController.setCurrentLanguage(voice.lang);
 
-        voicePackController.onVoicesChanged();
+    voicePackController.onVoicesChanged();
 
-        assertEquals(naturalVoice, voicePackController.getCurrentVoice());
-      });
+    assertTrue(onAvailableVoicesChange);
+    assertEquals(naturalVoice, voicePackController.getCurrentVoice());
+  });
 
   test(
       'onVoicesChanged with a user selected voice, does not switch to a ' +
@@ -439,40 +474,10 @@
 
         voicePackController.onVoicesChanged();
 
+        assertTrue(onAvailableVoicesChange);
         assertEquals(voice, voicePackController.getCurrentVoice());
       });
 
-  test('refreshAvailableVoices', () => {
-    const voices = [
-      createSpeechSynthesisVoice({lang: 'en', name: 'Google Henry'}),
-      createSpeechSynthesisVoice({lang: 'en', name: 'Google Thomas'}),
-      createSpeechSynthesisVoice({lang: 'en', name: 'Google Matt'}),
-    ];
-    speech.setVoices(voices);
-
-    assertFalse(onAvailableVoicesChange);
-    assertArrayEquals([], voicePackController.getAvailableVoices());
-
-    voicePackController.refreshAvailableVoices();
-    assertTrue(onAvailableVoicesChange);
-    assertArrayEquals(voices, voicePackController.getAvailableVoices());
-
-    // If we already have voices and new voices come in, we only get those
-    // voices when we force a refresh.
-    onAvailableVoicesChange = false;
-    const newVoices = voices.concat(
-        createSpeechSynthesisVoice({lang: 'it', name: 'Google Charles'}));
-    speech.setVoices(newVoices);
-
-    voicePackController.refreshAvailableVoices();
-    assertFalse(onAvailableVoicesChange);
-    assertArrayEquals(voices, voicePackController.getAvailableVoices());
-
-    voicePackController.refreshAvailableVoices(true);
-    assertTrue(onAvailableVoicesChange);
-    assertArrayEquals(newVoices, voicePackController.getAvailableVoices());
-  });
-
   // <if expr="not is_chromeos">
   test('onVoicesChanged enables newly available langs', () => {
     const lang1 = 'en-gb';
@@ -485,7 +490,6 @@
     speech.setVoices([
       createSpeechSynthesisVoice({lang: lang1, name: 'Henry'}),
     ]);
-    voicePackController.refreshAvailableVoices();
     voicePackController.restoreFromPrefs();
     assertArrayEquals([lang1], voicePackController.getEnabledLangs());
     assertArrayEquals([], chrome.readingMode.getLanguagesEnabledInPref());
@@ -523,6 +527,7 @@
     voicePackController.onVoicesChanged();
 
     assertArrayEquals(['bn', 'hu'], installedLangs);
+    assertTrue(onAvailableVoicesChange);
     assertFalse(voicePackController.hasAvailableVoices());
   });
 
@@ -536,6 +541,7 @@
     voicePackController.onVoicesChanged();
 
     assertTrue(voicePackController.isLangEnabled(lang));
+    assertTrue(onAvailableVoicesChange);
     assertArrayEquals([voice], voicePackController.getAvailableVoices());
   });
 
@@ -552,6 +558,7 @@
 
     voicePackController.onVoicesChanged();
 
+    assertTrue(onAvailableVoicesChange);
     assertArrayEquals([lang1, lang2, lang3], requestInfoLangs);
   });
 
@@ -573,7 +580,8 @@
       'onVoicesChanged does nothing when current voice' +
           ' still available',
       () => {
-        const voice = createSpeechSynthesisVoice({lang: 'id', name: 'Dog'});
+        const voice =
+            createSpeechSynthesisVoice({lang: 'id', name: 'Google Dog'});
         speech.setVoices([voice]);
         voicePackController.enableLang(voice.lang);
         voicePackController.setCurrentVoice(voice);
@@ -582,12 +590,12 @@
         voicePackController.onVoicesChanged();
 
         assertFalse(onCurrentVoiceChange);
+        assertTrue(onAvailableVoicesChange);
         assertEquals(voice, voicePackController.getCurrentVoice());
       });
 
   test(
-      'onVoicesChanged gets default voice when current' +
-          ' voice unavailable',
+      'onVoicesChanged gets default voice when current voice unavailable',
       () => {
         const voice =
             createSpeechSynthesisVoice({lang: 'id', name: 'Google Cat'});
@@ -601,9 +609,27 @@
         voicePackController.onVoicesChanged();
 
         assertTrue(onCurrentVoiceChange);
+        assertTrue(onAvailableVoicesChange);
         assertEquals(defaultVoice, voicePackController.getCurrentVoice());
       });
 
+  test('onVoicesChanged gets stored voice', () => {
+    const voice1 =
+        createSpeechSynthesisVoice({lang: 'id', name: 'Google Tiger'});
+    const voice2 =
+        createSpeechSynthesisVoice({lang: 'id', name: 'Google Moose'});
+    speech.setVoices([voice1, voice2]);
+    chrome.readingMode.getStoredVoice = () => voice1.name;
+    voicePackController.enableLang(voice1.lang);
+    onCurrentVoiceChange = false;
+
+    voicePackController.onVoicesChanged();
+
+    assertTrue(onCurrentVoiceChange);
+    assertTrue(onAvailableVoicesChange);
+    assertEquals(voice1, voicePackController.getCurrentVoice());
+  });
+
   test('stopWaitingForSpeechExtension stops waiting for engine timeout', () => {
     const lang = 'fi';
     voicePackController.setServerStatus(
@@ -636,7 +662,22 @@
     assertEquals(lang, voicePackController.getCurrentLanguage());
   });
 
-  test('onPageLanguageChanged when not installed, requests info', () => {
+  test('onPageLanguageChanged installs lang if no status', () => {
+    const lang = 'en-gb';
+    voicePackController.enableLang(lang);
+
+    chrome.readingMode.baseLanguageForSpeech = lang;
+    voicePackController.onPageLanguageChanged();
+    assertArrayEquals([lang], requestInfoLangs);
+
+    voicePackController.updateVoicePackStatus(lang, 'kNotInstalled');
+    assertEquals(
+        VoiceClientSideStatusCode.SENT_INSTALL_REQUEST,
+        voicePackController.getLocalStatus(lang));
+    assertArrayEquals([lang], installedLangs);
+  });
+
+  test('onPageLanguageChanged installs lang if not installed', () => {
     const lang = 'ja';
     chrome.readingMode.baseLanguageForSpeech = lang;
     voicePackController.setServerStatus(
@@ -692,6 +733,202 @@
     assertArrayEquals([], installedLangs);
   });
 
+  test('onPageLanguageChanged doesn\'t install unsupported language', () => {
+    chrome.readingMode.baseLanguageForSpeech = 'zh';
+
+    voicePackController.onPageLanguageChanged();
+
+    // Use this check to ensure this stays updated if the supported
+    // languages changes.
+    assertFalse(PACK_MANAGER_SUPPORTED_LANGS_AND_LOCALES.has(
+        chrome.readingMode.baseLanguageForSpeech));
+    assertArrayEquals([], requestInfoLangs);
+  });
+
+  test('onPageLanguageChanged installs without exact match', () => {
+    const lang = 'bn';
+    chrome.readingMode.baseLanguageForSpeech = lang;
+
+    voicePackController.onPageLanguageChanged();
+
+    // Use these checks to ensure this stays updated if the supported
+    // languages changes.
+    assertTrue(PACK_MANAGER_SUPPORTED_LANGS_AND_LOCALES.has(lang));
+    assertFalse(AVAILABLE_GOOGLE_TTS_LOCALES.has(lang));
+    assertArrayEquals([lang], requestInfoLangs);
+  });
+
+  test('onPageLanguageChanged uses the stored voice for this language', () => {
+    speech.setVoices(voices);
+    chrome.readingMode.getStoredVoice = () => otherVoice.name;
+
+    voicePackController.onPageLanguageChanged();
+
+    assertTrue(onCurrentVoiceChange);
+    assertEquals(otherVoice, voicePackController.getCurrentVoice());
+  });
+
+  test(
+      'onPageLanguageChanged uses default voice if the stored voice is invalid',
+      () => {
+        speech.setVoices(voices);
+        chrome.readingMode.getStoredVoice = () => 'Matt';
+        voicePackController.enableLang(langForDefaultVoice);
+
+        voicePackController.onPageLanguageChanged();
+
+        assertTrue(onCurrentVoiceChange);
+        assertEquals(defaultVoice, voicePackController.getCurrentVoice());
+      });
+
+  suite('onPageLanguageChanged with no stored voice for this language', () => {
+    setup(() => {
+      chrome.readingMode.getStoredVoice = () => '';
+      speech.setVoices(voices);
+      voicePackController.setServerStatus(
+          lang1, mojoVoicePackStatusToVoicePackStatusEnum('kOther'));
+      voicePackController.setServerStatus(
+          lang2, mojoVoicePackStatusToVoicePackStatusEnum('kOther'));
+      voicePackController.setServerStatus(
+          lang3, mojoVoicePackStatusToVoicePackStatusEnum('kOther'));
+    });
+
+    suite('and no voices at all for this language', () => {
+      setup(() => {
+        chrome.readingMode.baseLanguageForSpeech = langWithNoVoices;
+      });
+
+      test('uses the current voice if there is one', () => {
+        voicePackController.setCurrentVoice(otherVoice);
+        voicePackController.onPageLanguageChanged();
+        assertEquals(otherVoice, voicePackController.getCurrentVoice());
+      });
+
+      test('uses a natural voice if there\'s no current voice', () => {
+        voicePackController.onPageLanguageChanged();
+        assertEquals(
+            naturalVoiceWithLang3, voicePackController.getCurrentVoice());
+      });
+
+      test('uses the device default if there\'s no natural', () => {
+        speech.setVoices(voices.filter(v => v !== naturalVoiceWithLang3));
+        voicePackController.onVoicesChanged();
+
+        voicePackController.onPageLanguageChanged();
+
+        assertEquals(
+            defaultVoice, voicePackController.getCurrentVoice(),
+            voicePackController.getCurrentVoice()?.name);
+      });
+    });
+
+    test('enables pack manager locale', () => {
+      chrome.readingMode.baseLanguageForSpeech = lang3;
+      voicePackController.setAvailableVoices([firstVoiceWithLang3]);
+      voicePackController.onVoicesChanged();
+
+      voicePackController.onPageLanguageChanged();
+
+      assertTrue(voicePackController.isLangEnabled(lang3));
+      assertEquals(
+          naturalVoiceWithLang3, voicePackController.getCurrentVoice());
+    });
+
+    test('enables other locale if not supported by pack manager', () => {
+      chrome.readingMode.baseLanguageForSpeech = lang1;
+      voicePackController.setAvailableVoices([firstVoiceWithLang1]);
+      voicePackController.onVoicesChanged();
+
+      voicePackController.onPageLanguageChanged();
+
+      assertTrue(voicePackController.isLangEnabled(lang1));
+      assertEquals(
+          defaultVoiceWithLang1, voicePackController.getCurrentVoice());
+    });
+
+    test('uses a natural voice for this language', () => {
+      chrome.readingMode.baseLanguageForSpeech = lang3;
+      voicePackController.enableLang(lang3);
+
+      voicePackController.onPageLanguageChanged();
+
+      assertEquals(
+          naturalVoiceWithLang3, voicePackController.getCurrentVoice());
+    });
+
+    test(
+        'uses the default voice for this language with no natural voice',
+        () => {
+          chrome.readingMode.baseLanguageForSpeech = lang1;
+          voicePackController.enableLang(lang1);
+
+          voicePackController.onPageLanguageChanged();
+
+          assertEquals(
+              defaultVoiceWithLang1, voicePackController.getCurrentVoice());
+        });
+
+    test(
+        'uses the first listed voice for this language if there\'s no default',
+        () => {
+          chrome.readingMode.baseLanguageForSpeech = lang2;
+          voicePackController.enableLang(lang2);
+
+          voicePackController.onPageLanguageChanged();
+
+          assertEquals(
+              firstVoiceWithLang2, voicePackController.getCurrentVoice());
+        });
+
+
+    test('uses a voice in a different locale but same language', () => {
+      chrome.readingMode.baseLanguageForSpeech = 'en-US';
+      voicePackController.enableLang('en-gb');
+      const voice = createSpeechSynthesisVoice(
+          {lang: 'en-GB', name: 'British', default: true});
+      setVoices(speech, [voice]);
+      voicePackController.setServerStatus(
+          'en-gb', mojoVoicePackStatusToVoicePackStatusEnum('kInstalled'));
+      voicePackController.setServerStatus(
+          'en-us', mojoVoicePackStatusToVoicePackStatusEnum('kInstalled'));
+
+      voicePackController.onPageLanguageChanged();
+
+      assertEquals(voice, voicePackController.getCurrentVoice());
+    });
+
+    test('uses a natural enabled voice if no same locale', () => {
+      voicePackController.enableLang(lang3);
+      chrome.readingMode.baseLanguageForSpeech = lang2;
+
+      voicePackController.onPageLanguageChanged();
+
+      assertEquals(
+          naturalVoiceWithLang3, voicePackController.getCurrentVoice());
+    });
+
+    test('uses a default enabled voice if no natural voice', () => {
+      voicePackController.enableLang(lang1);
+      chrome.readingMode.baseLanguageForSpeech = lang2;
+
+      voicePackController.onPageLanguageChanged();
+
+      assertEquals(
+          defaultVoiceWithLang1, voicePackController.getCurrentVoice());
+    });
+
+    test('no voice if no enabled languages', () => {
+      chrome.readingMode.baseLanguageForSpeech = lang2;
+      for (const lang of voicePackController.getEnabledLangs()) {
+        voicePackController.onLanguageToggle(lang);
+      }
+
+      voicePackController.onPageLanguageChanged();
+
+      assertFalse(!!voicePackController.getCurrentVoice());
+    });
+  });
+
   suite('updateVoicePackStatus', () => {
     const lang = 'pt-br';
 
@@ -962,8 +1199,7 @@
     });
 
     test(
-        'switches to newly available voices if it\'s for the current language',
-        () => {
+        'uses newly available voices if it\'s for the current language', () => {
           const lang = 'en-us';
           chrome.readingMode.baseLanguageForSpeech = lang;
           voicePackController.enableLang(lang);
diff --git a/chrome/test/data/webui/tab_search/split_new_tab_page_test.ts b/chrome/test/data/webui/tab_search/split_new_tab_page_test.ts
index 534f609..d028bbf6 100644
--- a/chrome/test/data/webui/tab_search/split_new_tab_page_test.ts
+++ b/chrome/test/data/webui/tab_search/split_new_tab_page_test.ts
@@ -13,92 +13,99 @@
 
 const ACTIVE_TAB_ID = 5;
 
+function createWindowData() {
+  return [
+    {
+      active: true,
+      height: SAMPLE_WINDOW_HEIGHT,
+      tabs: [
+        createTab({
+          tabId: 3,
+          title: 'Google',
+          url: {url: 'https://www.google.com'},
+          visible: true,
+        }),
+        createTab({
+          active: true,
+          index: 1,
+          tabId: ACTIVE_TAB_ID,
+          title: 'Split View New Tab Page',
+          url: {url: 'chrome://tab-search.top-chrome/split_new_tab_page.html'},
+          visible: true,
+        }),
+        createTab({
+          alertStates: [TabAlertState.kMediaRecording],
+          index: 2,
+          lastActiveTimeTicks: {internalValue: BigInt(2)},
+          tabId: 6,
+          title: 'Facebook',
+          url: {url: 'https://www.facebook.com'},
+        }),
+        createTab({
+          index: 4,
+          lastActiveTimeTicks: {internalValue: BigInt(7)},
+          tabId: 7,
+          title: 'Expedia',
+          url: {url: 'https://www.expedia.com'},
+        }),
+        createTab({
+          index: 5,
+          lastActiveTimeTicks: {internalValue: BigInt(8)},
+          tabId: 8,
+          title: 'Wikipedia',
+          url: {url: 'https://en.wikipedia.org'},
+        }),
+      ],
+    },
+    {
+      active: false,
+      height: SAMPLE_WINDOW_HEIGHT,
+      tabs: [
+        createTab({
+          active: true,
+          tabId: 4,
+          title: 'Apple',
+          url: {url: 'https://www.apple.com/'},
+        }),
+      ],
+    },
+  ];
+}
+
 suite('SplitNewTabPageTest', () => {
   let splitNewTabPage: SplitNewTabPageAppElement;
   let testApiProxy: TestTabSearchApiProxy;
 
-  function createWindowData() {
-    return [
-      {
-        active: true,
-        height: SAMPLE_WINDOW_HEIGHT,
-        tabs: [
-          createTab({
-            tabId: 3,
-            title: 'Google',
-            url: {url: 'https://www.google.com'},
-            visible: true,
-          }),
-          createTab({
-            active: true,
-            index: 1,
-            tabId: ACTIVE_TAB_ID,
-            title: 'Split View New Tab Page',
-            url:
-                {url: 'chrome://tab-search.top-chrome/split_new_tab_page.html'},
-            visible: true,
-          }),
-          createTab({
-            alertStates: [TabAlertState.kMediaRecording],
-            index: 2,
-            lastActiveTimeTicks: {internalValue: BigInt(2)},
-            tabId: 6,
-            title: 'Facebook',
-            url: {url: 'https://www.facebook.com'},
-          }),
-          createTab({
-            index: 4,
-            lastActiveTimeTicks: {internalValue: BigInt(7)},
-            tabId: 7,
-            title: 'Expedia',
-            url: {url: 'https://www.expedia.com'},
-          }),
-          createTab({
-            index: 5,
-            lastActiveTimeTicks: {internalValue: BigInt(8)},
-            tabId: 8,
-            title: 'Wikipedia',
-            url: {url: 'https://en.wikipedia.org'},
-          }),
-        ],
-      },
-      {
-        active: false,
-        height: SAMPLE_WINDOW_HEIGHT,
-        tabs: [
-          createTab({
-            active: true,
-            tabId: 4,
-            title: 'Apple',
-            url: {url: 'https://www.apple.com/'},
-          }),
-        ],
-      },
-    ];
-  }
-
-  async function splitNewTabPageSetup(windowData?: any) {
-    loadTimeData.overrideValues({
-      splitViewEnabled: true,
-    });
-
-    document.body.innerHTML = window.trustedTypes!.emptyHTML;
-
-    testApiProxy = new TestTabSearchApiProxy();
-    testApiProxy.setProfileData(
-        createProfileData({windows: (windowData || createWindowData())}));
-    testApiProxy.setIsSplit(true);
-    TabSearchApiProxyImpl.setInstance(testApiProxy);
-
+  async function splitNewTabPageSetup() {
     splitNewTabPage = document.createElement('split-new-tab-page-app');
     document.body.appendChild(splitNewTabPage);
 
     // TODO(crbug.com/412693981): Figure out why this is needed only in tests.
     splitNewTabPage.shadowRoot.querySelector<HTMLElement>(
                                   '.tab-list')!.style.flexGrow = '1';
+
     await eventToPromise('viewport-filled', splitNewTabPage.$.splitTabsList);
   }
 
+  setup(() => {
+    document.body.innerHTML = window.trustedTypes!.emptyHTML;
+
+    loadTimeData.overrideValues({
+      splitViewEnabled: true,
+    });
+
+    testApiProxy = new TestTabSearchApiProxy();
+    testApiProxy.setProfileData(
+        createProfileData({windows: createWindowData()}));
+    testApiProxy.setIsSplit(true);
+    TabSearchApiProxyImpl.setInstance(testApiProxy);
+  });
+
+  teardown(() => {
+    testApiProxy.reset();
+    splitNewTabPage.remove();
+  });
+
   test('Shows correct tab count', async () => {
     await splitNewTabPageSetup();
     assertEquals(1, testApiProxy.getCallCount('getProfileData'));
@@ -147,7 +154,8 @@
     const windowData = createWindowData();
     const tab = windowData[0]!.tabs[1] as Tab;
     tab.visible = false;
-    await splitNewTabPageSetup(windowData);
+    testApiProxy.setProfileData(createProfileData({windows: windowData}));
+    await splitNewTabPageSetup();
     const initialTabSearchItems =
         splitNewTabPage.shadowRoot.querySelectorAll('tab-search-item');
     assertEquals(4, initialTabSearchItems.length);
diff --git a/chrome/test/interaction/interactive_browser_test_internal.cc b/chrome/test/interaction/interactive_browser_test_internal.cc
index bd0ecf3..020b852 100644
--- a/chrome/test/interaction/interactive_browser_test_internal.cc
+++ b/chrome/test/interaction/interactive_browser_test_internal.cc
@@ -48,6 +48,9 @@
       public views::WidgetObserver {
  public:
   BrowserWidgetFocusSupplier() {
+    for (Browser* browser : *BrowserList::GetInstance()) {
+      ObserveBrowserActivationChange(browser);
+    }
     observation_.Observe(BrowserList::GetInstance());
   }
 
@@ -60,11 +63,7 @@
   DECLARE_FRAMEWORK_SPECIFIC_METADATA()
 
   void OnBrowserAdded(Browser* browser) override {
-    if (auto* const view = BrowserView::GetBrowserViewForBrowser(browser)) {
-      if (auto* const widget = view->GetWidget()) {
-        widget->AddObserver(this);
-      }
-    }
+    ObserveBrowserActivationChange(browser);
   }
 
   void OnBrowserRemoved(Browser* browser) override {
@@ -99,6 +98,14 @@
   }
 
  private:
+  void ObserveBrowserActivationChange(Browser* browser) {
+    if (auto* const view = BrowserView::GetBrowserViewForBrowser(browser)) {
+      if (auto* const widget = view->GetWidget()) {
+        widget->AddObserver(this);
+      }
+    }
+  }
+
   base::ScopedObservation<BrowserList, BrowserListObserver> observation_{this};
 };
 
diff --git a/chromecast/CHROMECAST_OWNERS b/chromecast/CHROMECAST_OWNERS
index 5321a22..84ff774 100644
--- a/chromecast/CHROMECAST_OWNERS
+++ b/chromecast/CHROMECAST_OWNERS
@@ -3,6 +3,7 @@
 
 # For Android specific changes, please add:
 sanfin@chromium.org
+sandv@google.com
 
 # For Linux specific changes, please add:
 antoniori@google.com
diff --git a/chromeos/ash/components/assistant/ambient.gni b/chromeos/ash/components/assistant/ambient.gni
index ddd43f35..eba4bc25 100644
--- a/chromeos/ash/components/assistant/ambient.gni
+++ b/chromeos/ash/components/assistant/ambient.gni
@@ -1,7 +1,13 @@
+# Copyright 2025 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
 import("//build/config/chrome_build.gni")
 
+assert(is_chromeos)
+
 declare_args() {
   # Enable ambient mode backend controller implementation based on
   # is_chrome_branded.
-  enable_cros_ambient_mode_backend = is_chromeos && is_chrome_branded
+  enable_cros_ambient_mode_backend = is_chrome_branded
 }
diff --git a/chromeos/ash/components/scalable_iph/scalable_iph.gni b/chromeos/ash/components/scalable_iph/scalable_iph.gni
index c0d7247..2788200 100644
--- a/chromeos/ash/components/scalable_iph/scalable_iph.gni
+++ b/chromeos/ash/components/scalable_iph/scalable_iph.gni
@@ -1,6 +1,12 @@
+# Copyright 2025 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
 import("//build/config/chrome_build.gni")
 
+assert(is_chromeos)
+
 declare_args() {
   # Enable scalable IPH.
-  enable_cros_scalable_iph = is_chromeos && is_chrome_branded
+  enable_cros_scalable_iph = is_chrome_branded
 }
diff --git a/chromeos/ash/services/quick_pair/fast_pair_data_parser_unittest.cc b/chromeos/ash/services/quick_pair/fast_pair_data_parser_unittest.cc
index 439407d..97da14e 100644
--- a/chromeos/ash/services/quick_pair/fast_pair_data_parser_unittest.cc
+++ b/chromeos/ash/services/quick_pair/fast_pair_data_parser_unittest.cc
@@ -10,10 +10,12 @@
 #include <iterator>
 #include <optional>
 
+#include "ash/constants/ash_features.h"
 #include "ash/quick_pair/common/fast_pair/fast_pair_service_data_creator.h"
 #include "base/run_loop.h"
 #include "base/strings/string_number_conversions.h"
 #include "base/test/bind.h"
+#include "base/test/scoped_feature_list.h"
 #include "base/test/task_environment.h"
 #include "chromeos/ash/services/quick_pair/public/cpp/decrypted_passkey.h"
 #include "chromeos/ash/services/quick_pair/public/cpp/decrypted_response.h"
@@ -78,6 +80,11 @@
 };
 
 TEST_F(FastPairDataParserTest, DecryptResponseUnsuccessfully) {
+  base::test::ScopedFeatureList feature_list;
+  feature_list.InitWithFeatures(
+      /*enabled_features=*/{},
+      /*disabled_features=*/{features::kFastPairKeyboards});
+
   std::vector<uint8_t> response_bytes = {/*message_type=*/0x02,
                                          /*address_bytes=*/0x02,
                                          0x03,
@@ -143,6 +150,60 @@
   run_loop.Run();
 }
 
+TEST_F(FastPairDataParserTest, DecryptExtendedResponseSuccessfully) {
+  base::test::ScopedFeatureList feature_list;
+  feature_list.InitWithFeatures(
+      /*enabled_features=*/{features::kFastPairKeyboards},
+      /*disabled_features=*/{});
+
+  std::vector<uint8_t> response_bytes;
+
+  // Message type.
+  response_bytes.push_back(0x02);
+
+  // Flags.
+  uint8_t flags = 0x01;
+  response_bytes.push_back(flags);
+
+  // Num Addresses.
+  uint8_t num_addresses = 0x01;
+  response_bytes.push_back(num_addresses);
+
+  // Address bytes.
+  std::array<uint8_t, 6> address_bytes = {0x04, 0x05, 0x06, 0x07, 0x08, 0x09};
+  std::ranges::copy(address_bytes, std::back_inserter(response_bytes));
+
+  // Random salt
+  std::array<uint8_t, 7> salt = {0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x00};
+  std::array<uint8_t, 9> expected_salt;
+  expected_salt.fill(0);
+  std::copy(salt.begin(), salt.end(), expected_salt.begin());
+  std::ranges::copy(salt, std::back_inserter(response_bytes));
+
+  std::vector<uint8_t> encrypted_bytes = EncryptBytes(response_bytes);
+
+  base::RunLoop run_loop;
+  auto callback = base::BindLambdaForTesting(
+      [&run_loop, &flags, &num_addresses, &address_bytes,
+       &expected_salt](const std::optional<DecryptedResponse>& response) {
+        EXPECT_TRUE(response.has_value());
+        EXPECT_EQ(response->message_type,
+                  FastPairMessageType::kKeyBasedPairingExtendedResponse);
+        EXPECT_TRUE(response->flags.has_value());
+        EXPECT_EQ(response->flags.value(), flags);
+        EXPECT_TRUE(response->num_addresses.has_value());
+        EXPECT_EQ(response->num_addresses.value(), num_addresses);
+        EXPECT_EQ(response->address_bytes, address_bytes);
+        EXPECT_FALSE(response->secondary_address_bytes);
+        EXPECT_EQ(response->salt, expected_salt);
+        run_loop.Quit();
+      });
+
+  data_parser_->ParseDecryptedResponse(aes_key_bytes, encrypted_bytes,
+                                       std::move(callback));
+  run_loop.Run();
+}
+
 TEST_F(FastPairDataParserTest, DecryptPasskeyUnsuccessfully) {
   std::vector<uint8_t> passkey_bytes = {/*message_type=*/0x04,
                                         /*passkey=*/0x02,
diff --git a/chromeos/ash/services/quick_pair/fast_pair_decryption_unittest.cc b/chromeos/ash/services/quick_pair/fast_pair_decryption_unittest.cc
index c629c1c..231cdc0 100644
--- a/chromeos/ash/services/quick_pair/fast_pair_decryption_unittest.cc
+++ b/chromeos/ash/services/quick_pair/fast_pair_decryption_unittest.cc
@@ -54,6 +54,11 @@
 }
 
 TEST_F(FastPairDecryptionTest, ParseDecryptedResponse_Failure) {
+  base::test::ScopedFeatureList feature_list;
+  feature_list.InitWithFeatures(
+      /*enabled_features=*/{},
+      /*disabled_features=*/{features::kFastPairKeyboards});
+
   std::array<uint8_t, kBlockByteSize> response_bytes = {/*message_type=*/0x02,
                                                         /*address_bytes=*/0x02,
                                                         0x03,
diff --git a/chromeos/components/magic_boost/public/cpp/magic_boost_state.cc b/chromeos/components/magic_boost/public/cpp/magic_boost_state.cc
index 40784e4b..4a2f623 100644
--- a/chromeos/components/magic_boost/public/cpp/magic_boost_state.cc
+++ b/chromeos/components/magic_boost/public/cpp/magic_boost_state.cc
@@ -62,6 +62,22 @@
   return true;
 }
 
+bool MagicBoostState::IsMagicBoostAvailable() const {
+  return magic_boost_available_.value_or(false);
+}
+
+void MagicBoostState::UpdateMagicBoostAvailable(bool available) {
+  if (magic_boost_available_ == available) {
+    return;
+  }
+
+  magic_boost_available_ = available;
+
+  for (auto& observer : observers_) {
+    observer.OnMagicBoostAvailableUpdated(magic_boost_available_.value());
+  }
+}
+
 void MagicBoostState::UpdateMagicBoostEnabled(bool enabled) {
   magic_boost_enabled_ = enabled;
 
diff --git a/chromeos/components/magic_boost/public/cpp/magic_boost_state.h b/chromeos/components/magic_boost/public/cpp/magic_boost_state.h
index 59119c7..7f0388a3 100644
--- a/chromeos/components/magic_boost/public/cpp/magic_boost_state.h
+++ b/chromeos/components/magic_boost/public/cpp/magic_boost_state.h
@@ -46,6 +46,7 @@
   // A checked observer which receives MagicBoost state changes.
   class Observer : public base::CheckedObserver {
    public:
+    virtual void OnMagicBoostAvailableUpdated(bool available) {}
     virtual void OnMagicBoostEnabledUpdated(bool enabled) {}
     virtual void OnHMREnabledUpdated(bool enabled) {}
     virtual void OnHMRConsentStatusUpdated(HMRConsentStatus status) {}
@@ -73,10 +74,6 @@
   void AddObserver(Observer* observer);
   void RemoveObserver(Observer* observer);
 
-  // Check if the feature is available to use. It will be unavailable in lacros
-  // and if mahi is not available.
-  virtual bool IsMagicBoostAvailable() = 0;
-
   // Check if HMR requires the notice banner to appear in the settings page.
   // It will be false in lacros and if the HMR consent status is anything other
   // than Declined.
@@ -114,6 +111,12 @@
   // is approved or pending).
   bool ShouldShowHmrCard();
 
+  bool IsMagicBoostAvailable() const;
+
+  base::expected<bool, Error> magic_boost_available() const {
+    return magic_boost_available_;
+  }
+
   base::expected<bool, Error> magic_boost_enabled() const {
     return magic_boost_enabled_;
   }
@@ -129,6 +132,7 @@
   }
 
  protected:
+  void UpdateMagicBoostAvailable(bool available);
   void UpdateMagicBoostEnabled(bool enabled);
   void UpdateHMREnabled(bool enabled);
   void UpdateHMRConsentStatus(HMRConsentStatus status);
@@ -139,6 +143,8 @@
 
   // Use `base::expected` instead of `std::optional` to avoid implicit bool
   // conversion: https://abseil.io/tips/141.
+  base::expected<bool, Error> magic_boost_available_ =
+      base::unexpected(Error::kUninitialized);
   base::expected<bool, Error> magic_boost_enabled_ =
       base::unexpected(Error::kUninitialized);
   base::expected<bool, Error> hmr_enabled_ =
diff --git a/chromeos/components/magic_boost/test/fake_magic_boost_state.cc b/chromeos/components/magic_boost/test/fake_magic_boost_state.cc
index 435c6a4..24b63d0 100644
--- a/chromeos/components/magic_boost/test/fake_magic_boost_state.cc
+++ b/chromeos/components/magic_boost/test/fake_magic_boost_state.cc
@@ -9,10 +9,6 @@
 namespace chromeos {
 namespace test {
 
-bool FakeMagicBoostState::IsMagicBoostAvailable() {
-  return is_magic_boost_available_;
-}
-
 bool FakeMagicBoostState::ShouldIncludeOrcaInOptInSync() {
   return false;
 }
@@ -34,8 +30,8 @@
   UpdateHMREnabled(enabled);
 }
 
-void FakeMagicBoostState::SetMagicBoostAvailability(bool available) {
-  is_magic_boost_available_ = available;
+void FakeMagicBoostState::SetAvailability(bool available) {
+  UpdateMagicBoostAvailable(available);
 }
 
 void FakeMagicBoostState::SetMagicBoostEnabled(bool enabled) {
diff --git a/chromeos/components/magic_boost/test/fake_magic_boost_state.h b/chromeos/components/magic_boost/test/fake_magic_boost_state.h
index 5b6c1fa..dd92a60 100644
--- a/chromeos/components/magic_boost/test/fake_magic_boost_state.h
+++ b/chromeos/components/magic_boost/test/fake_magic_boost_state.h
@@ -12,7 +12,6 @@
 
 class FakeMagicBoostState : public chromeos::MagicBoostState {
  public:
-  bool IsMagicBoostAvailable() override;
   bool CanShowNoticeBannerForHMR() override;
   int32_t AsyncIncrementHMRConsentWindowDismissCount() override;
   void AsyncWriteConsentStatus(
@@ -22,11 +21,8 @@
   void DisableOrcaFeature() override {}
   void DisableLobsterSettings() override {}
 
-  void SetMagicBoostAvailability(bool available);
+  void SetAvailability(bool available);
   void SetMagicBoostEnabled(bool enabled);
-
- private:
-  bool is_magic_boost_available_ = true;
 };
 
 }  // namespace test
diff --git a/chromeos/components/quick_answers/public/cpp/quick_answers_state.cc b/chromeos/components/quick_answers/public/cpp/quick_answers_state.cc
index a36563c..191385c 100644
--- a/chromeos/components/quick_answers/public/cpp/quick_answers_state.cc
+++ b/chromeos/components/quick_answers/public/cpp/quick_answers_state.cc
@@ -186,15 +186,9 @@
   observers_.RemoveObserver(observer);
 }
 
-void QuickAnswersState::OnMagicBoostEnabledUpdated(bool enabled) {
+void QuickAnswersState::OnMagicBoostAvailableUpdated(bool available) {
   // MagicBoost's availability check includes an async operation. It can return
   // false for a short period even if a user/device is eligible.
-  // `MagicBoostState` does not have an interface to allow clients to listen
-  // availability change. As a workaround, we are currently using
-  // `OnMagicBoostEnabled` as a signal. See
-  // `SearchSection::OnMagicBoostEnabledUpdated` as an example.
-  // TODO(b/383612536): allow clients to observe MagicBoostState availability
-  // change
   MaybeNotifyFeatureTypeChanged();
 }
 
@@ -257,9 +251,24 @@
     return base::unexpected(QuickAnswersState::Error::kUninitialized);
   }
 
-  return magic_boost_state->IsMagicBoostAvailable()
-             ? QuickAnswersState::FeatureType::kHmr
-             : QuickAnswersState::FeatureType::kQuickAnswers;
+  return magic_boost_state->magic_boost_available()
+      .transform([](bool available) {
+        if (available) {
+          return QuickAnswersState::FeatureType::kHmr;
+        } else {
+          return QuickAnswersState::FeatureType::kQuickAnswers;
+        }
+      })
+      .transform_error([](chromeos::MagicBoostState::Error error) {
+        // Use `switch` statement as it gets a compile error when
+        // `MagicBoostState::Error` enum class value added.
+        switch (error) {
+          case chromeos::MagicBoostState::Error::kUninitialized:
+            return QuickAnswersState::Error::kUninitialized;
+        }
+        CHECK(false)
+            << "Unknown MagicBoostState::Error enum class value provided.";
+      });
 }
 
 base::expected<bool, QuickAnswersState::Error>
diff --git a/chromeos/components/quick_answers/public/cpp/quick_answers_state.h b/chromeos/components/quick_answers/public/cpp/quick_answers_state.h
index f43e1b90..8b3ca886 100644
--- a/chromeos/components/quick_answers/public/cpp/quick_answers_state.h
+++ b/chromeos/components/quick_answers/public/cpp/quick_answers_state.h
@@ -113,7 +113,7 @@
   void RemoveObserver(QuickAnswersStateObserver* observer);
 
   // chromeos::MagicBoostState::Observer:
-  void OnMagicBoostEnabledUpdated(bool enabled) override;
+  void OnMagicBoostAvailableUpdated(bool available) override;
   void OnHMREnabledUpdated(bool enabled) override;
   void OnHMRConsentStatusUpdated(
       chromeos::HMRConsentStatus consent_status) override;
diff --git a/chromeos/components/quick_answers/public/cpp/quick_answers_state_unittest.cc b/chromeos/components/quick_answers/public/cpp/quick_answers_state_unittest.cc
index f99e9b97..b13228b5 100644
--- a/chromeos/components/quick_answers/public/cpp/quick_answers_state_unittest.cc
+++ b/chromeos/components/quick_answers/public/cpp/quick_answers_state_unittest.cc
@@ -97,6 +97,7 @@
 
 TEST_F(QuickAnswersStateWithMagicBoostTest, IsEligibleFeatureType) {
   chromeos::test::FakeMagicBoostState magic_boost_state;
+  magic_boost_state.SetAvailability(true);
   FakeQuickAnswersState quick_answers_state;
   quick_answers_state.SetApplicationLocale("en");
 
@@ -139,7 +140,7 @@
 // user/device is eligible.
 TEST_F(QuickAnswersStateWithMagicBoostTest, MagicBoostStateEligibilityChanged) {
   chromeos::test::FakeMagicBoostState magic_boost_state;
-  magic_boost_state.SetMagicBoostAvailability(false);
+  magic_boost_state.SetAvailability(false);
   FakeQuickAnswersState quick_answers_state;
   quick_answers_state.SetApplicationLocale("en");
 
@@ -153,7 +154,7 @@
 
   // Simulate that MagicBoost availability check async operation is compalted
   // and a user has went through MagicBoost consent flow.
-  magic_boost_state.SetMagicBoostAvailability(true);
+  magic_boost_state.SetAvailability(true);
   magic_boost_state.SetMagicBoostEnabled(true);
 
   EXPECT_EQ(QuickAnswersState::FeatureType::kHmr,
@@ -191,6 +192,7 @@
 
 TEST_F(QuickAnswersStateWithMagicBoostTest, IsEnabledUnderMagicBoost) {
   chromeos::test::FakeMagicBoostState magic_boost_state;
+  magic_boost_state.SetAvailability(true);
   FakeQuickAnswersState quick_answers_state;
   quick_answers_state.SetApplicationLocale("en");
   quick_answers_state.SetSettingsEnabled(true);
@@ -271,6 +273,7 @@
 
 TEST_F(QuickAnswersStateWithMagicBoostTest, GetConsentStatusUnderMagicBoost) {
   chromeos::test::FakeMagicBoostState magic_boost_state;
+  magic_boost_state.SetAvailability(true);
   FakeObserver observer;
   FakeQuickAnswersState quick_answers_state;
   quick_answers_state.AddObserver(&observer);
@@ -298,6 +301,7 @@
   FakeQuickAnswersState quick_answers_state;
   quick_answers_state.SetApplicationLocale("en");
 
+  magic_boost_state.SetAvailability(true);
   magic_boost_state.AsyncWriteHMREnabled(true);
   magic_boost_state.AsyncWriteConsentStatus(
       chromeos::HMRConsentStatus::kPendingDisclaimer);
@@ -317,6 +321,7 @@
   FakeQuickAnswersState quick_answers_state;
   quick_answers_state.SetApplicationLocale("en");
 
+  magic_boost_state.SetAvailability(true);
   magic_boost_state.AsyncWriteConsentStatus(
       chromeos::HMRConsentStatus::kPendingDisclaimer);
 
@@ -397,6 +402,7 @@
 
 TEST_F(QuickAnswersStateWithMagicBoostTest, IsIntentEligibleUnderMagicBoost) {
   chromeos::test::FakeMagicBoostState magic_boost_state;
+  magic_boost_state.SetAvailability(true);
 
   FakeQuickAnswersState quick_answers_state;
   quick_answers_state.SetApplicationLocale("en");
diff --git a/chromeos/profiles/arm.afdo.newest.txt b/chromeos/profiles/arm.afdo.newest.txt
index 80c93ba..580d8be 100644
--- a/chromeos/profiles/arm.afdo.newest.txt
+++ b/chromeos/profiles/arm.afdo.newest.txt
@@ -1 +1 @@
-chromeos-chrome-arm-none-138-7137.0-1746411594-benchmark-138.0.7171.0-r1-redacted.afdo.xz
+chromeos-chrome-arm-none-138-7151.8-1747016258-benchmark-138.0.7171.0-r1-redacted.afdo.xz
diff --git a/chromeos/profiles/atom.afdo.newest.txt b/chromeos/profiles/atom.afdo.newest.txt
index 3e8c0da..3f85456e 100644
--- a/chromeos/profiles/atom.afdo.newest.txt
+++ b/chromeos/profiles/atom.afdo.newest.txt
@@ -1 +1 @@
-chromeos-chrome-amd64-atom-138-7137.0-1746412216-benchmark-138.0.7171.0-r1-redacted.afdo.xz
+chromeos-chrome-amd64-atom-138-7151.17-1747032091-benchmark-138.0.7171.0-r1-redacted.afdo.xz
diff --git a/chromeos/tast_control.gni b/chromeos/tast_control.gni
index 9c295b6..4f786b2 100644
--- a/chromeos/tast_control.gni
+++ b/chromeos/tast_control.gni
@@ -102,10 +102,6 @@
   "health.MonitorKeyboardDiagnosticEvent.tablet_mode_form_factors",
   "health.MonitorKeyboardDiagnosticEvent.non_tablet_mode_form_factors",
 
-  # b/408523771
-  "diagnostics.CheckCPURoutine@jacuzzi",
-  "diagnostics.CheckCPURoutine@octopus",
-
   # b/409349162
   "inputs.PhysicalKeyboardKoreanTyping@jacuzzi",
 
diff --git a/clank b/clank
index 99be04a..24dba409 160000
--- a/clank
+++ b/clank
@@ -1 +1 @@
-Subproject commit 99be04a0cf568303f3f820a5e8de7d39287e6dfb
+Subproject commit 24dba4094427e89cdb21ff38df13ac54ce2bb238
diff --git a/components/browser_ui/styles/android/java/src/org/chromium/components/browser_ui/styles/SemanticColorUtils.java b/components/browser_ui/styles/android/java/src/org/chromium/components/browser_ui/styles/SemanticColorUtils.java
index c79fc44e..6501f21 100644
--- a/components/browser_ui/styles/android/java/src/org/chromium/components/browser_ui/styles/SemanticColorUtils.java
+++ b/components/browser_ui/styles/android/java/src/org/chromium/components/browser_ui/styles/SemanticColorUtils.java
@@ -183,6 +183,11 @@
         return resolve(R.attr.colorOnPrimary, context);
     }
 
+    /** Returns the semantic color values that correspond to colorOnSurface. */
+    public static @ColorInt int getColorOnSurface(Context context) {
+        return resolve(R.attr.colorOnSurface, context);
+    }
+
     /** Returns the semantic color values that correspond to colorOnSurfaceInverse. */
     public static @ColorInt int getColorOnSurfaceInverse(Context context) {
         return resolve(R.attr.colorOnSurfaceInverse, context);
diff --git a/components/collaboration/internal/android/java/src/org/chromium/components/collaboration/messaging/MessagingBackendServiceBridge.java b/components/collaboration/internal/android/java/src/org/chromium/components/collaboration/messaging/MessagingBackendServiceBridge.java
index 14b9a8d..8c24646c 100644
--- a/components/collaboration/internal/android/java/src/org/chromium/components/collaboration/messaging/MessagingBackendServiceBridge.java
+++ b/components/collaboration/internal/android/java/src/org/chromium/components/collaboration/messaging/MessagingBackendServiceBridge.java
@@ -19,6 +19,7 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Optional;
+import java.util.Set;
 
 /** Implementation of {@link MessagingBackendService} that connects to the native counterpart. */
 @JNINamespace("collaboration::messaging::android")
@@ -238,6 +239,14 @@
                 });
     }
 
+    @CalledByNative
+    private void hideInstantaneousMessage(Set<String> messageIds) {
+        if (mInstantMessageDelegate == null) {
+            return;
+        }
+        mInstantMessageDelegate.hideInstantaneousMessage(messageIds);
+    }
+
     @NativeMethods
     interface Natives {
         boolean isInitialized(
diff --git a/components/collaboration/internal/android/java/src/org/chromium/components/collaboration/messaging/MessagingBackendServiceBridgeUnitTestCompanion.java b/components/collaboration/internal/android/java/src/org/chromium/components/collaboration/messaging/MessagingBackendServiceBridgeUnitTestCompanion.java
index 572f1ed..c763e24 100644
--- a/components/collaboration/internal/android/java/src/org/chromium/components/collaboration/messaging/MessagingBackendServiceBridgeUnitTestCompanion.java
+++ b/components/collaboration/internal/android/java/src/org/chromium/components/collaboration/messaging/MessagingBackendServiceBridgeUnitTestCompanion.java
@@ -25,8 +25,11 @@
 import org.chromium.components.tab_group_sync.LocalTabGroupId;
 import org.chromium.components.tab_groups.TabGroupColorId;
 
+import java.util.Arrays;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Optional;
+import java.util.Set;
 
 /** A companion object to the native MessagingBackendServiceBridgeTest. */
 @JNINamespace("collaboration::messaging")
@@ -43,6 +46,8 @@
             ArgumentCaptor.forClass(InstantMessage.class);
     private final ArgumentCaptor<Callback> mInstantMessageCallbackCaptor =
             ArgumentCaptor.forClass(Callback.class);
+    private ArgumentCaptor<Set<String>> mHideInstantMessageIdsCaptor =
+            ArgumentCaptor.forClass(Set.class);
 
     @CalledByNative
     private MessagingBackendServiceBridgeUnitTestCompanion(MessagingBackendService service) {
@@ -233,6 +238,18 @@
     }
 
     @CalledByNative
+    private void verifyHideInstantMessageCalledWithIds(String[] expectedIdsArray) {
+        verify(mInstantMessageDelegate)
+                .hideInstantaneousMessage(mHideInstantMessageIdsCaptor.capture());
+        Set<String> actualIds = mHideInstantMessageIdsCaptor.getValue();
+        Assert.assertEquals(
+                "Number of hidden IDs does not match", expectedIdsArray.length, actualIds.size());
+        // Convert String[] to Set<String> for proper comparison, as order doesn't matter in Set.
+        Set<String> expectedIdsSet = new HashSet<>(Arrays.asList(expectedIdsArray));
+        Assert.assertEquals("Hidden message IDs do not match", expectedIdsSet, actualIds);
+    }
+
+    @CalledByNative
     private void invokeGetActivityLogAndVerify() {
         ActivityLogQueryParams queryParams = new ActivityLogQueryParams();
         queryParams.collaborationId = "collaboration1";
diff --git a/components/collaboration/internal/android/java/src/org/chromium/components/collaboration/messaging/bridge/ConversionUtils.java b/components/collaboration/internal/android/java/src/org/chromium/components/collaboration/messaging/bridge/ConversionUtils.java
index 6f28651..ab688806 100644
--- a/components/collaboration/internal/android/java/src/org/chromium/components/collaboration/messaging/bridge/ConversionUtils.java
+++ b/components/collaboration/internal/android/java/src/org/chromium/components/collaboration/messaging/bridge/ConversionUtils.java
@@ -29,6 +29,8 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Optional;
+import java.util.Set;
+import java.util.TreeSet;
 
 /**
  * Helper class meant to be called by native. Used to create Java objects from C++ objects. Do not
@@ -130,6 +132,16 @@
     }
 
     @CalledByNative
+    private static Set<String> createStringSet() {
+        return new TreeSet<String>();
+    }
+
+    @CalledByNative
+    private static void addStringToStringSet(Set<String> set, String string) {
+        set.add(string);
+    }
+
+    @CalledByNative
     private static List<MessageAttribution> addAttributionToList(
             @Nullable List<MessageAttribution> attributions, MessageAttribution attribution) {
         if (attributions == null) {
diff --git a/components/collaboration/internal/android/messaging/conversion_utils.cc b/components/collaboration/internal/android/messaging/conversion_utils.cc
index 350b4a6..376af12996 100644
--- a/components/collaboration/internal/android/messaging/conversion_utils.cc
+++ b/components/collaboration/internal/android/messaging/conversion_utils.cc
@@ -174,6 +174,20 @@
       j_attribution_list);
 }
 
+ScopedJavaLocalRef<jobject> UuidSetToJavaStringSet(
+    JNIEnv* env,
+    const std::set<base::Uuid>& uuids) {
+  ScopedJavaLocalRef<jobject> j_string_set =
+      Java_ConversionUtils_createStringSet(env);
+
+  for (const auto& uuid : uuids) {
+    Java_ConversionUtils_addStringToStringSet(
+        env, j_string_set,
+        ConvertUTF8ToJavaString(env, uuid.AsLowercaseString()));
+  }
+  return j_string_set;
+}
+
 ScopedJavaLocalRef<jobject> ActivityLogItemsToJava(
     JNIEnv* env,
     const std::vector<ActivityLogItem>& activity_log_items) {
diff --git a/components/collaboration/internal/android/messaging/conversion_utils.h b/components/collaboration/internal/android/messaging/conversion_utils.h
index c517753..37eaa16c 100644
--- a/components/collaboration/internal/android/messaging/conversion_utils.h
+++ b/components/collaboration/internal/android/messaging/conversion_utils.h
@@ -30,6 +30,10 @@
     JNIEnv* env,
     const InstantMessage& message);
 
+base::android::ScopedJavaLocalRef<jobject> UuidSetToJavaStringSet(
+    JNIEnv* env,
+    const std::set<base::Uuid>& uuids);
+
 // Helper method to convert a ActivityLogItem C++ list to a
 // List<ActivityLogItem> Java object.
 base::android::ScopedJavaLocalRef<jobject> ActivityLogItemsToJava(
diff --git a/components/collaboration/internal/android/messaging/messaging_backend_service_bridge.cc b/components/collaboration/internal/android/messaging/messaging_backend_service_bridge.cc
index 8162227..cc4f415 100644
--- a/components/collaboration/internal/android/messaging/messaging_backend_service_bridge.cc
+++ b/components/collaboration/internal/android/messaging/messaging_backend_service_bridge.cc
@@ -6,6 +6,7 @@
 
 #include <memory>
 #include <optional>
+#include <set>
 
 #include "base/android/jni_android.h"
 #include "base/android/jni_array.h"
@@ -14,6 +15,7 @@
 #include "base/functional/callback.h"
 #include "base/memory/ptr_util.h"
 #include "base/notreached.h"
+#include "base/uuid.h"
 #include "components/collaboration/internal/android/messaging/conversion_utils.h"
 #include "components/collaboration/public/messaging/activity_log.h"
 #include "components/collaboration/public/messaging/message.h"
@@ -268,4 +270,15 @@
       env, java_ref_, InstantMessageToJava(env, message), j_native_ptr);
 }
 
+void MessagingBackendServiceBridge::HideInstantaneousMessage(
+    const std::set<base::Uuid>& message_ids) {
+  if (java_ref_.is_null()) {
+    return;
+  }
+
+  JNIEnv* env = base::android::AttachCurrentThread();
+  Java_MessagingBackendServiceBridge_hideInstantaneousMessage(
+      env, java_ref_, UuidSetToJavaStringSet(env, message_ids));
+}
+
 }  // namespace collaboration::messaging::android
diff --git a/components/collaboration/internal/android/messaging/messaging_backend_service_bridge.h b/components/collaboration/internal/android/messaging/messaging_backend_service_bridge.h
index 5cdda732..79098c0 100644
--- a/components/collaboration/internal/android/messaging/messaging_backend_service_bridge.h
+++ b/components/collaboration/internal/android/messaging/messaging_backend_service_bridge.h
@@ -5,8 +5,11 @@
 #ifndef COMPONENTS_COLLABORATION_INTERNAL_ANDROID_MESSAGING_MESSAGING_BACKEND_SERVICE_BRIDGE_H_
 #define COMPONENTS_COLLABORATION_INTERNAL_ANDROID_MESSAGING_MESSAGING_BACKEND_SERVICE_BRIDGE_H_
 
+#include <set>
+
 #include "base/android/scoped_java_ref.h"
 #include "base/supports_user_data.h"
+#include "base/uuid.h"
 #include "components/collaboration/public/messaging/messaging_backend_service.h"
 
 namespace collaboration::messaging::android {
@@ -89,6 +92,8 @@
   void DisplayInstantaneousMessage(
       InstantMessage message,
       InstantMessageDelegate::SuccessCallback success_callback) override;
+  void HideInstantaneousMessage(
+      const std::set<base::Uuid>& message_ids) override;
 
   raw_ptr<MessagingBackendService> service_;
 
diff --git a/components/collaboration/internal/android/messaging/messaging_backend_service_bridge_unittest.cc b/components/collaboration/internal/android/messaging/messaging_backend_service_bridge_unittest.cc
index f6af5f6..f50678bf 100644
--- a/components/collaboration/internal/android/messaging/messaging_backend_service_bridge_unittest.cc
+++ b/components/collaboration/internal/android/messaging/messaging_backend_service_bridge_unittest.cc
@@ -112,6 +112,10 @@
             base::Unretained(this), expected_success_value));
   }
 
+  void HideInstantaneousMessage(const std::set<base::Uuid>& message_ids) {
+    bridge()->HideInstantaneousMessage(message_ids);
+  }
+
   // Member accessors.
   MessagingBackendServiceBridge* bridge() { return bridge_.get(); }
   MockMessagingBackendService& service() { return service_; }
@@ -312,6 +316,37 @@
   EXPECT_EQ(1U, success_callback_invocation_count());
 }
 
+TEST_F(MessagingBackendServiceBridgeTest, TestHideInstantMessage) {
+  JNIEnv* env = base::android::AttachCurrentThread();
+  // Set up the delegate for instant messages in Java.
+  Java_MessagingBackendServiceBridgeUnitTestCompanion_setInstantMessageDelegate(
+      env, j_companion());
+
+  // Create a set of UUIDs to hide.
+  std::set<base::Uuid> uuids_to_hide;
+  base::Uuid uuid1 = base::Uuid::GenerateRandomV4();
+  base::Uuid uuid2 = base::Uuid::GenerateRandomV4();
+  uuids_to_hide.insert(uuid1);
+  uuids_to_hide.insert(uuid2);
+
+  // Call the C++ bridge method. This will propagate to the Java delegate.
+  HideInstantaneousMessage(uuids_to_hide);
+
+  // Prepare the expected IDs for Java verification.
+  // The order in this vector doesn't strictly matter as it's converted to a Set
+  // in Java for comparison, but it's good practice to be consistent.
+  std::vector<std::string> expected_ids_str_vector;
+  expected_ids_str_vector.push_back(uuid1.AsLowercaseString());
+  expected_ids_str_vector.push_back(uuid2.AsLowercaseString());
+  base::android::ScopedJavaLocalRef<jobjectArray> j_expected_ids =
+      base::android::ToJavaArrayOfStrings(env, expected_ids_str_vector);
+
+  // Verify on the Java side that hideInstantaneousMessage was called with the
+  // correct set of IDs.
+  Java_MessagingBackendServiceBridgeUnitTestCompanion_verifyHideInstantMessageCalledWithIds(
+      env, j_companion(), j_expected_ids);
+}
+
 TEST_F(MessagingBackendServiceBridgeTest, TestGetMessages) {
   std::vector<PersistentMessage> messages = GetDefaultPersistentMessages();
 
diff --git a/components/collaboration/internal/messaging/instant_message_processor.h b/components/collaboration/internal/messaging/instant_message_processor.h
index 54f19c9..42fe45a 100644
--- a/components/collaboration/internal/messaging/instant_message_processor.h
+++ b/components/collaboration/internal/messaging/instant_message_processor.h
@@ -5,6 +5,9 @@
 #ifndef COMPONENTS_COLLABORATION_INTERNAL_MESSAGING_INSTANT_MESSAGE_PROCESSOR_H_
 #define COMPONENTS_COLLABORATION_INTERNAL_MESSAGING_INSTANT_MESSAGE_PROCESSOR_H_
 
+#include <set>
+
+#include "base/uuid.h"
 #include "components/collaboration/public/messaging/message.h"
 #include "components/collaboration/public/messaging/messaging_backend_service.h"
 
@@ -32,6 +35,10 @@
   // Notifies the InstantMessageDelegate to display the message for all the
   // provided levels.
   virtual void DisplayInstantMessage(const InstantMessage& message) = 0;
+
+  // Notifies the InstantMessageDelegate to hide the messages with the provided
+  // IDs.
+  virtual void HideInstantMessage(const std::set<base::Uuid>& message_ids) = 0;
 };
 
 }  // namespace collaboration::messaging
diff --git a/components/collaboration/internal/messaging/instant_message_processor_impl.cc b/components/collaboration/internal/messaging/instant_message_processor_impl.cc
index a06d3f9..a67c7d2 100644
--- a/components/collaboration/internal/messaging/instant_message_processor_impl.cc
+++ b/components/collaboration/internal/messaging/instant_message_processor_impl.cc
@@ -5,9 +5,11 @@
 #include "components/collaboration/internal/messaging/instant_message_processor_impl.h"
 
 #include <optional>
+#include <set>
 #include <unordered_map>
 #include <vector>
 
+#include "base/containers/contains.h"
 #include "base/functional/callback_helpers.h"
 #include "base/hash/hash.h"
 #include "base/strings/utf_string_conversions.h"
@@ -117,6 +119,29 @@
   ScheduleProcessing();
 }
 
+void InstantMessageProcessorImpl::HideInstantMessage(
+    const std::set<base::Uuid>& message_ids) {
+  // Remove the messages from the queue that have MessageAttribution IDs that
+  // match.
+  message_queue_.erase(
+      std::remove_if(message_queue_.begin(), message_queue_.end(),
+                     [&message_ids](const InstantMessage& message) {
+                       CHECK(IsSingleMessage(message));
+                       std::optional<base::Uuid> current_message_id =
+                           message.attributions[0].id;
+
+                       if (!current_message_id.has_value()) {
+                         return false;
+                       }
+                       return base::Contains(message_ids, *current_message_id);
+                     }),
+      message_queue_.end());
+
+  // In case the message was displayed previously, we still tell the UI about
+  // every message ID, even if it might have been removed from the queue.
+  instant_message_delegate_->HideInstantaneousMessage(message_ids);
+}
+
 void InstantMessageProcessorImpl::ScheduleProcessing() {
   if (processing_scheduled_) {
     return;
@@ -132,6 +157,10 @@
 
 void InstantMessageProcessorImpl::ProcessQueue() {
   processing_scheduled_ = false;
+  if (message_queue_.empty()) {
+    // The queue might have been cleared by HideInstantMessage.
+    return;
+  }
   std::vector<InstantMessage> aggregated_messages =
       AggregateMessages(message_queue_);
   message_queue_.clear();
diff --git a/components/collaboration/internal/messaging/instant_message_processor_impl.h b/components/collaboration/internal/messaging/instant_message_processor_impl.h
index cc0479a48..d566bbb 100644
--- a/components/collaboration/internal/messaging/instant_message_processor_impl.h
+++ b/components/collaboration/internal/messaging/instant_message_processor_impl.h
@@ -6,6 +6,7 @@
 #define COMPONENTS_COLLABORATION_INTERNAL_MESSAGING_INSTANT_MESSAGE_PROCESSOR_IMPL_H_
 
 #include <memory>
+#include <set>
 #include <vector>
 
 #include "base/memory/raw_ptr.h"
@@ -30,6 +31,7 @@
       InstantMessageDelegate* instant_message_delegate) override;
   bool IsEnabled() const override;
   void DisplayInstantMessage(const InstantMessage& message) override;
+  void HideInstantMessage(const std::set<base::Uuid>& message_ids) override;
 
  private:
   void ScheduleProcessing();
diff --git a/components/collaboration/internal/messaging/instant_message_processor_impl_unittest.cc b/components/collaboration/internal/messaging/instant_message_processor_impl_unittest.cc
index 1597597..d2435e1 100644
--- a/components/collaboration/internal/messaging/instant_message_processor_impl_unittest.cc
+++ b/components/collaboration/internal/messaging/instant_message_processor_impl_unittest.cc
@@ -5,6 +5,7 @@
 #include "components/collaboration/internal/messaging/instant_message_processor_impl.h"
 
 #include <optional>
+#include <set>
 #include <vector>
 
 #include "base/functional/callback_forward.h"
@@ -13,6 +14,7 @@
 #include "base/test/gmock_callback_support.h"
 #include "base/test/gmock_move_support.h"
 #include "base/test/task_environment.h"
+#include "base/uuid.h"
 #include "components/collaboration/public/messaging/message.h"
 #include "components/collaboration/public/messaging/messaging_backend_service.h"
 #include "components/collaboration/test_support/mock_messaging_backend_service.h"
@@ -35,6 +37,10 @@
               DisplayInstantaneousMessage,
               (InstantMessage message, SuccessCallback success_callback),
               (override));
+  MOCK_METHOD(void,
+              HideInstantaneousMessage,
+              (const std::set<base::Uuid>& message_ids),
+              (override));
 };
 
 class InstantMessageProcessorImplTest : public testing::Test {
@@ -417,4 +423,87 @@
             actual_messages[0].collaboration_event);
 }
 
+// Test for HideInstantMessage when the message is in the queue.
+TEST_F(InstantMessageProcessorImplTest, HideInstantMessage_MessageInQueue) {
+  SetupInstantMessageDelegate();
+  InstantMessage message_to_hide = CreateInstantMessage();
+  message_to_hide.attributions[0].id = msg_id1_;
+  // Use a non-aggregatable type to simplify verification.
+  message_to_hide.collaboration_event = CollaborationEvent::TAB_REMOVED;
+  message_to_hide.localized_message = u"Message to be hidden";
+
+  InstantMessage message_to_keep = CreateInstantMessage();
+  message_to_keep.attributions[0].id = msg_id2_;
+  message_to_keep.collaboration_event = CollaborationEvent::TAB_REMOVED;
+  message_to_keep.localized_message = u"Message to be kept";
+
+  // Add both messages to the queue.
+  processor_->DisplayInstantMessage(message_to_hide);
+  processor_->DisplayInstantMessage(message_to_keep);
+
+  std::set<base::Uuid> ids_to_hide = {msg_id1_};
+
+  // Expect the delegate to be asked to hide the first message.
+  EXPECT_CALL(mock_instant_message_delegate_,
+              HideInstantaneousMessage(Eq(ids_to_hide)))
+      .Times(1);
+
+  // Expect DisplayInstantaneousMessage to be called only for the message
+  // that was not hidden.
+  InstantMessage displayed_message;
+  EXPECT_CALL(mock_instant_message_delegate_, DisplayInstantaneousMessage(_, _))
+      .WillOnce(SaveArg<0>(&displayed_message));
+
+  // Hide the first message. This should happen before ProcessQueue runs.
+  processor_->HideInstantMessage(ids_to_hide);
+
+  // Fast forward time to allow ProcessQueue to run.
+  task_environment_.FastForwardBy(base::Seconds(10));
+
+  // Verify that the correct message was displayed.
+  ASSERT_FALSE(displayed_message.attributions.empty());
+  EXPECT_EQ(msg_id2_, displayed_message.attributions[0].id);
+  EXPECT_EQ(message_to_keep.localized_message,
+            displayed_message.localized_message);
+}
+
+// Test for HideInstantMessage when the message is NOT in the queue.
+TEST_F(InstantMessageProcessorImplTest, HideInstantMessage_NoMessageInQueue) {
+  SetupInstantMessageDelegate();
+
+  InstantMessage message_in_queue = CreateInstantMessage();
+  message_in_queue.attributions[0].id = msg_id2_;
+  message_in_queue.collaboration_event = CollaborationEvent::TAB_REMOVED;
+  message_in_queue.localized_message = u"Message in queue";
+
+  // Add one message to the queue.
+  processor_->DisplayInstantMessage(message_in_queue);
+
+  // Attempt to hide a different message (msg_id1_) that is not in the queue.
+  std::set<base::Uuid> ids_to_hide = {msg_id1_};
+
+  // Expect the delegate to be asked to hide msg_id1_, even if it's not in
+  // the queue.
+  EXPECT_CALL(mock_instant_message_delegate_,
+              HideInstantaneousMessage(Eq(ids_to_hide)))
+      .Times(1);
+
+  // Expect DisplayInstantaneousMessage to be called for the message
+  // that was in the queue and not targeted by HideInstantMessage.
+  InstantMessage displayed_message;
+  EXPECT_CALL(mock_instant_message_delegate_, DisplayInstantaneousMessage(_, _))
+      .WillOnce(SaveArg<0>(&displayed_message));
+
+  processor_->HideInstantMessage(ids_to_hide);
+
+  // Fast forward time to allow ProcessQueue to run.
+  task_environment_.FastForwardBy(base::Seconds(10));
+
+  // Verify that the message originally in the queue was displayed.
+  ASSERT_FALSE(displayed_message.attributions.empty());
+  EXPECT_EQ(msg_id2_, displayed_message.attributions[0].id);
+  EXPECT_EQ(message_in_queue.localized_message,
+            displayed_message.localized_message);
+}
+
 }  // namespace collaboration::messaging
diff --git a/components/collaboration/internal/messaging/messaging_backend_service_impl.cc b/components/collaboration/internal/messaging/messaging_backend_service_impl.cc
index cb4a94f90..ec1fa83 100644
--- a/components/collaboration/internal/messaging/messaging_backend_service_impl.cc
+++ b/components/collaboration/internal/messaging/messaging_backend_service_impl.cc
@@ -763,11 +763,18 @@
   // section.
   std::vector<collaboration_pb::Message> messages =
       store_->GetRecentMessagesForGroup(*collaboration_group_id);
-  std::set<std::string> message_uuids;
+  std::set<std::string> message_uuid_strings;
+  std::set<base::Uuid> message_uuids;
   for (auto& message : messages) {
-    message_uuids.insert(message.uuid());
+    message_uuid_strings.insert(message.uuid());
+    message_uuids.insert(base::Uuid::ParseLowercase(message.uuid()));
   }
-  store_->RemoveMessages(message_uuids);
+  store_->RemoveMessages(message_uuid_strings);
+
+  // Regardless of whether the user is leaving or deleting the group and
+  // regardless of whether it happened from a remote event or a local event,
+  // we should hide any instant messages related to the group.
+  instant_message_processor_->HideInstantMessage(message_uuids);
 
   if (source == tab_groups::TriggerSource::LOCAL) {
     return;
diff --git a/components/collaboration/internal/messaging/messaging_backend_service_impl_unittest.cc b/components/collaboration/internal/messaging/messaging_backend_service_impl_unittest.cc
index 74373e0..473e532 100644
--- a/components/collaboration/internal/messaging/messaging_backend_service_impl_unittest.cc
+++ b/components/collaboration/internal/messaging/messaging_backend_service_impl_unittest.cc
@@ -129,6 +129,10 @@
               DisplayInstantaneousMessage,
               (InstantMessage message, SuccessCallback success_callback),
               (override));
+  MOCK_METHOD(void,
+              HideInstantaneousMessage,
+              (const std::set<base::Uuid>& message_ids),
+              (override));
 };
 
 class MockPersistentMessageObserver
@@ -585,6 +589,7 @@
 
 TEST_F(MessagingBackendServiceImplTest, TestStoringTabGroupEventsFromRemote) {
   CreateAndInitializeService();
+  SetupInstantMessageDelegate();
 
   data_sharing::GroupId collaboration_group_id =
       data_sharing::GroupId("my group id");
@@ -633,6 +638,7 @@
 
 TEST_F(MessagingBackendServiceImplTest, TestStoringTabGroupEventsFromLocal) {
   CreateAndInitializeService();
+  SetupInstantMessageDelegate();
 
   data_sharing::GroupId collaboration_group_id =
       data_sharing::GroupId("my group id");
@@ -1635,6 +1641,7 @@
        TestTabGroupRemovalWillHideExistingMessagesFromUi) {
   CreateAndInitializeService();
   AddPersistentMessageObserver();
+  SetupInstantMessageDelegate();
 
   data_sharing::GroupId collaboration_group_id =
       data_sharing::GroupId("my group id");
@@ -1650,6 +1657,11 @@
       DirtyType::kDotAndChip, now);
   AddMessage(message);
 
+  collaboration_pb::Message instant_message_db = CreateStoredMessage(
+      collaboration_group_id, collaboration_pb::EventType::TAB_REMOVED,
+      DirtyType::kMessageOnly, now);
+  AddMessage(instant_message_db);
+
   // Setup a tab group in TabGroupSyncService associated with the collaboration.
   // It's necessary because messaging backend will consult TabGroupSyncService
   // for the group info.
@@ -1679,6 +1691,7 @@
   // messages that are already showing.
   // 1. Hide two persistent messages for the tab (chip and dirty dot).
   // 2. Hide one persistent message for tab group dirty dot.
+  // 3. Hide all instant messages that it knows about.
 
   PersistentMessage message1, message2, message3;
   testing::InSequence sequence;
@@ -1689,6 +1702,10 @@
   EXPECT_CALL(mock_persistent_message_observer_, HidePersistentMessage(_))
       .WillOnce(SaveArg<0>(&message3));  // Capture the third message
 
+  std::set<base::Uuid> instant_message_ids;
+  EXPECT_CALL(*mock_instant_message_delegate_, HideInstantaneousMessage(_))
+      .WillOnce(SaveArg<0>(&instant_message_ids));
+
   // Invoke the service API for tab group removal (e.g. unshare flow).
   service_->OnTabGroupRemoved(tab_group, tab_groups::TriggerSource::REMOTE);
 
@@ -1716,6 +1733,13 @@
   EXPECT_EQ(PersistentNotificationType::DIRTY_TAB_GROUP, message3.type);
   EXPECT_EQ(tab_group.saved_guid(),
             message3.attribution.tab_group_metadata->sync_tab_group_id.value());
+
+  // We forcefully hide all messages that could have been instant messages.
+  EXPECT_FALSE(instant_message_ids.empty());
+  EXPECT_TRUE(
+      instant_message_ids.contains(base::Uuid::ParseLowercase(message.uuid())));
+  EXPECT_TRUE(instant_message_ids.contains(
+      base::Uuid::ParseLowercase(instant_message_db.uuid())));
 }
 
 TEST_F(MessagingBackendServiceImplTest,
diff --git a/components/collaboration/public/android/java/src/org/chromium/components/collaboration/messaging/MessagingBackendService.java b/components/collaboration/public/android/java/src/org/chromium/components/collaboration/messaging/MessagingBackendService.java
index 6c1d68a..02ffadf 100644
--- a/components/collaboration/public/android/java/src/org/chromium/components/collaboration/messaging/MessagingBackendService.java
+++ b/components/collaboration/public/android/java/src/org/chromium/components/collaboration/messaging/MessagingBackendService.java
@@ -11,6 +11,7 @@
 
 import java.util.List;
 import java.util.Optional;
+import java.util.Set;
 
 /**
  * Java shim for a MessagingBackendService. See
@@ -52,6 +53,17 @@
          * be given to the garbage collector without invoking it first.
          */
         void displayInstantaneousMessage(InstantMessage message, Callback<Boolean> successCallback);
+
+        /**
+         * Invoked when the frontend should hide instant messages. This is intended to be a no-op if
+         * the message is not currently displayed or not in the queue to be displayed. The provided
+         * {@code messageIds} are the IDs of the messages that should be hidden. These correspond to
+         * the {@link MessageAttribution#id id} values from the {@link MessageAttribution} objects
+         * within the {@link InstantMessage#attributions attributions} list of the {@link
+         * InstantMessage} argument originally passed to {@link
+         * #displayInstantaneousMessage(InstantMessage, Callback) displayInstantaneousMessage}.
+         */
+        void hideInstantaneousMessage(Set<String> messageIds);
     }
 
     /** Sets the delegate for instant (one-off) messages. */
diff --git a/components/collaboration/public/messaging/messaging_backend_service.h b/components/collaboration/public/messaging/messaging_backend_service.h
index 55076c99..eb4cf33f 100644
--- a/components/collaboration/public/messaging/messaging_backend_service.h
+++ b/components/collaboration/public/messaging/messaging_backend_service.h
@@ -5,10 +5,13 @@
 #ifndef COMPONENTS_COLLABORATION_PUBLIC_MESSAGING_MESSAGING_BACKEND_SERVICE_H_
 #define COMPONENTS_COLLABORATION_PUBLIC_MESSAGING_MESSAGING_BACKEND_SERVICE_H_
 
+#include <set>
+
 #include "base/functional/callback_forward.h"
 #include "base/observer_list_types.h"
 #include "base/scoped_observation_traits.h"
 #include "base/supports_user_data.h"
+#include "base/uuid.h"
 #include "components/collaboration/public/messaging/activity_log.h"
 #include "components/collaboration/public/messaging/message.h"
 #include "components/keyed_service/core/keyed_service.h"
@@ -52,6 +55,15 @@
     virtual void DisplayInstantaneousMessage(
         InstantMessage message,
         SuccessCallback success_callback) = 0;
+
+    // Invoked when the frontend should hide instant messages.  This is intended
+    // to be a no-op if the message is not currently displayed or not in the
+    // queue to be displayed. The provided message IDs are the IDs of the
+    // messages that should be hidden, and they are the same IDs as the
+    // `InstantMessage::attributions[].id` values from the `InstantMessage`
+    // argument originally passed to `DisplayInstantaneousMessage(..)`.
+    virtual void HideInstantaneousMessage(
+        const std::set<base::Uuid>& message_ids) = 0;
   };
 
   ~MessagingBackendService() override = default;
diff --git a/components/data_sharing/internal/logger_impl.cc b/components/data_sharing/internal/logger_impl.cc
index 7ceb887..b616d48 100644
--- a/components/data_sharing/internal/logger_impl.cc
+++ b/components/data_sharing/internal/logger_impl.cc
@@ -48,6 +48,8 @@
                      const std::string& source_file,
                      int source_line,
                      const std::string& message) {
+  VLOG(1) << log_source << ": " << message;
+
   if (!ShouldEnableDebugLogs()) {
     return;
   }
diff --git a/components/data_sharing/public/logger_utils.h b/components/data_sharing/public/logger_utils.h
index 6d7ae00..c0ff3ab6 100644
--- a/components/data_sharing/public/logger_utils.h
+++ b/components/data_sharing/public/logger_utils.h
@@ -14,7 +14,6 @@
     if (logger && logger->ShouldEnableDebugLogs()) [[unlikely]] {              \
       logger->Log(base::Time::Now(), log_source, __FILE__, __LINE__, message); \
     }                                                                          \
-    VLOG(1) << "Data sharing flow event: " << message;                         \
   } while (0)
 
 #endif  // COMPONENTS_DATA_SHARING_PUBLIC_LOGGER_UTILS_H_
diff --git a/components/embedder_support/android/DEPS b/components/embedder_support/android/DEPS
index 5d1f824..5a702df 100644
--- a/components/embedder_support/android/DEPS
+++ b/components/embedder_support/android/DEPS
@@ -12,6 +12,7 @@
   "+content/public/common",
   "+net/android/java/src/org/chromium/net",
   "+third_party/blink/public/common/context_menu_data/context_menu_data.h",
+  "+third_party/blink/public/mojom/annotation/annotation.mojom-shared.h",
   "+ui/accessibility",
   "+ui/android",
   "+ui/base",
diff --git a/components/embedder_support/android/contextmenu/context_menu_builder.cc b/components/embedder_support/android/contextmenu/context_menu_builder.cc
index 58e1414..6332d3fd 100644
--- a/components/embedder_support/android/contextmenu/context_menu_builder.cc
+++ b/components/embedder_support/android/contextmenu/context_menu_builder.cc
@@ -12,6 +12,7 @@
 #include "content/public/browser/android/impression_android.h"
 #include "content/public/browser/context_menu_params.h"
 #include "third_party/blink/public/common/context_menu_data/context_menu_data.h"
+#include "third_party/blink/public/mojom/annotation/annotation.mojom-shared.h"
 #include "url/android/gurl_android.h"
 
 // Must come after all headers that specialize FromJniType() / ToJniType().
@@ -60,8 +61,10 @@
           url::GURLAndroid::FromNativeGURL(env, sanitizedReferrer),
           static_cast<int>(params.referrer_policy), can_save, params.x,
           params.y, static_cast<int>(params.source_type),
-          params.opened_from_highlight, params.opened_from_interest_target,
-          params.interest_target_node_id, additional_navigation_params));
+          params.annotation_type ==
+              blink::mojom::AnnotationType::kSharedHighlight,
+          params.opened_from_interest_target, params.interest_target_node_id,
+          additional_navigation_params));
 }
 
 content::ContextMenuParams* ContextMenuParamsFromJavaObject(
diff --git a/components/embedder_support/ios/delegate/color_chooser/color_chooser_mediator_ios.h b/components/embedder_support/ios/delegate/color_chooser/color_chooser_mediator_ios.h
index 555681e4..089be36 100644
--- a/components/embedder_support/ios/delegate/color_chooser/color_chooser_mediator_ios.h
+++ b/components/embedder_support/ios/delegate/color_chooser/color_chooser_mediator_ios.h
@@ -26,7 +26,7 @@
         colorChooser;
 
 // Consumer that is configured by this mediator.
-@property(nonatomic, assign) id<ColorChooserConsumerIOS> consumer;
+@property(nonatomic, weak) id<ColorChooserConsumerIOS> consumer;
 
 // Initializer.
 - (instancetype)initWithColorChooser:
diff --git a/components/find_in_page/find_tab_helper.h b/components/find_in_page/find_tab_helper.h
index 87547de1..2575c4cc 100644
--- a/components/find_in_page/find_tab_helper.h
+++ b/components/find_in_page/find_tab_helper.h
@@ -74,6 +74,12 @@
     find_ui_active_ = find_ui_active;
   }
 
+  // Accessors/Setters for find_ui_focused_.
+  bool find_ui_focused() const { return find_ui_focused_; }
+  void set_find_ui_focused(bool find_ui_focused) {
+    find_ui_focused_ = find_ui_focused;
+  }
+
   // Used _only_ by testing to get the current request ID.
   int current_find_request_id() { return current_find_request_id_; }
 
@@ -140,6 +146,9 @@
   // True if the Find UI is active for this Tab.
   bool find_ui_active_ = false;
 
+  // True if the Find UI is focused for this Tab.
+  bool find_ui_focused_ = false;
+
   // True if a Find operation was aborted. This can happen if the Find box is
   // closed or if the search term inside the Find box is erased while a search
   // is in progress. This can also be set if a page has been reloaded, and will
diff --git a/components/ip_protection/common/ip_protection_config_http.cc b/components/ip_protection/common/ip_protection_config_http.cc
index 206ca9c9..a6b2958 100644
--- a/components/ip_protection/common/ip_protection_config_http.cc
+++ b/components/ip_protection/common/ip_protection_config_http.cc
@@ -142,7 +142,7 @@
 void IpProtectionConfigHttp::OnDoRequestCompleted(
     std::unique_ptr<network::SimpleURLLoader> url_loader,
     quiche::BlindSignMessageCallback callback,
-    std::unique_ptr<std::string> response) {
+    std::optional<std::string> response) {
   int response_code = 0;
   if (url_loader->ResponseInfo() && url_loader->ResponseInfo()->headers) {
     response_code = url_loader->ResponseInfo()->headers->response_code();
@@ -156,7 +156,7 @@
     return;
   }
 
-  if (!response) {
+  if (!response.has_value()) {
     std::move(callback)(
         absl::InternalError("Failed Request to Authentication Server"));
     return;
diff --git a/components/ip_protection/common/ip_protection_config_http.h b/components/ip_protection/common/ip_protection_config_http.h
index 7db6704..d434c66 100644
--- a/components/ip_protection/common/ip_protection_config_http.h
+++ b/components/ip_protection/common/ip_protection_config_http.h
@@ -40,7 +40,7 @@
   void OnDoRequestCompleted(
       std::unique_ptr<network::SimpleURLLoader> url_loader,
       quiche::BlindSignMessageCallback callback,
-      std::unique_ptr<std::string> response);
+      std::optional<std::string> response);
   scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory_;
 
   const GURL ip_protection_server_url_;
diff --git a/components/neterror/resources/BUILD.gn b/components/neterror/resources/BUILD.gn
index 172a8e1..c3c4cf66 100644
--- a/components/neterror/resources/BUILD.gn
+++ b/components/neterror/resources/BUILD.gn
@@ -25,7 +25,7 @@
   "dino_game/obstacle.ts",
   "dino_game/offline.js",
   "dino_game/offline_sprite_definitions.ts",
-  "dino_game/trex.js",
+  "dino_game/trex.ts",
   "dino_game/utils.ts",
 ]
 
diff --git a/components/neterror/resources/dino_game/offline.js b/components/neterror/resources/dino_game/offline.js
index c79b432..822372a1 100644
--- a/components/neterror/resources/dino_game/offline.js
+++ b/components/neterror/resources/dino_game/offline.js
@@ -14,7 +14,7 @@
 import {Horizon} from './horizon.js';
 import {Obstacle} from './obstacle.js';
 import {CollisionBox, GAME_TYPE, spriteDefinitionByType} from './offline_sprite_definitions.js';
-import {Trex} from './trex.js';
+import {Status as TrexStatus, Trex} from './trex.js';
 import {getTimeStamp} from './utils.js';
 
 /**
@@ -309,15 +309,15 @@
       this.config[setting] = value;
 
       switch (setting) {
-        case 'GRAVITY':
-        case 'MIN_JUMP_HEIGHT':
-        case 'SPEED_DROP_COEFFICIENT':
+        case 'gravity':
+        case 'minJumpHeight':
+        case 'speedDropCoefficient':
           this.tRex.config[setting] = value;
           break;
-        case 'INITIAL_JUMP_VELOCITY':
+        case 'initialJumpVelocity':
           this.tRex.setJumpVelocity(value);
           break;
-        case 'SPEED':
+        case 'speed':
           this.setSpeed(/** @type {number} */ (value));
           break;
       }
@@ -603,7 +603,7 @@
 
       // CSS animation definition.
       const keyframes = '@-webkit-keyframes intro { ' +
-          'from { width:' + Trex.config.WIDTH + 'px }' +
+          'from { width:' + this.tRex.config.width + 'px }' +
           'to { width: ' + this.dimensions.width + 'px }' +
           '}';
       document.styleSheets[0].insertRule(keyframes, 0);
@@ -751,7 +751,7 @@
       // For a11y, audio cues.
       if (Runner.audioCues && hasObstacles) {
         const jumpObstacle =
-            this.horizon.obstacles[0].typeConfig.type !== 'COLLECTABLE';
+            this.horizon.obstacles[0].typeConfig.type !== 'collectable';
 
         if (!this.horizon.obstacles[0].jumpAlerted) {
           const threshold = Runner.isMobileMouseInput ?
@@ -771,7 +771,7 @@
 
       // Activated alt game mode.
       if (Runner.isAltGameModeEnabled() && collision &&
-          this.horizon.obstacles[0].typeConfig.type === 'COLLECTABLE') {
+          this.horizon.obstacles[0].typeConfig.type === 'collectable') {
         this.horizon.removeFirstObstacle();
         this.tRex.setFlashing(true);
         collision = false;
@@ -1272,7 +1272,7 @@
     this.crashed = true;
     this.distanceMeter.achievement = false;
 
-    this.tRex.update(100, Trex.status.CRASHED);
+    this.tRex.update(100, TrexStatus.CRASHED);
 
     // Game over panel.
     if (!this.gameOverPanel) {
@@ -1339,7 +1339,7 @@
     if (!this.crashed) {
       this.setPlayStatus(true);
       this.paused = false;
-      this.tRex.update(0, Trex.status.RUNNING);
+      this.tRex.update(0, TrexStatus.RUNNING);
       this.time = getTimeStamp();
       this.update();
       if (Runner.audioCues) {
@@ -1635,8 +1635,8 @@
   // Adjustments are made to the bounding box as there is a 1 pixel white
   // border around the t-rex and obstacles.
   const tRexBox = new CollisionBox(
-      tRex.xPos + 1, tRex.yPos + 1, tRex.config.WIDTH - 2,
-      tRex.config.HEIGHT - 2);
+      tRex.xPos + 1, tRex.yPos + 1, tRex.config.width - 2,
+      tRex.config.height - 2);
 
   const obstacleBox = new CollisionBox(
       obstacle.xPos + 1, obstacle.yPos + 1,
@@ -1654,10 +1654,9 @@
     let tRexCollisionBoxes = [];
 
     if (Runner.isAltGameModeEnabled()) {
-      tRexCollisionBoxes = Runner.spriteDefinition.tRex.COLLISION_BOXES;
+      tRexCollisionBoxes = Runner.spriteDefinition.tRex.collisionBoxes;
     } else {
-      tRexCollisionBoxes = tRex.ducking ? Trex.collisionBoxes.DUCKING :
-                                          Trex.collisionBoxes.RUNNING;
+      tRexCollisionBoxes = tRex.getCollisionBoxes();
     }
 
     // Detailed axis aligned box check.
diff --git a/components/neterror/resources/dino_game/offline_sprite_definitions.ts b/components/neterror/resources/dino_game/offline_sprite_definitions.ts
index 2dc4b7a..90e7e5e0 100644
--- a/components/neterror/resources/dino_game/offline_sprite_definitions.ts
+++ b/components/neterror/resources/dino_game/offline_sprite_definitions.ts
@@ -5,6 +5,7 @@
 import type {BackgroundElConfig, BackgroundElSpriteConfig} from './background_el.js';
 import type {HorizonLineConfig} from './horizon_line.js';
 import type {SpritePosition} from './sprite_position.js';
+import type {AltGameModeSpriteConfig as AltTrexSpriteDefinition} from './trex.js';
 
 /*
  * List of alternative game types defined in spriteDefinitionByType.
@@ -79,8 +80,7 @@
   maxObstacleLength: number;
   hasClouds: boolean;
   bottomPad: number;
-  // TODO(crbug.com/373951324): Use type from trex.ts after it gets migrated.
-  tRex: any;
+  tRex?: AltTrexSpriteDefinition;
   obstacles: ObstacleType[];
   backgroundEl: {
     [key: string]: BackgroundElSpriteConfig,
@@ -137,22 +137,6 @@
     maxObstacleLength: 3,
     hasClouds: true,
     bottomPad: 10,
-    tRex: {
-      WAITING_1: {x: 44, w: 44, h: 47, xOffset: 0},
-      WAITING_2: {x: 0, w: 44, h: 47, xOffset: 0},
-      RUNNING_1: {x: 88, w: 44, h: 47, xOffset: 0},
-      RUNNING_2: {x: 132, w: 44, h: 47, xOffset: 0},
-      JUMPING: {x: 0, w: 44, h: 47, xOffset: 0},
-      CRASHED: {x: 220, w: 44, h: 47, xOffset: 0},
-      COLLISION_BOXES: [
-        {x: 22, y: 0, width: 17, height: 16},
-        {x: 1, y: 18, width: 30, height: 9},
-        {x: 10, y: 35, width: 14, height: 8},
-        {x: 1, y: 24, width: 29, height: 5},
-        {x: 5, y: 30, width: 21, height: 4},
-        {x: 9, y: 34, width: 15, height: 4},
-      ],
-    },
     obstacles: [
       {
         type: 'cactusSmall',
diff --git a/components/neterror/resources/dino_game/trex.js b/components/neterror/resources/dino_game/trex.js
deleted file mode 100644
index 8491c99..0000000
--- a/components/neterror/resources/dino_game/trex.js
+++ /dev/null
@@ -1,511 +0,0 @@
-// Copyright 2024 The Chromium Authors
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-import {FPS, IS_HIDPI} from './constants.js';
-import {Runner} from './offline.js';
-import {CollisionBox} from './offline_sprite_definitions.js';
-import {getTimeStamp} from './utils.js';
-
-
-export class Trex {
-  /**
-   * T-rex game character.
-   * @param {HTMLCanvasElement} canvas
-   * @param {Object} spritePos Positioning within image sprite.
-   */
-  constructor(canvas, spritePos) {
-    this.canvas = canvas;
-    this.canvasCtx =
-        /** @type {CanvasRenderingContext2D} */ (canvas.getContext('2d'));
-    this.spritePos = spritePos;
-    this.xPos = 0;
-    this.yPos = 0;
-    this.xInitialPos = 0;
-    // Position when on the ground.
-    this.groundYPos = 0;
-    this.currentFrame = 0;
-    this.currentAnimFrames = [];
-    this.blinkDelay = 0;
-    this.blinkCount = 0;
-    this.animStartTime = 0;
-    this.timer = 0;
-    this.msPerFrame = 1000 / FPS;
-    this.config = Object.assign(Trex.config, Trex.normalJumpConfig);
-    // Current status.
-    this.status = Trex.status.WAITING;
-    this.jumping = false;
-    this.ducking = false;
-    this.jumpVelocity = 0;
-    this.reachedMinHeight = false;
-    this.speedDrop = false;
-    this.jumpCount = 0;
-    this.jumpspotX = 0;
-    this.altGameModeEnabled = false;
-    this.flashing = false;
-
-    this.init();
-  }
-
-
-  /**
-   * T-rex player initialiser.
-   * Sets the t-rex to blink at random intervals.
-   */
-  init() {
-    this.groundYPos = Runner.defaultDimensions.height - this.config.HEIGHT -
-        Runner.config.BOTTOM_PAD;
-    this.yPos = this.groundYPos;
-    this.minJumpHeight = this.groundYPos - this.config.MIN_JUMP_HEIGHT;
-
-    this.draw(0, 0);
-    this.update(0, Trex.status.WAITING);
-  }
-
-  /**
-   * Assign the appropriate jump parameters based on the game speed.
-   */
-  enableSlowConfig() {
-    const jumpConfig =
-        Runner.slowDown ? Trex.slowJumpConfig : Trex.normalJumpConfig;
-    Trex.config = Object.assign(Trex.config, jumpConfig);
-
-    this.adjustAltGameConfigForSlowSpeed();
-  }
-
-  /**
-   * Enables the alternative game. Redefines the dino config.
-   * @param {Object} spritePos New positioning within image sprite.
-   */
-  enableAltGameMode(spritePos) {
-    this.altGameModeEnabled = true;
-    this.spritePos = spritePos;
-    const spriteDefinition = Runner.spriteDefinition['tRex'];
-
-    // Update animation frames.
-    Trex.animFrames.RUNNING.frames =
-        [spriteDefinition.RUNNING_1.x, spriteDefinition.RUNNING_2.x];
-    Trex.animFrames.CRASHED.frames = [spriteDefinition.CRASHED.x];
-
-    if (typeof spriteDefinition.JUMPING.x === 'object') {
-      Trex.animFrames.JUMPING.frames = spriteDefinition.JUMPING.x;
-    } else {
-      Trex.animFrames.JUMPING.frames = [spriteDefinition.JUMPING.x];
-    }
-
-    Trex.animFrames.DUCKING.frames =
-        [spriteDefinition.DUCKING_1.x, spriteDefinition.DUCKING_2.x];
-
-    // Update Trex config
-    Trex.config.GRAVITY = spriteDefinition.GRAVITY || Trex.config.GRAVITY;
-    Trex.config.HEIGHT = spriteDefinition.RUNNING_1.h,
-    Trex.config.INITIAL_JUMP_VELOCITY = spriteDefinition.INITIAL_JUMP_VELOCITY;
-    Trex.config.MAX_JUMP_HEIGHT = spriteDefinition.MAX_JUMP_HEIGHT;
-    Trex.config.MIN_JUMP_HEIGHT = spriteDefinition.MIN_JUMP_HEIGHT;
-    Trex.config.WIDTH = spriteDefinition.RUNNING_1.w;
-    Trex.config.WIDTH_CRASHED = spriteDefinition.CRASHED.w;
-    Trex.config.WIDTH_JUMP = spriteDefinition.JUMPING.w;
-    Trex.config.INVERT_JUMP = spriteDefinition.INVERT_JUMP;
-
-    this.adjustAltGameConfigForSlowSpeed(spriteDefinition.GRAVITY);
-    this.config = Trex.config;
-
-    // Adjust bottom horizon placement.
-    this.groundYPos = Runner.defaultDimensions.height - this.config.HEIGHT -
-        Runner.spriteDefinition['bottomPad'];
-    this.yPos = this.groundYPos;
-    this.reset();
-  }
-
-  /**
-   * Slow speeds adjustments for the alt game modes.
-   * @param {number=} opt_gravityValue
-   */
-  adjustAltGameConfigForSlowSpeed(opt_gravityValue) {
-    if (Runner.slowDown) {
-      if (opt_gravityValue) {
-        Trex.config.GRAVITY = opt_gravityValue / 1.5;
-      }
-      Trex.config.MIN_JUMP_HEIGHT *= 1.5;
-      Trex.config.MAX_JUMP_HEIGHT *= 1.5;
-      Trex.config.INITIAL_JUMP_VELOCITY =
-          Trex.config.INITIAL_JUMP_VELOCITY * 1.5;
-    }
-  }
-
-  /**
-   * Setter whether dino is flashing.
-   * @param {boolean} status
-   */
-  setFlashing(status) {
-    this.flashing = status;
-  }
-
-  /**
-   * Setter for the jump velocity.
-   * The appropriate drop velocity is also set.
-   * @param {number} setting
-   */
-  setJumpVelocity(setting) {
-    this.config.INITIAL_JUMP_VELOCITY = -setting;
-    this.config.DROP_VELOCITY = -setting / 2;
-  }
-
-  /**
-   * Set the animation status.
-   * @param {!number} deltaTime
-   * @param {Trex.status=} opt_status Optional status to switch to.
-   */
-  update(deltaTime, opt_status) {
-    this.timer += deltaTime;
-
-    // Update the status.
-    if (opt_status) {
-      this.status = opt_status;
-      this.currentFrame = 0;
-      this.msPerFrame = Trex.animFrames[opt_status].msPerFrame;
-      this.currentAnimFrames = Trex.animFrames[opt_status].frames;
-
-      if (opt_status === Trex.status.WAITING) {
-        this.animStartTime = getTimeStamp();
-        this.setBlinkDelay();
-      }
-    }
-    // Game intro animation, T-rex moves in from the left.
-    if (this.playingIntro && this.xPos < this.config.START_X_POS) {
-      this.xPos += Math.round(
-          (this.config.START_X_POS / this.config.INTRO_DURATION) * deltaTime);
-      this.xInitialPos = this.xPos;
-    }
-
-    if (this.status === Trex.status.WAITING) {
-      this.blink(getTimeStamp());
-    } else {
-      this.draw(this.currentAnimFrames[this.currentFrame], 0);
-    }
-
-    // Update the frame position.
-    if (!this.flashing && this.timer >= this.msPerFrame) {
-      this.currentFrame =
-          this.currentFrame === this.currentAnimFrames.length - 1 ?
-          0 :
-          this.currentFrame + 1;
-      this.timer = 0;
-    }
-
-    // Speed drop becomes duck if the down key is still being pressed.
-    if (this.speedDrop && this.yPos === this.groundYPos) {
-      this.speedDrop = false;
-      this.setDuck(true);
-    }
-  }
-
-  /**
-   * Draw the t-rex to a particular position.
-   * @param {number} x
-   * @param {number} y
-   */
-  draw(x, y) {
-    let sourceX = x;
-    let sourceY = y;
-    let sourceWidth = this.ducking && this.status !== Trex.status.CRASHED ?
-        this.config.WIDTH_DUCK :
-        this.config.WIDTH;
-    let sourceHeight = this.config.HEIGHT;
-    const outputHeight = sourceHeight;
-    const outputWidth =
-        this.altGameModeEnabled && this.status === Trex.status.CRASHED ?
-        this.config.WIDTH_CRASHED :
-        this.config.WIDTH;
-
-    let jumpOffset = Runner.spriteDefinition.tRex.JUMPING.xOffset;
-
-    // Width of sprite can change on jump or crashed.
-    if (this.altGameModeEnabled) {
-      if (this.jumping && this.status !== Trex.status.CRASHED) {
-        sourceWidth = this.config.WIDTH_JUMP;
-      } else if (this.status === Trex.status.CRASHED) {
-        sourceWidth = this.config.WIDTH_CRASHED;
-      }
-    }
-
-    if (IS_HIDPI) {
-      sourceX *= 2;
-      sourceY *= 2;
-      sourceWidth *= 2;
-      sourceHeight *= 2;
-      jumpOffset *= 2;
-    }
-
-    // Adjustments for sprite sheet position.
-    sourceX += this.spritePos.x;
-    sourceY += this.spritePos.y;
-
-    // Flashing.
-    if (this.flashing) {
-      if (this.timer < this.config.FLASH_ON) {
-        this.canvasCtx.globalAlpha = 0.5;
-      } else if (this.timer > this.config.FLASH_OFF) {
-        this.timer = 0;
-      }
-    }
-
-    // Ducking.
-    if (this.ducking && this.status !== Trex.status.CRASHED) {
-      this.canvasCtx.drawImage(
-          Runner.imageSprite, sourceX, sourceY, sourceWidth, sourceHeight,
-          this.xPos, this.yPos, this.config.WIDTH_DUCK, outputHeight);
-    } else if (
-        this.altGameModeEnabled && this.jumping &&
-        this.status !== Trex.status.CRASHED) {
-      // Jumping with adjustments.
-      this.canvasCtx.drawImage(
-          Runner.imageSprite, sourceX, sourceY, sourceWidth, sourceHeight,
-          this.xPos - jumpOffset, this.yPos, this.config.WIDTH_JUMP,
-          outputHeight);
-    } else {
-      // Crashed whilst ducking. Trex is standing up so needs adjustment.
-      if (this.ducking && this.status === Trex.status.CRASHED) {
-        this.xPos++;
-      }
-      // Standing / running
-      this.canvasCtx.drawImage(
-          Runner.imageSprite, sourceX, sourceY, sourceWidth, sourceHeight,
-          this.xPos, this.yPos, outputWidth, outputHeight);
-    }
-    this.canvasCtx.globalAlpha = 1;
-  }
-
-  /**
-   * Sets a random time for the blink to happen.
-   */
-  setBlinkDelay() {
-    this.blinkDelay = Math.ceil(Math.random() * Trex.BLINK_TIMING);
-  }
-
-  /**
-   * Make t-rex blink at random intervals.
-   * @param {number} time Current time in milliseconds.
-   */
-  blink(time) {
-    const deltaTime = time - this.animStartTime;
-
-    if (deltaTime >= this.blinkDelay) {
-      this.draw(this.currentAnimFrames[this.currentFrame], 0);
-
-      if (this.currentFrame === 1) {
-        // Set new random delay to blink.
-        this.setBlinkDelay();
-        this.animStartTime = time;
-        this.blinkCount++;
-      }
-    }
-  }
-
-  /**
-   * Initialise a jump.
-   * @param {number} speed
-   */
-  startJump(speed) {
-    if (!this.jumping) {
-      this.update(0, Trex.status.JUMPING);
-      // Tweak the jump velocity based on the speed.
-      this.jumpVelocity = this.config.INITIAL_JUMP_VELOCITY - (speed / 10);
-      this.jumping = true;
-      this.reachedMinHeight = false;
-      this.speedDrop = false;
-
-      if (this.config.INVERT_JUMP) {
-        this.minJumpHeight = this.groundYPos + this.config.MIN_JUMP_HEIGHT;
-      }
-    }
-  }
-
-  /**
-   * Jump is complete, falling down.
-   */
-  endJump() {
-    if (this.reachedMinHeight &&
-        this.jumpVelocity < this.config.DROP_VELOCITY) {
-      this.jumpVelocity = this.config.DROP_VELOCITY;
-    }
-  }
-
-  /**
-   * Update frame for a jump.
-   * @param {number} deltaTime
-   */
-  updateJump(deltaTime) {
-    const msPerFrame = Trex.animFrames[this.status].msPerFrame;
-    const framesElapsed = deltaTime / msPerFrame;
-
-    // Speed drop makes Trex fall faster.
-    if (this.speedDrop) {
-      this.yPos += Math.round(
-          this.jumpVelocity * this.config.SPEED_DROP_COEFFICIENT *
-          framesElapsed);
-    } else if (this.config.INVERT_JUMP) {
-      this.yPos -= Math.round(this.jumpVelocity * framesElapsed);
-    } else {
-      this.yPos += Math.round(this.jumpVelocity * framesElapsed);
-    }
-
-    this.jumpVelocity += this.config.GRAVITY * framesElapsed;
-
-    // Minimum height has been reached.
-    if (this.config.INVERT_JUMP && (this.yPos > this.minJumpHeight) ||
-        !this.config.INVERT_JUMP && (this.yPos < this.minJumpHeight) ||
-        this.speedDrop) {
-      this.reachedMinHeight = true;
-    }
-
-    // Reached max height.
-    if (this.config.INVERT_JUMP && (this.yPos > -this.config.MAX_JUMP_HEIGHT) ||
-        !this.config.INVERT_JUMP && (this.yPos < this.config.MAX_JUMP_HEIGHT) ||
-        this.speedDrop) {
-      this.endJump();
-    }
-
-    // Back down at ground level. Jump completed.
-    if ((this.config.INVERT_JUMP && this.yPos) < this.groundYPos ||
-        (!this.config.INVERT_JUMP && this.yPos) > this.groundYPos) {
-      this.reset();
-      this.jumpCount++;
-
-      if (Runner.audioCues) {
-        Runner.generatedSoundFx.loopFootSteps();
-      }
-    }
-  }
-
-  /**
-   * Set the speed drop. Immediately cancels the current jump.
-   */
-  setSpeedDrop() {
-    this.speedDrop = true;
-    this.jumpVelocity = 1;
-  }
-
-  /**
-   * @param {boolean} isDucking
-   */
-  setDuck(isDucking) {
-    if (isDucking && this.status !== Trex.status.DUCKING) {
-      this.update(0, Trex.status.DUCKING);
-      this.ducking = true;
-    } else if (this.status === Trex.status.DUCKING) {
-      this.update(0, Trex.status.RUNNING);
-      this.ducking = false;
-    }
-  }
-
-  /**
-   * Reset the t-rex to running at start of game.
-   */
-  reset() {
-    this.xPos = this.xInitialPos;
-    this.yPos = this.groundYPos;
-    this.jumpVelocity = 0;
-    this.jumping = false;
-    this.ducking = false;
-    this.update(0, Trex.status.RUNNING);
-    this.midair = false;
-    this.speedDrop = false;
-    this.jumpCount = 0;
-  }
-}
-
-
-/**
- * T-rex player config.
- */
-Trex.config = {
-  DROP_VELOCITY: -5,
-  FLASH_OFF: 175,
-  FLASH_ON: 100,
-  HEIGHT: 47,
-  HEIGHT_DUCK: 25,
-  INTRO_DURATION: 1500,
-  SPEED_DROP_COEFFICIENT: 3,
-  SPRITE_WIDTH: 262,
-  START_X_POS: 50,
-  WIDTH: 44,
-  WIDTH_DUCK: 59,
-};
-
-Trex.slowJumpConfig = {
-  GRAVITY: 0.25,
-  MAX_JUMP_HEIGHT: 50,
-  MIN_JUMP_HEIGHT: 45,
-  INITIAL_JUMP_VELOCITY: -20,
-};
-
-Trex.normalJumpConfig = {
-  GRAVITY: 0.6,
-  MAX_JUMP_HEIGHT: 30,
-  MIN_JUMP_HEIGHT: 30,
-  INITIAL_JUMP_VELOCITY: -10,
-};
-
-/**
- * Used in collision detection.
- * @enum {Array<CollisionBox>}
- */
-Trex.collisionBoxes = {
-  DUCKING: [new CollisionBox(1, 18, 55, 25)],
-  RUNNING: [
-    new CollisionBox(22, 0, 17, 16),
-    new CollisionBox(1, 18, 30, 9),
-    new CollisionBox(10, 35, 14, 8),
-    new CollisionBox(1, 24, 29, 5),
-    new CollisionBox(5, 30, 21, 4),
-    new CollisionBox(9, 34, 15, 4),
-  ],
-};
-
-
-/**
- * Animation states.
- * @enum {string}
- */
-Trex.status = {
-  CRASHED: 'CRASHED',
-  DUCKING: 'DUCKING',
-  JUMPING: 'JUMPING',
-  RUNNING: 'RUNNING',
-  WAITING: 'WAITING',
-};
-
-/**
- * Blinking coefficient.
- * @const
- */
-Trex.BLINK_TIMING = 7000;
-
-
-/**
- * Animation config for different states.
- * @enum {Object}
- */
-Trex.animFrames = {
-  WAITING: {
-    frames: [44, 0],
-    msPerFrame: 1000 / 3,
-  },
-  RUNNING: {
-    frames: [88, 132],
-    msPerFrame: 1000 / 12,
-  },
-  CRASHED: {
-    frames: [220],
-    msPerFrame: 1000 / 60,
-  },
-  JUMPING: {
-    frames: [0],
-    msPerFrame: 1000 / 60,
-  },
-  DUCKING: {
-    frames: [264, 323],
-    msPerFrame: 1000 / 8,
-  },
-};
\ No newline at end of file
diff --git a/components/neterror/resources/dino_game/trex.ts b/components/neterror/resources/dino_game/trex.ts
new file mode 100644
index 0000000..c3e7d0e8
--- /dev/null
+++ b/components/neterror/resources/dino_game/trex.ts
@@ -0,0 +1,561 @@
+// Copyright 2024 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {assert} from 'chrome://resources/js/assert.js';
+
+import {FPS, IS_HIDPI} from './constants.js';
+import {CollisionBox} from './offline_sprite_definitions.js';
+import type {SpritePosition} from './sprite_position.js';
+import {getRunnerAudioCues, getRunnerConfigValue, getRunnerDefaultDimensions, getRunnerGeneratedSoundFx, getRunnerImageSprite, getRunnerSlowdown, getRunnerSpriteDefinition, getTimeStamp} from './utils.js';
+
+
+interface BaseTrexConfig {
+  dropVelocity: number;
+  flashOff: number;
+  flashOn: number;
+  height: number;
+  heightDuck: number;
+  introDuration: number;
+  speedDropCoefficient: number;
+  spriteWidth: number;
+  startXPos: number;
+  invertJump: boolean;
+  width: number;
+  widthDuck: number;
+  widthCrashed?: number;
+  widthJump?: number;
+}
+
+interface TrexJumpConfig {
+  gravity: number;
+  maxJumpHeight: number;
+  minJumpHeight: number;
+  initialJumpVelocity: number;
+}
+
+type TrexConfig = BaseTrexConfig&TrexJumpConfig;
+
+interface TrexSpritePosition {
+  x: number;
+  w: number;
+  h: number;
+  xOffset: number;
+}
+
+// Sprite config for alternative game modes.
+export type AltGameModeSpriteConfig = TrexConfig&{
+  jumping: TrexSpritePosition,
+  crashed: TrexSpritePosition,
+  running1: TrexSpritePosition,
+  running2: TrexSpritePosition,
+  ducking1: TrexSpritePosition,
+  ducking2: TrexSpritePosition,
+  collisionBoxes: CollisionBox[],
+};
+
+/**
+ * T-rex player config.
+ */
+const defaultTrexConfig: BaseTrexConfig = {
+  dropVelocity: -5,
+  flashOff: 175,
+  flashOn: 100,
+  height: 47,
+  heightDuck: 25,
+  introDuration: 1500,
+  speedDropCoefficient: 3,
+  spriteWidth: 262,
+  startXPos: 50,
+  width: 44,
+  widthDuck: 59,
+  invertJump: false,
+};
+
+const slowJumpConfig: TrexJumpConfig = {
+  gravity: 0.25,
+  maxJumpHeight: 50,
+  minJumpHeight: 45,
+  initialJumpVelocity: -20,
+};
+
+const normalJumpConfig: TrexJumpConfig = {
+  gravity: 0.6,
+  maxJumpHeight: 30,
+  minJumpHeight: 30,
+  initialJumpVelocity: -10,
+};
+
+/**
+ * Used in collision detection.
+ */
+const collisionBoxes: {ducking: CollisionBox[], running: CollisionBox[]} = {
+  ducking: [new CollisionBox(1, 18, 55, 25)],
+  running: [
+    new CollisionBox(22, 0, 17, 16),
+    new CollisionBox(1, 18, 30, 9),
+    new CollisionBox(10, 35, 14, 8),
+    new CollisionBox(1, 24, 29, 5),
+    new CollisionBox(5, 30, 21, 4),
+    new CollisionBox(9, 34, 15, 4),
+  ],
+};
+
+export enum Status {
+  CRASHED,
+  DUCKING,
+  JUMPING,
+  RUNNING,
+  WAITING,
+}
+
+/**
+ * Blinking coefficient.
+ */
+const BLINK_TIMING: number = 7000;
+
+interface FrameInfo {
+  frames: number[];
+  msPerFrame: number;
+}
+
+/**
+ * Animation config for different states.
+ */
+const animFrames: Record<Status, FrameInfo> = {
+  [Status.WAITING]: {
+    frames: [44, 0],
+    msPerFrame: 1000 / 3,
+  },
+  [Status.RUNNING]: {
+    frames: [88, 132],
+    msPerFrame: 1000 / 12,
+  },
+  [Status.CRASHED]: {
+    frames: [220],
+    msPerFrame: 1000 / 60,
+  },
+  [Status.JUMPING]: {
+    frames: [0],
+    msPerFrame: 1000 / 60,
+  },
+  [Status.DUCKING]: {
+    frames: [264, 323],
+    msPerFrame: 1000 / 8,
+  },
+};
+
+export class Trex {
+  config: TrexConfig;
+  playingIntro: boolean = false;
+  xPos: number = 0;
+  yPos: number = 0;
+  jumpCount: number = 0;
+  ducking: boolean = false;
+  blinkCount: number = 0;
+  jumping: boolean = false;
+  speedDrop: boolean = false;
+
+  private canvasCtx: CanvasRenderingContext2D;
+  private spritePos: SpritePosition;
+  private xInitialPos: number = 0;
+  // Position when on the ground.
+  private groundYPos: number = 0;
+  private currentFrame: number = 0;
+  private currentAnimFrames: number[] = [];
+  private blinkDelay: number = 0;
+  private animStartTime: number = 0;
+  private timer: number = 0;
+  private msPerFrame: number = 1000 / FPS;
+  // Current status.
+  private status: Status = Status.WAITING;
+  private jumpVelocity: number = 0;
+  private reachedMinHeight: boolean = false;
+  private altGameModeEnabled: boolean = false;
+  private flashing: boolean = false;
+  private minJumpHeight: number;
+
+
+  /**
+   * T-rex game character.
+   */
+  constructor(canvas: HTMLCanvasElement, spritePos: SpritePosition) {
+    const canvasContext = canvas.getContext('2d');
+    assert(canvasContext);
+    this.canvasCtx = canvasContext;
+    this.spritePos = spritePos;
+    this.config = Object.assign(defaultTrexConfig, normalJumpConfig);
+
+    const runnerDefaultDimensions = getRunnerDefaultDimensions();
+    const runnerBottomPadding = getRunnerConfigValue('BOTTOM_PAD');
+    assert(runnerDefaultDimensions);
+    assert(runnerBottomPadding);
+    this.groundYPos = runnerDefaultDimensions.height - this.config.height -
+        runnerBottomPadding;
+
+    this.yPos = this.groundYPos;
+    this.minJumpHeight = this.groundYPos - this.config.minJumpHeight;
+
+    this.draw(0, 0);
+    this.update(0, Status.WAITING);
+  }
+
+  /**
+   * Assign the appropriate jump parameters based on the game speed.
+   */
+  enableSlowConfig() {
+    const jumpConfig = getRunnerSlowdown() ? slowJumpConfig : normalJumpConfig;
+    this.config = Object.assign(defaultTrexConfig, jumpConfig);
+
+    this.adjustAltGameConfigForSlowSpeed();
+  }
+
+  /**
+   * Enables the alternative game. Redefines the dino config.
+   * @param spritePos New positioning within image sprite.
+   */
+  enableAltGameMode(spritePos: SpritePosition) {
+    this.altGameModeEnabled = true;
+    this.spritePos = spritePos;
+    const spriteDefinition = getRunnerSpriteDefinition();
+    assert(spriteDefinition);
+    const tRexSpriteDefinition =
+        spriteDefinition.tRex as AltGameModeSpriteConfig;
+    assert(tRexSpriteDefinition.running1);
+    const runnerDefaultDimensions = getRunnerDefaultDimensions();
+    assert(runnerDefaultDimensions);
+
+
+    // Update animation frames.
+    animFrames[Status.RUNNING].frames =
+        [tRexSpriteDefinition.running1.x, tRexSpriteDefinition.running2.x];
+    animFrames[Status.CRASHED].frames = [tRexSpriteDefinition.crashed.x];
+
+    if (typeof tRexSpriteDefinition.jumping.x === 'object') {
+      animFrames[Status.JUMPING].frames = tRexSpriteDefinition.jumping.x;
+    } else {
+      animFrames[Status.JUMPING].frames = [tRexSpriteDefinition.jumping.x];
+    }
+
+    animFrames[Status.DUCKING].frames =
+        [tRexSpriteDefinition.ducking1.x, tRexSpriteDefinition.ducking2.x];
+
+    // Update Trex config
+    this.config.gravity = tRexSpriteDefinition.gravity || this.config.gravity;
+    this.config.height = tRexSpriteDefinition.running1.h,
+    this.config.initialJumpVelocity = tRexSpriteDefinition.initialJumpVelocity;
+    this.config.maxJumpHeight = tRexSpriteDefinition.maxJumpHeight;
+    this.config.minJumpHeight = tRexSpriteDefinition.minJumpHeight;
+    this.config.width = tRexSpriteDefinition.running1.w;
+    this.config.widthCrashed = tRexSpriteDefinition.crashed.w;
+    this.config.widthJump = tRexSpriteDefinition.jumping.w;
+    this.config.invertJump = tRexSpriteDefinition.invertJump;
+
+    this.adjustAltGameConfigForSlowSpeed(tRexSpriteDefinition.gravity);
+
+    // Adjust bottom horizon placement.
+    this.groundYPos = runnerDefaultDimensions.height - this.config.height -
+        spriteDefinition.bottomPad;
+    this.yPos = this.groundYPos;
+    this.reset();
+  }
+
+  /**
+   * Slow speeds adjustments for the alt game modes.
+   */
+  private adjustAltGameConfigForSlowSpeed(gravityValue?: number) {
+    if (getRunnerSlowdown()) {
+      if (gravityValue) {
+        this.config.gravity = gravityValue / 1.5;
+      }
+      this.config.minJumpHeight *= 1.5;
+      this.config.maxJumpHeight *= 1.5;
+      this.config.initialJumpVelocity *= 1.5;
+    }
+  }
+
+  /**
+   * Setter whether dino is flashing.
+   */
+  setFlashing(status: boolean) {
+    this.flashing = status;
+  }
+
+  /**
+   * Setter for the jump velocity.
+   * The appropriate drop velocity is also set.
+   */
+  setJumpVelocity(setting: number) {
+    this.config.initialJumpVelocity = -setting;
+    this.config.dropVelocity = -setting / 2;
+  }
+
+  /**
+   * Set the animation status.
+   */
+  update(deltaTime: number, status?: Status) {
+    this.timer += deltaTime;
+
+    // Update the status.
+    if (status) {
+      this.status = status;
+      this.currentFrame = 0;
+      this.msPerFrame = animFrames[status].msPerFrame;
+      this.currentAnimFrames = animFrames[status].frames;
+
+      if (status === Status.WAITING) {
+        this.animStartTime = getTimeStamp();
+        this.setBlinkDelay();
+      }
+    }
+    // Game intro animation, T-rex moves in from the left.
+    if (this.playingIntro && this.xPos < this.config.startXPos) {
+      this.xPos += Math.round(
+          (this.config.startXPos / this.config.introDuration) * deltaTime);
+      this.xInitialPos = this.xPos;
+    }
+
+    if (this.status === Status.WAITING) {
+      this.blink(getTimeStamp());
+    } else {
+      this.draw(this.currentAnimFrames[this.currentFrame]!, 0);
+    }
+
+    // Update the frame position.
+    if (!this.flashing && this.timer >= this.msPerFrame) {
+      this.currentFrame =
+          this.currentFrame === this.currentAnimFrames.length - 1 ?
+          0 :
+          this.currentFrame + 1;
+      this.timer = 0;
+    }
+
+    // Speed drop becomes duck if the down key is still being pressed.
+    if (this.speedDrop && this.yPos === this.groundYPos) {
+      this.speedDrop = false;
+      this.setDuck(true);
+    }
+  }
+
+  /**
+   * Draw the t-rex to a particular position.
+   */
+  draw(x: number, y: number) {
+    let sourceX = x;
+    let sourceY = y;
+    let sourceWidth = this.ducking && this.status !== Status.CRASHED ?
+        this.config.widthDuck :
+        this.config.width;
+    let sourceHeight = this.config.height;
+    const outputHeight = sourceHeight;
+    if (this.altGameModeEnabled) {
+      assert(this.config.widthCrashed);
+    }
+    const outputWidth =
+        this.altGameModeEnabled && this.status === Status.CRASHED ?
+        this.config.widthCrashed! :
+        this.config.width;
+
+    const runnerImageSprite = getRunnerImageSprite();
+    assert(runnerImageSprite);
+
+
+    // Width of sprite can change on jump or crashed.
+    if (this.altGameModeEnabled) {
+      if (this.jumping && this.status !== Status.CRASHED) {
+        assert(this.config.widthJump);
+        sourceWidth = this.config.widthJump;
+      } else if (this.status === Status.CRASHED) {
+        assert(this.config.widthCrashed);
+        sourceWidth = this.config.widthCrashed;
+      }
+    }
+
+    if (IS_HIDPI) {
+      sourceX *= 2;
+      sourceY *= 2;
+      sourceWidth *= 2;
+      sourceHeight *= 2;
+    }
+
+    // Adjustments for sprite sheet position.
+    sourceX += this.spritePos.x;
+    sourceY += this.spritePos.y;
+
+    // Flashing.
+    if (this.flashing) {
+      if (this.timer < this.config.flashOn) {
+        this.canvasCtx.globalAlpha = 0.5;
+      } else if (this.timer > this.config.flashOff) {
+        this.timer = 0;
+      }
+    }
+
+    // Ducking.
+    if (this.ducking && this.status !== Status.CRASHED) {
+      this.canvasCtx.drawImage(
+          runnerImageSprite, sourceX, sourceY, sourceWidth, sourceHeight,
+          this.xPos, this.yPos, this.config.widthDuck, outputHeight);
+    } else if (
+        this.altGameModeEnabled && this.jumping &&
+        this.status !== Status.CRASHED) {
+      assert(this.config.widthJump);
+      const spriteDefinition = getRunnerSpriteDefinition();
+      assert(spriteDefinition);
+      assert(spriteDefinition.tRex);
+      const jumpOffset =
+          spriteDefinition.tRex.jumping.xOffset * (IS_HIDPI ? 2 : 1);
+      // Jumping with adjustments.
+      this.canvasCtx.drawImage(
+          runnerImageSprite, sourceX, sourceY, sourceWidth, sourceHeight,
+          this.xPos - jumpOffset, this.yPos, this.config.widthJump,
+          outputHeight);
+    } else {
+      // Crashed whilst ducking. Trex is standing up so needs adjustment.
+      if (this.ducking && this.status === Status.CRASHED) {
+        this.xPos++;
+      }
+      // Standing / running
+      this.canvasCtx.drawImage(
+          runnerImageSprite, sourceX, sourceY, sourceWidth, sourceHeight,
+          this.xPos, this.yPos, outputWidth, outputHeight);
+    }
+    this.canvasCtx.globalAlpha = 1;
+  }
+
+  /**
+   * Sets a random time for the blink to happen.
+   */
+  private setBlinkDelay() {
+    this.blinkDelay = Math.ceil(Math.random() * BLINK_TIMING);
+  }
+
+  /**
+   * Make t-rex blink at random intervals.
+   * @param time Current time in milliseconds.
+   */
+  private blink(time: number) {
+    const deltaTime = time - this.animStartTime;
+
+    if (deltaTime >= this.blinkDelay) {
+      this.draw(this.currentAnimFrames[this.currentFrame]!, 0);
+
+      if (this.currentFrame === 1) {
+        // Set new random delay to blink.
+        this.setBlinkDelay();
+        this.animStartTime = time;
+        this.blinkCount++;
+      }
+    }
+  }
+
+  /**
+   * Initialise a jump.
+   */
+  startJump(speed: number) {
+    if (!this.jumping) {
+      this.update(0, Status.JUMPING);
+      // Tweak the jump velocity based on the speed.
+      this.jumpVelocity = this.config.initialJumpVelocity - (speed / 10);
+      this.jumping = true;
+      this.reachedMinHeight = false;
+      this.speedDrop = false;
+
+      if (this.config.invertJump) {
+        this.minJumpHeight = this.groundYPos + this.config.minJumpHeight;
+      }
+    }
+  }
+
+  /**
+   * Jump is complete, falling down.
+   */
+  endJump() {
+    if (this.reachedMinHeight && this.jumpVelocity < this.config.dropVelocity) {
+      this.jumpVelocity = this.config.dropVelocity;
+    }
+  }
+
+  /**
+   * Update frame for a jump.
+   */
+  updateJump(deltaTime: number) {
+    const msPerFrame = animFrames[this.status].msPerFrame;
+    const framesElapsed = deltaTime / msPerFrame;
+
+    // Speed drop makes Trex fall faster.
+    if (this.speedDrop) {
+      this.yPos += Math.round(
+          this.jumpVelocity * this.config.speedDropCoefficient * framesElapsed);
+    } else if (this.config.invertJump) {
+      this.yPos -= Math.round(this.jumpVelocity * framesElapsed);
+    } else {
+      this.yPos += Math.round(this.jumpVelocity * framesElapsed);
+    }
+
+    this.jumpVelocity += this.config.gravity * framesElapsed;
+
+    // Minimum height has been reached.
+    if (this.config.invertJump && (this.yPos > this.minJumpHeight) ||
+        !this.config.invertJump && (this.yPos < this.minJumpHeight) ||
+        this.speedDrop) {
+      this.reachedMinHeight = true;
+    }
+
+    // Reached max height.
+    if (this.config.invertJump && (this.yPos > -this.config.maxJumpHeight) ||
+        !this.config.invertJump && (this.yPos < this.config.maxJumpHeight) ||
+        this.speedDrop) {
+      this.endJump();
+    }
+
+    // Back down at ground level. Jump completed.
+    if ((this.config.invertJump && (this.yPos < this.groundYPos)) ||
+        (!this.config.invertJump && (this.yPos > this.groundYPos))) {
+      this.reset();
+      this.jumpCount++;
+
+      if (getRunnerAudioCues()) {
+        const generatedSoundFx = getRunnerGeneratedSoundFx();
+        assert(generatedSoundFx);
+        generatedSoundFx.loopFootSteps();
+      }
+    }
+  }
+
+  /**
+   * Set the speed drop. Immediately cancels the current jump.
+   */
+  setSpeedDrop() {
+    this.speedDrop = true;
+    this.jumpVelocity = 1;
+  }
+
+  setDuck(isDucking: boolean) {
+    if (isDucking && this.status !== Status.DUCKING) {
+      this.update(0, Status.DUCKING);
+      this.ducking = true;
+    } else if (this.status === Status.DUCKING) {
+      this.update(0, Status.RUNNING);
+      this.ducking = false;
+    }
+  }
+
+  /**
+   * Reset the t-rex to running at start of game.
+   */
+  reset() {
+    this.xPos = this.xInitialPos;
+    this.yPos = this.groundYPos;
+    this.jumpVelocity = 0;
+    this.jumping = false;
+    this.ducking = false;
+    this.update(0, Status.RUNNING);
+    this.speedDrop = false;
+    this.jumpCount = 0;
+  }
+
+  getCollisionBoxes(): CollisionBox[] {
+    return this.ducking ? collisionBoxes.ducking : collisionBoxes.running;
+  }
+}
diff --git a/components/neterror/resources/dino_game/utils.ts b/components/neterror/resources/dino_game/utils.ts
index 755eaff..418c483 100644
--- a/components/neterror/resources/dino_game/utils.ts
+++ b/components/neterror/resources/dino_game/utils.ts
@@ -3,7 +3,10 @@
 // found in the LICENSE file.
 
 import {IS_IOS} from './constants.js';
+import type {Dimensions} from './dimensions.js';
+import type {GeneratedSoundFx} from './generated_sound_fx.js';
 import {Runner} from './offline.js';
+import type {SpriteDefinition} from './offline_sprite_definitions.js';
 
 
 export function getRandomNum(min: number, max: number): number {
@@ -64,3 +67,32 @@
   }
   return null;
 }
+
+export function getRunnerSpriteDefinition(): SpriteDefinition|null {
+  if ('spriteDefinition' in Runner) {
+    return Runner.spriteDefinition as SpriteDefinition;
+  }
+  return null;
+}
+
+export function getRunnerDefaultDimensions(): Dimensions|null {
+  if ('defaultDimensions' in Runner) {
+    return Runner.defaultDimensions as Dimensions;
+  }
+  return null;
+}
+
+export function getRunnerConfigValue(
+    key: 'BOTTOM_PAD'|'MAX_OBSTACLE_DUPLICATION'): number|null {
+  if ('config' in Runner && Runner.config && key in Runner.config) {
+    return Runner.config[key];
+  }
+  return null;
+}
+
+export function getRunnerGeneratedSoundFx(): GeneratedSoundFx|null {
+  if ('generatedSoundFx' in Runner) {
+    return Runner.generatedSoundFx as GeneratedSoundFx;
+  }
+  return null;
+}
diff --git a/components/omnibox/browser/BUILD.gn b/components/omnibox/browser/BUILD.gn
index 8312adb..0afdf8b 100644
--- a/components/omnibox/browser/BUILD.gn
+++ b/components/omnibox/browser/BUILD.gn
@@ -100,7 +100,6 @@
     "tab.icon",
     "trending_up.icon",
     "trending_up_chrome_refresh.icon",
-    "vertical_bar.icon",
   ]
 
   if (is_mac) {
diff --git a/components/omnibox/browser/actions/contextual_search_action.cc b/components/omnibox/browser/actions/contextual_search_action.cc
index 8e29b00..11d9d50b 100644
--- a/components/omnibox/browser/actions/contextual_search_action.cc
+++ b/components/omnibox/browser/actions/contextual_search_action.cc
@@ -41,64 +41,6 @@
 
 ////////////////////////////////////////////////////////////////////////////////
 
-ContextualSearchAskAboutPageAction::ContextualSearchAskAboutPageAction()
-    : OmniboxAction(
-          OmniboxAction::LabelStrings(u"Ask about this page", u"", u"", u""),
-          GURL()) {}
-
-OmniboxActionId ContextualSearchAskAboutPageAction::ActionId() const {
-  return OmniboxActionId::CONTEXTUAL_SEARCH_ASK_ABOUT_PAGE;
-}
-
-void ContextualSearchAskAboutPageAction::Execute(
-    ExecutionContext& context) const {
-  context.client_->OpenLensOverlay(/*show=*/false);
-}
-
-#if defined(SUPPORT_PEDALS_VECTOR_ICONS)
-const gfx::VectorIcon& ContextualSearchSelectRegionAction::GetVectorIcon()
-    const {
-#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
-  return vector_icons::kGoogleLensMonochromeLogoIcon;
-#else
-  return vector_icons::kSearchChromeRefreshIcon;
-#endif
-}
-#endif  // defined(SUPPORT_PEDALS_VECTOR_ICONS)
-
-ContextualSearchAskAboutPageAction::~ContextualSearchAskAboutPageAction() =
-    default;
-
-////////////////////////////////////////////////////////////////////////////////
-
-ContextualSearchSelectRegionAction::ContextualSearchSelectRegionAction()
-    : OmniboxAction(OmniboxAction::LabelStrings(u"Search with Google Lens",
-                                                u"",
-                                                u"",
-                                                u""),
-                    GURL()) {}
-
-OmniboxActionId ContextualSearchSelectRegionAction::ActionId() const {
-  return OmniboxActionId::CONTEXTUAL_SEARCH_SELECT_REGION;
-}
-
-void ContextualSearchSelectRegionAction::Execute(
-    ExecutionContext& context) const {
-  context.client_->OpenLensOverlay(/*show=*/true);
-}
-
-#if defined(SUPPORT_PEDALS_VECTOR_ICONS)
-const gfx::VectorIcon& ContextualSearchAskAboutPageAction::GetVectorIcon()
-    const {
-  return omnibox::kPageSparkIcon;
-}
-#endif  // defined(SUPPORT_PEDALS_VECTOR_ICONS)
-
-ContextualSearchSelectRegionAction::~ContextualSearchSelectRegionAction() =
-    default;
-
-////////////////////////////////////////////////////////////////////////////////
-
 ContextualSearchOpenLensAction::ContextualSearchOpenLensAction()
     : OmniboxAction(
           OmniboxAction::LabelStrings(u"Ask Google Lens about this page",
diff --git a/components/omnibox/browser/actions/contextual_search_action.h b/components/omnibox/browser/actions/contextual_search_action.h
index 51d172f..a2cb926 100644
--- a/components/omnibox/browser/actions/contextual_search_action.h
+++ b/components/omnibox/browser/actions/contextual_search_action.h
@@ -28,36 +28,6 @@
   bool is_zero_prefix_suggestion_;
 };
 
-class ContextualSearchSelectRegionAction : public OmniboxAction {
- public:
-  ContextualSearchSelectRegionAction();
-
-  // OmniboxAction:
-  OmniboxActionId ActionId() const override;
-  void Execute(ExecutionContext& context) const override;
-#if defined(SUPPORT_PEDALS_VECTOR_ICONS)
-  const gfx::VectorIcon& GetVectorIcon() const override;
-#endif
-
- protected:
-  ~ContextualSearchSelectRegionAction() override;
-};
-
-class ContextualSearchAskAboutPageAction : public OmniboxAction {
- public:
-  ContextualSearchAskAboutPageAction();
-
-  // OmniboxAction:
-  OmniboxActionId ActionId() const override;
-  void Execute(ExecutionContext& context) const override;
-#if defined(SUPPORT_PEDALS_VECTOR_ICONS)
-  const gfx::VectorIcon& GetVectorIcon() const override;
-#endif
-
- protected:
-  ~ContextualSearchAskAboutPageAction() override;
-};
-
 class ContextualSearchOpenLensAction : public OmniboxAction {
  public:
   ContextualSearchOpenLensAction();
diff --git a/components/omnibox/browser/actions/omnibox_action_concepts.h b/components/omnibox/browser/actions/omnibox_action_concepts.h
index bddc283..49a9c1a 100644
--- a/components/omnibox/browser/actions/omnibox_action_concepts.h
+++ b/components/omnibox/browser/actions/omnibox_action_concepts.h
@@ -26,8 +26,8 @@
   CONTEXTUAL_SEARCH_FULFILLMENT,
 
   // Actions that enter @page scope for direct query or with lens selection.
-  CONTEXTUAL_SEARCH_ASK_ABOUT_PAGE,
-  CONTEXTUAL_SEARCH_SELECT_REGION,
+  CONTEXTUAL_SEARCH_ASK_ABOUT_PAGE,  // Obsolete
+  CONTEXTUAL_SEARCH_SELECT_REGION,   // Obsolete
 
   // An action to open lens with contextual search side panel ready.
   CONTEXTUAL_SEARCH_OPEN_LENS,
diff --git a/components/omnibox/browser/autocomplete_grouper_sections.cc b/components/omnibox/browser/autocomplete_grouper_sections.cc
index 8811eee..fac969b 100644
--- a/components/omnibox/browser/autocomplete_grouper_sections.cc
+++ b/components/omnibox/browser/autocomplete_grouper_sections.cc
@@ -561,17 +561,6 @@
               },
               group_configs) {}
 
-DesktopWebZpsActionsSection::DesktopWebZpsActionsSection(
-    omnibox::GroupConfigMap& group_configs)
-    : ZpsSection(2,
-                 {
-                     Group(2,
-                           {
-                               {omnibox::GROUP_CONTEXTUAL_SEARCH_ACTION, 2},
-                           }),
-                 },
-                 group_configs) {}
-
 DesktopLensContextualZpsSection::DesktopLensContextualZpsSection(
     omnibox::GroupConfigMap& group_configs)
     : ZpsSection(5,
diff --git a/components/omnibox/browser/autocomplete_grouper_sections.h b/components/omnibox/browser/autocomplete_grouper_sections.h
index 5acf201..00ebd2b6 100644
--- a/components/omnibox/browser/autocomplete_grouper_sections.h
+++ b/components/omnibox/browser/autocomplete_grouper_sections.h
@@ -240,12 +240,6 @@
                                       size_t contextual_search_limit);
 };
 
-// A section to follow contextual search matches with the advert actions.
-class DesktopWebZpsActionsSection : public ZpsSection {
- public:
-  explicit DesktopWebZpsActionsSection(omnibox::GroupConfigMap& group_configs);
-};
-
 // Section expressing the Desktop ZPS limits and grouping for the Lens
 // contextual searchbox.
 // - up to 8 suggestions total.
diff --git a/components/omnibox/browser/autocomplete_grouper_sections_unittest.cc b/components/omnibox/browser/autocomplete_grouper_sections_unittest.cc
index 888b4fc..92daa8c4 100644
--- a/components/omnibox/browser/autocomplete_grouper_sections_unittest.cc
+++ b/components/omnibox/browser/autocomplete_grouper_sections_unittest.cc
@@ -2019,10 +2019,8 @@
     sections.push_back(
         std::make_unique<DesktopWebURLZpsSection>(group_configs, 3u));
     sections.push_back(std::make_unique<DesktopWebSearchZpsSection>(
-        group_configs, /*limit=*/4u, /*contextual_action_limit=*/0u,
+        group_configs, /*limit=*/4u, /*contextual_action_limit=*/1u,
         /*contextual_search_limit=*/0u));
-    sections.push_back(
-        std::make_unique<DesktopWebZpsActionsSection>(group_configs));
     auto out_matches = Section::GroupMatches(std::move(sections), matches);
     VerifyMatches(out_matches, expected_relevances);
   };
@@ -2038,9 +2036,9 @@
             CreateMatch(94, omnibox::GROUP_CONTEXTUAL_SEARCH_ACTION),
             CreateMatch(93, omnibox::GROUP_CONTEXTUAL_SEARCH),
         },
-        // URLs, then searches, then actions, stable sorted.
+        // URLs, then searches, then one action, stable sorted.
         // No contextual search matches due to above configuration.
-        {99, 98, 97, 96, 94});
+        {99, 98, 97, 96});
   }
 }
 #endif  // !(BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_IOS))
diff --git a/components/omnibox/browser/autocomplete_match.cc b/components/omnibox/browser/autocomplete_match.cc
index 7fe68a76a..62bd4b17 100644
--- a/components/omnibox/browser/autocomplete_match.cc
+++ b/components/omnibox/browser/autocomplete_match.cc
@@ -558,10 +558,7 @@
              : (IsContextualSearchSuggestion() &&
                 omnibox_feature_configs::ContextualSearch::Get()
                     .contextual_zero_suggest_lens_fulfillment)
-                 ? (omnibox_feature_configs::ContextualSearch::Get()
-                            .use_vertical_bar
-                        ? omnibox::kVerticalBarIcon
-                        : omnibox::kArrowUpChromeRefreshIcon)
+                 ? omnibox::kPageSparkIcon
                  : vector_icons::kSearchChromeRefreshIcon;
 
     case Type::PEDAL:
diff --git a/components/omnibox/browser/autocomplete_result.cc b/components/omnibox/browser/autocomplete_result.cc
index a0e84f64..5222950 100644
--- a/components/omnibox/browser/autocomplete_result.cc
+++ b/components/omnibox/browser/autocomplete_result.cc
@@ -544,15 +544,6 @@
             suggestion_groups_map_,
             max_search_suggestions + contextual_action_limit,
             contextual_action_limit, contextual_zps_limit));
-        if (omnibox_feature_configs::ContextualSearch::Get()
-                .IsContextualSearchEnabled() &&
-            omnibox_feature_configs::ContextualSearch::Get().actions_at_top) {
-          // Since this one is above the search section, it will be given the
-          // contextual search actions so they appear at top.
-          sections.insert(sections.begin(),
-                          std::make_unique<DesktopWebZpsActionsSection>(
-                              suggestion_groups_map_));
-        }
 #if BUILDFLAG(ENABLE_EXTENSIONS)
         if (base::FeatureList::IsEnabled(
                 extensions_features::kExperimentalOmniboxLabs)) {
diff --git a/components/omnibox/browser/contextual_search_provider.cc b/components/omnibox/browser/contextual_search_provider.cc
index ea48f45c..bcad740 100644
--- a/components/omnibox/browser/contextual_search_provider.cc
+++ b/components/omnibox/browser/contextual_search_provider.cc
@@ -305,29 +305,18 @@
   // These matches are effectively pedals that don't require any query matching.
   AutocompleteMatch match(this, omnibox::kContextualActionZeroSuggestRelevance,
                           false, AutocompleteMatchType::PEDAL);
-  match.contents_class = {{0, ACMatchClassification::NONE}};
   match.transition = ui::PAGE_TRANSITION_GENERATED;
   match.suggest_type = omnibox::SuggestType::TYPE_NATIVE_CHROME;
   match.suggestion_group_id = omnibox::GroupId::GROUP_CONTEXTUAL_SEARCH_ACTION;
 
-  auto add_action = [&](auto action) {
-    match.relevance--;
-    match.takeover_action = std::move(action);
-    match.contents = match.takeover_action->GetLabelStrings().hint;
-    matches_.push_back(match);
-  };
-  if (omnibox_feature_configs::ContextualSearch::Get().single_lens_action) {
-    add_action(base::MakeRefCounted<ContextualSearchOpenLensAction>());
-    // This one is special in that it also gets secondary text to show URL host.
-    AutocompleteMatch& action_match = matches_.back();
-    action_match.description = action_match.contents;
-    action_match.description_class = action_match.contents_class;
-    action_match.contents = base::UTF8ToUTF16(input.current_url().host());
-    action_match.contents_class = {{0, ACMatchClassification::URL}};
-  } else {
-    add_action(base::MakeRefCounted<ContextualSearchAskAboutPageAction>());
-    add_action(base::MakeRefCounted<ContextualSearchSelectRegionAction>());
-  }
+  // Lens invocation action with secondary text that shows URL host.
+  match.takeover_action =
+      base::MakeRefCounted<ContextualSearchOpenLensAction>();
+  match.contents = base::UTF8ToUTF16(input.current_url().host());
+  match.contents_class = {{0, ACMatchClassification::URL}};
+  match.description = match.takeover_action->GetLabelStrings().hint;
+  match.description_class = {{0, ACMatchClassification::NONE}};
+  matches_.push_back(match);
 }
 
 void ContextualSearchProvider::AddDefaultVerbatimMatch(
diff --git a/components/omnibox/browser/omnibox_edit_model.cc b/components/omnibox/browser/omnibox_edit_model.cc
index fbeff5d..20a9911 100644
--- a/components/omnibox/browser/omnibox_edit_model.cc
+++ b/components/omnibox/browser/omnibox_edit_model.cc
@@ -356,7 +356,6 @@
       is_keyword_hint_(false),
       keyword_mode_entry_method_(OmniboxEventProto::INVALID),
       in_revert_(false),
-      close_lens_(false),
       allow_exact_keyword_match_(false) {}
 
 OmniboxEditModel::~OmniboxEditModel() = default;
@@ -508,7 +507,6 @@
 
 void OmniboxEditModel::SetUserText(const std::u16string& text) {
   SetInputInProgress(true);
-  MaybeCloseLens();
   keyword_.clear();
   keyword_placeholder_.clear();
   is_keyword_hint_ = false;
@@ -758,7 +756,6 @@
   input_.Clear();
   paste_state_ = NONE;
   InternalSetUserText(std::u16string());
-  MaybeCloseLens();
   keyword_.clear();
   keyword_placeholder_.clear();
   is_keyword_hint_ = false;
@@ -1064,7 +1061,6 @@
   bool entry_by_tab = keyword_mode_entry_method_ == OmniboxEventProto::TAB;
 
   controller_->ClearPopupKeywordMode();
-  MaybeCloseLens();
 
   // There are several possible states we could have been in before the user hit
   // backspace or shift-tab to enter this function:
@@ -2847,40 +2843,6 @@
                        controller_->client()->AsWeakPtr()),
         match_selection_timestamp, disposition);
     action->Execute(context);
-
-    // Actions aren't generally able to change omnibox state, but it may be
-    // worth considering an extension to OmniboxAction::ExecutionContext
-    // if more action types want to enter keyword modes, close the popup, etc.
-    // Note: The CONTEXTUAL_SEARCH_OPEN_LENS action does not enter the omnibox
-    // into '@page' keyword mode and the omnibox is committed and closed as
-    // normal in that case, with the lens UI managing its own state, so there's
-    // no need for the omnibox to close lens.
-    if (action->ActionId() ==
-            OmniboxActionId::CONTEXTUAL_SEARCH_ASK_ABOUT_PAGE ||
-        action->ActionId() ==
-            OmniboxActionId::CONTEXTUAL_SEARCH_SELECT_REGION) {
-      if (const TemplateURL* page_turl =
-              controller_->client()
-                  ->GetTemplateURLService()
-                  ->FindStarterPackTemplateURL(
-                      TemplateURLStarterPackData::kPage)) {
-        EnterKeywordMode(
-            OmniboxEventProto::SELECT_SUGGESTION, page_turl,
-            l10n_util::GetStringUTF16(IDS_OMNIBOX_PAGE_SCOPE_PLACEHOLDER_TEXT));
-        if (action->ActionId() ==
-                OmniboxActionId::CONTEXTUAL_SEARCH_SELECT_REGION &&
-            view_) {
-          view_->CloseOmniboxPopup();
-        }
-        close_lens_ = true;
-        return;
-      }
-    } else if (action->ActionId() ==
-               OmniboxActionId::CONTEXTUAL_SEARCH_FULFILLMENT) {
-      // Lens fulfills matches in the side panel, and should not be closed
-      // by the omnibox after opening this match.
-      close_lens_ = false;
-    }
   }
 
   if (disposition != WindowOpenDisposition::NEW_BACKGROUND_TAB && view_) {
@@ -3094,12 +3056,3 @@
   keyword_placeholder_ = keyword_placeholder;
 }
 
-void OmniboxEditModel::MaybeCloseLens() {
-  if (!close_lens_) {
-    return;
-  }
-  close_lens_ = false;
-  // TODO(crbug.com/413405157): This is a targeted bug-fix, but more complete
-  //  handling of keyword mode transitions is needed.
-  controller_->client()->OnKeywordModeChanged(false, keyword_);
-}
diff --git a/components/omnibox/browser/omnibox_edit_model.h b/components/omnibox/browser/omnibox_edit_model.h
index 02cdb6ba..c72bd13c 100644
--- a/components/omnibox/browser/omnibox_edit_model.h
+++ b/components/omnibox/browser/omnibox_edit_model.h
@@ -654,9 +654,6 @@
   void SetKeyword(const std::u16string& keyword);
   void SetKeywordPlaceholder(const std::u16string& keyword_placeholder);
 
-  // Closes lens if still needed.
-  void MaybeCloseLens();
-
   // Owns this.
   raw_ptr<OmniboxController> controller_;
 
@@ -795,13 +792,6 @@
   // presses escape.
   bool in_revert_;
 
-  // The omnibox sometimes opens the lens controller, e.g. when entering '@page'
-  // keyword mode. When exiting the scope or committing the omnibox, it should
-  // be closed again, unless lens is invoked by taking a contextual search
-  // match. In that case, the omnibox relinquishes the obligation to close so
-  // as to not interfere with lens match fulfillment and continued use.
-  bool close_lens_;
-
   // Indicates if the upcoming autocomplete search is allowed to be treated as
   // an exact keyword match.  If this is true then keyword mode will be
   // triggered automatically if the input is "<keyword> <search string>".  We
diff --git a/components/omnibox/browser/vector_icons/vertical_bar.icon b/components/omnibox/browser/vector_icons/vertical_bar.icon
deleted file mode 100644
index 9c3a2a5..0000000
--- a/components/omnibox/browser/vector_icons/vertical_bar.icon
+++ /dev/null
@@ -1,10 +0,0 @@
-// Copyright 2025 The Chromium Authors
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-CANVAS_DIMENSIONS, 24,
-MOVE_TO, 11, 0,
-LINE_TO, 13, 0,
-LINE_TO, 13, 24,
-LINE_TO, 11, 24,
-CLOSE
diff --git a/components/omnibox/common/omnibox_feature_configs.cc b/components/omnibox/common/omnibox_feature_configs.cc
index 705dffa2..97511f34 100644
--- a/components/omnibox/common/omnibox_feature_configs.cc
+++ b/components/omnibox/common/omnibox_feature_configs.cc
@@ -82,22 +82,10 @@
              "OmniboxContextualSearchOnFocusSuggestions",
              base::FEATURE_DISABLED_BY_DEFAULT);
 
-BASE_FEATURE(ContextualSearch::kOmniboxContextualSearchActionsAtTop,
-             "OmniboxContextualSearchActionsAtTop",
-             base::FEATURE_DISABLED_BY_DEFAULT);
-
-BASE_FEATURE(ContextualSearch::kOmniboxContextualSearchSingleLensAction,
-             "OmniboxContextualSearchSingleLensAction",
-             base::FEATURE_ENABLED_BY_DEFAULT);
-
 BASE_FEATURE(ContextualSearch::kContextualSearchBoxUsesContextualSearchProvider,
              "ContextualSearchBoxUsesContextualSearchProvider",
              base::FEATURE_ENABLED_BY_DEFAULT);
 
-BASE_FEATURE(ContextualSearch::kContextualSearchUseVerticalBar,
-             "ContextualSearchUseVerticalBar",
-             base::FEATURE_ENABLED_BY_DEFAULT);
-
 ContextualSearch::ContextualSearch() {
   // Meta-feature turns on/off other features, but only if it's overridden by
   // the user. If not then each feature is controlled separately.
@@ -122,14 +110,8 @@
                                     "Limit", 3)
                 .Get()
           : 0;
-  actions_at_top =
-      base::FeatureList::IsEnabled(kOmniboxContextualSearchActionsAtTop);
-  single_lens_action =
-      base::FeatureList::IsEnabled(kOmniboxContextualSearchSingleLensAction);
   csb_uses_csp = base::FeatureList::IsEnabled(
       kContextualSearchBoxUsesContextualSearchProvider);
-  use_vertical_bar =
-      base::FeatureList::IsEnabled(kContextualSearchUseVerticalBar);
 }
 
 ContextualSearch::ContextualSearch(const ContextualSearch&) = default;
diff --git a/components/omnibox/common/omnibox_feature_configs.h b/components/omnibox/common/omnibox_feature_configs.h
index ec81e38..7c0159b7 100644
--- a/components/omnibox/common/omnibox_feature_configs.h
+++ b/components/omnibox/common/omnibox_feature_configs.h
@@ -139,10 +139,7 @@
   DECLARE_FEATURE(kContextualSearchProviderAsyncSuggestInputs);
   DECLARE_FEATURE(kSendContextualUrlSuggestParam);
   DECLARE_FEATURE(kOmniboxContextualSearchOnFocusSuggestions);
-  DECLARE_FEATURE(kOmniboxContextualSearchActionsAtTop);
-  DECLARE_FEATURE(kOmniboxContextualSearchSingleLensAction);
   DECLARE_FEATURE(kContextualSearchBoxUsesContextualSearchProvider);
-  DECLARE_FEATURE(kContextualSearchUseVerticalBar);
 
   // Whether to use contextual search features, for example the lens action.
   bool IsContextualSearchEnabled() const;
@@ -164,19 +161,9 @@
   // Maximum number of contextual search suggestions for zero prefix suggest.
   size_t contextual_zps_limit;
 
-  // Whether to show actions at top of zero suggest list: default false, bottom.
-  bool actions_at_top;
-
-  // Whether to use the unified single action to open lens UI.
-  bool single_lens_action;
-
   // Whether to use ContextualSearchProvider instead of ZeroSuggestProvider for
   // sourcing contextual search box matches.
   bool csb_uses_csp;
-
-  // Whether to use vertical bar instead of regular icon on contextual search
-  // matches.
-  bool use_vertical_bar;
 };
 
 // If enabled, allows MIA zero-prefix suggestions in NTP omnibox and realbox.
diff --git a/components/optimization_guide/core/model_execution/model_broker_client.h b/components/optimization_guide/core/model_execution/model_broker_client.h
index 1b76ef1..3913bd1 100644
--- a/components/optimization_guide/core/model_execution/model_broker_client.h
+++ b/components/optimization_guide/core/model_execution/model_broker_client.h
@@ -60,6 +60,18 @@
     return weak_ptr_factory_.GetWeakPtr();
   }
 
+  mojom::ModelSolution& solution() { return *remote_; }
+
+  const OnDeviceModelFeatureAdapter& feature_adapter() const {
+    return *feature_adapter_;
+  }
+
+  uint32_t max_tokens() const { return max_tokens_; }
+
+  const proto::FeatureTextSafetyConfiguration& safety_config() const {
+    return safety_config_;
+  }
+
  private:
   // Called when the remote disconnects.
   void OnDisconnect();
@@ -86,7 +98,6 @@
       std::unique_ptr<OptimizationGuideModelExecutor::Session>;
   using CreateSessionCallback = base::OnceCallback<void(CreateSessionResult)>;
   using ClientCallback = base::OnceCallback<void(base::WeakPtr<ModelClient>)>;
-  using State = std::optional<mojom::ModelUnavailableReason>;
 
   // Get info about whether the model is / will be available.
   std::optional<mojom::ModelUnavailableReason> unavailable_reason() const {
@@ -99,11 +110,11 @@
                      const std::optional<SessionConfigParams>& config_params,
                      CreateSessionCallback callback);
 
- private:
   // Wait for the client to be available and call the callback with a reference.
   // Calls the callback with nullptr if the state become NotSupported.
   void WaitForClient(ClientCallback callback);
 
+ private:
   // mojom::ModelSubscriber
   void Unavailable(mojom::ModelUnavailableReason) override;
   void Available(mojom::ModelSolutionConfigPtr config,
diff --git a/components/optimization_guide/core/model_execution/on_device_model_service_controller.cc b/components/optimization_guide/core/model_execution/on_device_model_service_controller.cc
index 7b52335..aeadd81 100644
--- a/components/optimization_guide/core/model_execution/on_device_model_service_controller.cc
+++ b/components/optimization_guide/core/model_execution/on_device_model_service_controller.cc
@@ -496,6 +496,9 @@
                  receiver,
              on_device_model::ModelAssets assets) {
             if (!self || !self->controller_->service_client_.is_bound()) {
+              if (self) {
+                self->controller_->service_client_.RemovePendingUsage();
+              }
               CloseFilesInBackground(std::move(assets));
               return;
             }
@@ -517,6 +520,13 @@
 OnDeviceModelServiceController::BaseModelController::PopulateModelPaths() {
   on_device_model::ModelAssetPaths model_paths;
   model_paths.weights = model_metadata_->model_path().Append(kWeightsFile);
+
+  // TODO(crbug.com/400998489): Cache files are experimental for now.
+  if (features::ForceCpuBackendForOnDeviceModel()) {
+    model_paths.cache =
+        model_metadata_->model_path().Append(kExperimentalCacheFile);
+  }
+
   return model_paths;
 }
 
diff --git a/components/optimization_guide/core/model_execution/on_device_model_service_controller_unittest.cc b/components/optimization_guide/core/model_execution/on_device_model_service_controller_unittest.cc
index f6a806b0..2b2b348b 100644
--- a/components/optimization_guide/core/model_execution/on_device_model_service_controller_unittest.cc
+++ b/components/optimization_guide/core/model_execution/on_device_model_service_controller_unittest.cc
@@ -428,6 +428,43 @@
   EXPECT_EQ(limits.max_output_tokens, 17u);
 }
 
+TEST_F(OnDeviceModelServiceControllerTest, CacheWeightExecutionSuccess) {
+  // TODO(crbug.com/400998489): Cache files are experimental for now. Stop
+  // setting this feature flag once that's no longer the case.
+  base::test::ScopedFeatureList feature_list;
+  feature_list.InitAndEnableFeatureWithParameters(
+      features::kOptimizationGuideOnDeviceModel,
+      {{"on_device_model_force_cpu_backend", "true"},
+       {"on_device_model_topk", "1"},
+       {"on_device_model_temperature", "0"}});
+
+  FakeBaseModelAsset base_model_with_cache({
+      .cache_weight = 1015,
+  });
+
+  Initialize(InitializeParams{
+      .base_model = &base_model_with_cache,
+      .safety = &standard_assets_.safety,
+      .language = &standard_assets_.language,
+      .adaptations = {&standard_assets_.compose},
+  });
+  auto session = CreateSession();
+  ASSERT_TRUE(session);
+  session->ExecuteModel(PageUrlRequest("foo"),
+                        response_.GetStreamingCallback());
+  ASSERT_TRUE(response_.GetFinalStatus());
+  EXPECT_EQ(*response_.value(),
+            "Cache weight: 1015\nContext: execute:foo max:1024\n");
+
+  // If we destroy all sessions and wait long enough, everything should idle out
+  // and the service should get terminated.
+  session.reset();
+  task_environment_.FastForwardBy(features::GetOnDeviceModelIdleTimeout() +
+                                  base::Seconds(1));
+  task_environment_.RunUntilIdle();
+  EXPECT_FALSE(fake_launcher_.is_service_running());
+}
+
 TEST_F(OnDeviceModelServiceControllerTest, AdaptationModelExecutionSuccess) {
   FakeAdaptationAsset compose_asset({
       .config = SimpleComposeConfig(),
@@ -2399,6 +2436,7 @@
   EXPECT_TRUE(response_.value());
   const std::vector<std::string> partial_responses = {
       "Context: execute:foo max:1024\n",
+      "TopK: 3, Temp: 2\n",
   };
   EXPECT_EQ(*response_.value(), ConcatResponses(partial_responses));
   EXPECT_THAT(response_.partials(), ElementsAreArray(partial_responses));
@@ -3458,6 +3496,7 @@
   EXPECT_TRUE(response_.value());
   const std::vector<std::string> partial_responses = {
       "Context: execute:foo max:1024\n",
+      "TopK: 3, Temp: 2\n",
   };
   EXPECT_EQ(*response_.value(), ConcatResponses(partial_responses));
   EXPECT_THAT(response_.partials(), ElementsAreArray(partial_responses));
diff --git a/components/optimization_guide/core/model_execution/safety_client.cc b/components/optimization_guide/core/model_execution/safety_client.cc
index c23c6b31..d3cdecdf 100644
--- a/components/optimization_guide/core/model_execution/safety_client.cc
+++ b/components/optimization_guide/core/model_execution/safety_client.cc
@@ -109,6 +109,7 @@
               base::ThreadPool::PostTask(
                   FROM_HERE, {base::MayBlock()},
                   base::DoNothingWithBoundArgs(std::move(params)));
+              return;
             }
             self->service_client_->Get()->LoadTextSafetyModel(std::move(params),
                                                               std::move(model));
diff --git a/components/optimization_guide/core/model_execution/test/fake_model_assets.cc b/components/optimization_guide/core/model_execution/test/fake_model_assets.cc
index b3bcc90..5d2f557 100644
--- a/components/optimization_guide/core/model_execution/test/fake_model_assets.cc
+++ b/components/optimization_guide/core/model_execution/test/fake_model_assets.cc
@@ -39,6 +39,10 @@
 void FakeBaseModelAsset::Write(Content&& content) {
   CHECK(base::WriteFile(temp_dir_.GetPath().Append(kWeightsFile),
                         base::NumberToString(content.weight)));
+  if (content.cache_weight) {
+    CHECK(base::WriteFile(temp_dir_.GetPath().Append(kExperimentalCacheFile),
+                          base::NumberToString(content.cache_weight)));
+  }
   CHECK(base::WriteFile(
       temp_dir_.GetPath().Append(kOnDeviceModelExecutionConfigFile),
       content.config.SerializeAsString()));
diff --git a/components/optimization_guide/core/model_execution/test/fake_model_assets.h b/components/optimization_guide/core/model_execution/test/fake_model_assets.h
index 42d1a02..cc91e3cc 100644
--- a/components/optimization_guide/core/model_execution/test/fake_model_assets.h
+++ b/components/optimization_guide/core/model_execution/test/fake_model_assets.h
@@ -30,6 +30,7 @@
     uint32_t weight = 0;
     proto::OnDeviceModelExecutionConfig config;
     std::string version = "0.0.1";
+    uint32_t cache_weight = 0;
   };
   FakeBaseModelAsset();
   explicit FakeBaseModelAsset(Content&& content);
diff --git a/components/optimization_guide/core/model_execution/test/fake_model_broker.cc b/components/optimization_guide/core/model_execution/test/fake_model_broker.cc
index 1b277bd..1d9e2d8 100644
--- a/components/optimization_guide/core/model_execution/test/fake_model_broker.cc
+++ b/components/optimization_guide/core/model_execution/test/fake_model_broker.cc
@@ -48,4 +48,12 @@
   return remote;
 }
 
+void FakeModelBroker::UpdateModelAdaptation(const FakeAdaptationAsset& asset) {
+  // First clear the current adaptation, then add the new asset to force an
+  // update.
+  test_controller_->MaybeUpdateModelAdaptation(asset.feature(), nullptr);
+  test_controller_->MaybeUpdateModelAdaptation(asset.feature(),
+                                               asset.metadata());
+}
+
 }  // namespace optimization_guide
diff --git a/components/optimization_guide/core/model_execution/test/fake_model_broker.h b/components/optimization_guide/core/model_execution/test/fake_model_broker.h
index b086624..73f2053 100644
--- a/components/optimization_guide/core/model_execution/test/fake_model_broker.h
+++ b/components/optimization_guide/core/model_execution/test/fake_model_broker.h
@@ -24,6 +24,17 @@
 
   mojo::PendingRemote<mojom::ModelBroker> BindAndPassRemote();
 
+  on_device_model::FakeOnDeviceServiceSettings& settings() {
+    return fake_settings_;
+  }
+
+  void CrashService() { fake_launcher_.CrashService(); }
+
+  void UpdateModelAdaptation(const FakeAdaptationAsset& asset);
+  void UpdateSafetyModel(const optimization_guide::ModelInfo& model_info) {
+    test_controller_->MaybeUpdateSafetyModel(model_info);
+  }
+
  private:
   base::test::ScopedFeatureList feature_list_;
   TestingPrefServiceSimple pref_service_;
diff --git a/components/optimization_guide/core/optimization_guide_constants.cc b/components/optimization_guide/core/optimization_guide_constants.cc
index ea368b6..2356d752 100644
--- a/components/optimization_guide/core/optimization_guide_constants.cc
+++ b/components/optimization_guide/core/optimization_guide_constants.cc
@@ -51,6 +51,9 @@
 const base::FilePath::CharType kWeightsFile[] =
     FILE_PATH_LITERAL("weights.bin");
 
+const base::FilePath::CharType kExperimentalCacheFile[] =
+    FILE_PATH_LITERAL("cache.bin");
+
 const base::FilePath::CharType kTsDataFile[] = FILE_PATH_LITERAL("ts.bin");
 
 const base::FilePath::CharType kTsSpModelFile[] =
diff --git a/components/optimization_guide/core/optimization_guide_constants.h b/components/optimization_guide/core/optimization_guide_constants.h
index 0ab265ff..0becd41c 100644
--- a/components/optimization_guide/core/optimization_guide_constants.h
+++ b/components/optimization_guide/core/optimization_guide_constants.h
@@ -76,6 +76,10 @@
 // Files expected to be in the on device model bundle.
 COMPONENT_EXPORT(OPTIMIZATION_GUIDE_FEATURES)
 extern const base::FilePath::CharType kWeightsFile[];
+// TODO(crbug.com/400998489): Cache files are experimental. Eventually we
+// probably want a cache path per-backend. This is here now for testing.
+COMPONENT_EXPORT(OPTIMIZATION_GUIDE_FEATURES)
+extern const base::FilePath::CharType kExperimentalCacheFile[];
 COMPONENT_EXPORT(OPTIMIZATION_GUIDE_FEATURES)
 extern const base::FilePath::CharType kOnDeviceModelExecutionConfigFile[];
 
diff --git a/components/optimization_guide/internal b/components/optimization_guide/internal
index 03d56c6..c69f1d7 160000
--- a/components/optimization_guide/internal
+++ b/components/optimization_guide/internal
@@ -1 +1 @@
-Subproject commit 03d56c66293bf2113bb997d537a2749d2dd7da14
+Subproject commit c69f1d7a8ad0ed4c22595719e1d78b7bb8754516
diff --git a/components/optimization_guide/proto/features/OWNERS b/components/optimization_guide/proto/features/OWNERS
index 489aa46..8ed6efd 100644
--- a/components/optimization_guide/proto/features/OWNERS
+++ b/components/optimization_guide/proto/features/OWNERS
@@ -1,6 +1,5 @@
 curranmax@chromium.org
 sreejakshetty@chromium.org
-isherman@chromium.org
 ryansturm@chromium.org
 spelchat@chromium.org
 yulunz@chromium.org
diff --git a/components/permissions/features.cc b/components/permissions/features.cc
index 6795ce04e..c09a6a94 100644
--- a/components/permissions/features.cc
+++ b/components/permissions/features.cc
@@ -115,7 +115,7 @@
 
 BASE_FEATURE(kCpssUseTfliteSignatureRunner,
              "CpssUseTfliteSignatureRunner",
-             base::FEATURE_DISABLED_BY_DEFAULT);
+             base::FEATURE_ENABLED_BY_DEFAULT);
 
 // When enabled, FederatedIdentityApiEmbargoDurationDismiss will use values from
 // a field trial.
diff --git a/components/policy/resources/templates/policies.yaml b/components/policy/resources/templates/policies.yaml
index 2b7334f3..feabeb56 100644
--- a/components/policy/resources/templates/policies.yaml
+++ b/components/policy/resources/templates/policies.yaml
@@ -1358,7 +1358,7 @@
   1357: BuiltInAIAPIsEnabled
   1358: TabGroupSharingSettings
   1359: NTPFooterManagementNoticeEnabled
-  1360: NTPFooterThemeAttributionEnabled
+  1360: NTPFooterExtensionAttributionEnabled
   1361: ClearWindowNameForNewBrowsingContextGroup
 
 atomic_groups:
diff --git a/components/policy/resources/templates/policy_definitions/Miscellaneous/NTPFooterExtensionAttributionEnabled.yaml b/components/policy/resources/templates/policy_definitions/Miscellaneous/NTPFooterExtensionAttributionEnabled.yaml
new file mode 100644
index 0000000..170ef4ff3
--- /dev/null
+++ b/components/policy/resources/templates/policy_definitions/Miscellaneous/NTPFooterExtensionAttributionEnabled.yaml
@@ -0,0 +1,26 @@
+caption:  Control the visibility of the extension attribution on the New Tab Page for managed browsers
+default: true
+desc: |-
+  This policy controls the visibility of extension attribution within the footer of the New Tab Page (NTP) when the NTP is controlled by an extension. By default, the NTP footer displays information about the extension controlling the NTP.
+
+  If this policy is left unset or set to true, browsers with an extension controlled NTP will show the extension name and a link to it.
+
+  If this policy is set to false, the extension atribution will be hidden.
+example_value: true
+features:
+  dynamic_refresh: true
+  per_profile: true
+future_on:
+- chrome.*
+items:
+- caption: Enable extension theme attribution on NTP Footer
+  value: true
+- caption: Disable extension theme attribution on NTP Footer
+  value: false
+owners:
+- file://components/policy/OWNERS
+- esalma@google.com
+schema:
+  type: boolean
+tags: []
+type: main
diff --git a/components/policy/resources/templates/policy_definitions/Miscellaneous/NTPFooterThemeAttributionEnabled.yaml b/components/policy/resources/templates/policy_definitions/Miscellaneous/NTPFooterThemeAttributionEnabled.yaml
deleted file mode 100644
index c02807d5..0000000
--- a/components/policy/resources/templates/policy_definitions/Miscellaneous/NTPFooterThemeAttributionEnabled.yaml
+++ /dev/null
@@ -1,26 +0,0 @@
-caption:  Control the visibility of the extension theme attribution on the New Tab Page for managed browsers
-default: true
-desc: |-
-  This policy controls the visibility of extension theme attribution within the footer of the New Tab Page (NTP) when the NTP is controlled by an extension. By default, the NTP footer displays information about the extension controlling the NTP.
-
-  If this policy is left unset or set to true, browsers with an extension controlled NTP will show the extension name and a link to it.
-
-  If this policy is set to false, the theme atribution will be hidden.
-example_value: true
-features:
-  dynamic_refresh: true
-  per_profile: true
-future_on:
-- chrome.*
-items:
-- caption: Enable extension theme attribution on NTP Footer
-  value: true
-- caption: Disable extension theme attribution on NTP Footer
-  value: false
-owners:
-- file://components/policy/OWNERS
-- esalma@google.com
-schema:
-  type: boolean
-tags: []
-type: main
diff --git a/components/policy/test/data/pref_mapping/NTPFooterThemeAttributionEnabled.json b/components/policy/test/data/pref_mapping/NTPFooterExtensionAttributionEnabled.json
similarity index 78%
rename from components/policy/test/data/pref_mapping/NTPFooterThemeAttributionEnabled.json
rename to components/policy/test/data/pref_mapping/NTPFooterExtensionAttributionEnabled.json
index 6f70057..62dd553 100644
--- a/components/policy/test/data/pref_mapping/NTPFooterThemeAttributionEnabled.json
+++ b/components/policy/test/data/pref_mapping/NTPFooterExtensionAttributionEnabled.json
@@ -6,7 +6,7 @@
         "mac"
       ],
       "simple_policy_pref_mapping_test": {
-        "pref_name": "ntp_footer.settings.theme_attribution",
+        "pref_name": "ntp_footer.settings.extension_attribution",
         "default_value": true,
         "values_to_test": [
           true,
diff --git a/components/renderer_context_menu/context_menu_content_type.cc b/components/renderer_context_menu/context_menu_content_type.cc
index 7e5c737..c694dd4 100644
--- a/components/renderer_context_menu/context_menu_content_type.cc
+++ b/components/renderer_context_menu/context_menu_content_type.cc
@@ -65,7 +65,7 @@
   const bool has_selection = !params_.selection_text.empty();
   const bool is_password = params_.form_control_type ==
                            blink::mojom::FormControlType::kInputPassword;
-  const bool existing_highlight = params_.opened_from_highlight;
+  const bool existing_highlight = params_.annotation_type.has_value();
 
   switch (group) {
     case ITEM_GROUP_CUSTOM:
@@ -125,7 +125,7 @@
       return has_selection;
 
     case ITEM_GROUP_EXISTING_LINK_TO_TEXT:
-      return params_.opened_from_highlight;
+      return params_.annotation_type.has_value();
 
     case ITEM_GROUP_SEARCH_PROVIDER:
       return has_selection && !is_password;
diff --git a/components/tabs/public/tab_interface.h b/components/tabs/public/tab_interface.h
index 2b5c0ced..a01b18f2 100644
--- a/components/tabs/public/tab_interface.h
+++ b/components/tabs/public/tab_interface.h
@@ -205,6 +205,7 @@
   //   (3) It is not possible to perform dependency injection for legacy code
   //   that is conceptually a TabFeature and needs access to other TabFeatures.
   virtual tabs::TabFeatures* GetTabFeatures() = 0;
+  virtual const tabs::TabFeatures* GetTabFeatures() const = 0;
 
   // Return true if the tab is pinned in its tabstrip, or false otherwise.
   virtual bool IsPinned() const = 0;
diff --git a/components/url_pattern/url_pattern_util.cc b/components/url_pattern/url_pattern_util.cc
index 9fcdb9e..6e56c10 100644
--- a/components/url_pattern/url_pattern_util.cc
+++ b/components/url_pattern/url_pattern_util.cc
@@ -247,9 +247,7 @@
   url::RawCanonOutputT<char> canon_output;
   url::Component component;
 
-  url::CanonicalizeRef(input.data(),
-                       url::Component(0, base::checked_cast<int>(input.size())),
-                       &canon_output, &component);
+  url::CanonicalizeRef(input, &canon_output, &component);
 
   return StdStringFromCanonOutput(canon_output, component);
 }
diff --git a/content/browser/BUILD.gn b/content/browser/BUILD.gn
index 4c02b0a..c6c36f0 100644
--- a/content/browser/BUILD.gn
+++ b/content/browser/BUILD.gn
@@ -462,8 +462,8 @@
     "ai/echo_ai_language_model.h",
     "ai/echo_ai_manager_impl.cc",
     "ai/echo_ai_manager_impl.h",
-    "ai/echo_ai_proofreader.h",
     "ai/echo_ai_proofreader.cc",
+    "ai/echo_ai_proofreader.h",
     "ai/echo_ai_rewriter.cc",
     "ai/echo_ai_rewriter.h",
     "ai/echo_ai_summarizer.cc",
@@ -2407,8 +2407,12 @@
     "webid/digital_credentials/digital_identity_request_impl.h",
     "webid/fake_identity_request_dialog_controller.cc",
     "webid/fake_identity_request_dialog_controller.h",
+    "webid/fedcm_mappers.cc",
+    "webid/fedcm_mappers.h",
     "webid/fedcm_metrics.cc",
     "webid/fedcm_metrics.h",
+    "webid/fedcm_url_computations.cc",
+    "webid/fedcm_url_computations.h",
     "webid/federated_auth_autofill_source.cc",
     "webid/federated_auth_disconnect_request.cc",
     "webid/federated_auth_disconnect_request.h",
diff --git a/content/browser/accessibility/browser_accessibility_android_unittest.cc b/content/browser/accessibility/browser_accessibility_android_unittest.cc
index 0a1af6a..9576ba6 100644
--- a/content/browser/accessibility/browser_accessibility_android_unittest.cc
+++ b/content/browser/accessibility/browser_accessibility_android_unittest.cc
@@ -9,6 +9,7 @@
 #include "base/test/scoped_feature_list.h"
 #include "build/build_config.h"
 #include "content/browser/accessibility/browser_accessibility_manager_android.h"
+#include "content/browser/accessibility/web_contents_accessibility_android.h"
 #include "content/public/test/browser_task_environment.h"
 #include "content/test/test_content_client.h"
 #include "testing/gtest/include/gtest/gtest.h"
@@ -48,6 +49,12 @@
   }
 };
 
+class MockWebContentsAccessibilityAndroid
+    : public WebContentsAccessibilityAndroid {
+ public:
+  MockWebContentsAccessibilityAndroid() {}
+};
+
 class BrowserAccessibilityAndroidTest : public ::testing::Test {
  public:
   BrowserAccessibilityAndroidTest();
@@ -63,6 +70,7 @@
   std::unique_ptr<ui::TestAXPlatformTreeManagerDelegate>
       test_browser_accessibility_delegate_;
   ui::TestAXNodeIdDelegate node_id_delegate_;
+  MockWebContentsAccessibilityAndroid mock_web_contents_accessibility_android_;
 
  private:
   void SetUp() override;
@@ -80,7 +88,8 @@
 void BrowserAccessibilityAndroidTest::SetUp() {
   test_browser_accessibility_delegate_ =
       std::make_unique<ui::TestAXPlatformTreeManagerDelegate>();
-
+  test_browser_accessibility_delegate_->SetWebContentsAccessibility(
+      &mock_web_contents_accessibility_android_);
   SetContentClient(&client_);
 }
 
@@ -691,4 +700,119 @@
             image_succeeded_with_name->GetSupplementalDescription());
 }
 
+TEST_F(BrowserAccessibilityAndroidTest, TestJavaNodeCache_AttributeChange) {
+  ui::AXTreeUpdate tree;
+  tree.root_id = 1;
+  tree.nodes.resize(2);
+  tree.nodes[0].id = 1;
+  tree.nodes[0].role = ax::mojom::Role::kRootWebArea;
+  tree.nodes[0].child_ids = {2};
+
+  tree.nodes[1].id = 2;
+  tree.nodes[1].role = ax::mojom::Role::kButton;
+
+  std::unique_ptr<ui::BrowserAccessibilityManager> manager(
+      BrowserAccessibilityManagerAndroid::Create(
+          tree, node_id_delegate_, test_browser_accessibility_delegate_.get()));
+
+  BrowserAccessibilityManagerAndroid* android_manager =
+      ToBrowserAccessibilityManagerAndroid(manager.get());
+  const auto& actual = android_manager->nodes_already_cleared_for_test();
+  EXPECT_EQ(2, actual.size());
+  EXPECT_TRUE(actual.contains(1));
+  EXPECT_TRUE(actual.contains(2));
+
+  ui::AXUpdatesAndEvents updates_and_events;
+  updates_and_events.updates.resize(1);
+  updates_and_events.updates[0].nodes.resize(1);
+  updates_and_events.updates[0].nodes[0].id = 2;
+  updates_and_events.updates[0].nodes[0].AddStringAttribute(
+      ax::mojom::StringAttribute::kName, "hello");
+
+  manager->OnAccessibilityEvents(updates_and_events);
+
+  EXPECT_EQ(1, actual.size());
+  EXPECT_TRUE(actual.contains(2));
+}
+
+TEST_F(BrowserAccessibilityAndroidTest, TestJavaNodeCache_NodeDeleted) {
+  ui::AXTreeUpdate tree;
+  tree.root_id = 1;
+  tree.nodes.resize(2);
+  tree.nodes[0].id = 1;
+  tree.nodes[0].role = ax::mojom::Role::kRootWebArea;
+  tree.nodes[0].child_ids = {2};
+
+  tree.nodes[1].id = 2;
+  tree.nodes[1].role = ax::mojom::Role::kButton;
+
+  std::unique_ptr<ui::BrowserAccessibilityManager> manager(
+      BrowserAccessibilityManagerAndroid::Create(
+          tree, node_id_delegate_, test_browser_accessibility_delegate_.get()));
+
+  BrowserAccessibilityManagerAndroid* android_manager =
+      ToBrowserAccessibilityManagerAndroid(manager.get());
+  const auto& actual = android_manager->nodes_already_cleared_for_test();
+  EXPECT_EQ(2, actual.size());
+  EXPECT_TRUE(actual.contains(1));
+  EXPECT_TRUE(actual.contains(2));
+
+  ui::AXUpdatesAndEvents updates_and_events;
+  updates_and_events.updates.resize(1);
+  updates_and_events.updates[0].nodes.resize(1);
+  updates_and_events.updates[0].nodes[0].id = 1;
+  updates_and_events.updates[0].nodes[0].role = ax::mojom::Role::kRootWebArea;
+
+  manager->OnAccessibilityEvents(updates_and_events);
+
+  EXPECT_EQ(2, actual.size());
+  EXPECT_TRUE(actual.contains(1));
+  EXPECT_TRUE(actual.contains(2));
+}
+
+TEST_F(BrowserAccessibilityAndroidTest, TestJavaNodeCache_NodeUnignored) {
+  ui::AXTreeUpdate tree;
+  tree.root_id = 1;
+  tree.nodes.resize(3);
+  tree.nodes[0].id = 1;
+  tree.nodes[0].role = ax::mojom::Role::kRootWebArea;
+  tree.nodes[0].child_ids = {2};
+
+  tree.nodes[1].id = 2;
+  tree.nodes[1].role = ax::mojom::Role::kButton;
+  tree.nodes[1].AddState(ax::mojom::State::kIgnored);
+  tree.nodes[1].child_ids = {3};
+
+  tree.nodes[2].id = 3;
+  tree.nodes[2].role = ax::mojom::Role::kStaticText;
+
+  std::unique_ptr<ui::BrowserAccessibilityManager> manager(
+      BrowserAccessibilityManagerAndroid::Create(
+          tree, node_id_delegate_, test_browser_accessibility_delegate_.get()));
+
+  BrowserAccessibilityManagerAndroid* android_manager =
+      ToBrowserAccessibilityManagerAndroid(manager.get());
+  const auto& actual = android_manager->nodes_already_cleared_for_test();
+  EXPECT_EQ(3, actual.size());
+  EXPECT_TRUE(actual.contains(1));
+  EXPECT_TRUE(actual.contains(2));
+  EXPECT_TRUE(actual.contains(3));
+
+  ui::AXUpdatesAndEvents updates_and_events;
+  updates_and_events.updates.resize(1);
+  updates_and_events.updates[0].nodes.resize(1);
+  updates_and_events.updates[0].nodes[0].id = 2;
+  updates_and_events.updates[0].nodes[0].role = ax::mojom::Role::kButton;
+
+  manager->OnAccessibilityEvents(updates_and_events);
+
+  EXPECT_EQ(3, actual.size());
+  // From an AXEventGenerator::Event::CHILDREN_CHANGED.
+  EXPECT_TRUE(actual.contains(1));
+  // From an AXTreeObserver::Change; the only actual tree update.
+  EXPECT_TRUE(actual.contains(2));
+  // From an AXEventGenerator::Event::PARENT_CHANGED.
+  EXPECT_TRUE(actual.contains(3));
+}
+
 }  // namespace content
diff --git a/content/browser/accessibility/browser_accessibility_manager_android.cc b/content/browser/accessibility/browser_accessibility_manager_android.cc
index 68d2f3b..b590ed99c1 100644
--- a/content/browser/accessibility/browser_accessibility_manager_android.cc
+++ b/content/browser/accessibility/browser_accessibility_manager_android.cc
@@ -182,8 +182,7 @@
           GetFromAXNode(GetLastFocusedNode())) {
     BrowserAccessibilityAndroid* android_last_focused_node =
         static_cast<BrowserAccessibilityAndroid*>(last_focused_node);
-    wcax->ClearNodeInfoCacheForGivenId(
-        android_last_focused_node->GetUniqueId());
+    ClearNodeInfoCacheForGivenId(android_last_focused_node->GetUniqueId());
   }
 
   BrowserAccessibilityAndroid* android_node =
@@ -249,10 +248,6 @@
   BrowserAccessibilityAndroid* android_node =
       static_cast<BrowserAccessibilityAndroid*>(wrapper);
 
-  if (event_type == ui::AXEventGenerator::Event::CHILDREN_CHANGED) {
-    BrowserAccessibilityAndroid::ResetLeafCache();
-  }
-
   // Always send AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED to notify
   // the Android system that the accessibility hierarchy rooted at this
   // node has changed.
@@ -342,9 +337,6 @@
       break;
     }
     case ui::AXEventGenerator::Event::NAME_CHANGED: {
-      // Clear node from cache whenever the name changes to ensure fresh data.
-      wcax->ClearNodeInfoCacheForGivenId(android_node->GetUniqueId());
-
       // If this is a simple text element, also send an event to the framework.
       if (ui::IsText(android_node->GetRole()) ||
           android_node->IsAndroidTextView()) {
@@ -387,10 +379,6 @@
       }
       break;
 
-    case ui::AXEventGenerator::Event::CHILDREN_CHANGED:
-      wcax->ClearNodeInfoCacheForGivenId(android_node->GetUniqueId());
-      break;
-
     // Currently unused events on this platform.
     case ui::AXEventGenerator::Event::NONE:
     case ui::AXEventGenerator::Event::ACCESS_KEY_CHANGED:
@@ -403,6 +391,7 @@
     case ui::AXEventGenerator::Event::BUSY_CHANGED:
     case ui::AXEventGenerator::Event::CARET_BOUNDS_CHANGED:
     case ui::AXEventGenerator::Event::CHECKED_STATE_DESCRIPTION_CHANGED:
+    case ui::AXEventGenerator::Event::CHILDREN_CHANGED:
     case ui::AXEventGenerator::Event::CONTROLS_CHANGED:
     case ui::AXEventGenerator::Event::DETAILS_CHANGED:
     case ui::AXEventGenerator::Event::DESCRIBED_BY_CHANGED:
@@ -632,38 +621,54 @@
   }
 }
 
-void BrowserAccessibilityManagerAndroid::OnNodeWillBeDeleted(ui::AXTree* tree,
-                                                             ui::AXNode* node) {
-  // https://crbug.com/361196029 looks like a nullptr deref. It's unexpected
-  // that ui::AXTree would pass a null node to an observer, and that the
-  // manager would not have a BrowserAccessibility wrapper for it.
-  DUMP_WILL_BE_CHECK(node);
-  ui::BrowserAccessibility* wrapper = GetFromAXNode(node);
-  DUMP_WILL_BE_CHECK(wrapper);
-
-  BrowserAccessibilityAndroid* android_node =
-      static_cast<BrowserAccessibilityAndroid*>(wrapper);
-
-  ClearNodeInfoCacheForGivenId(android_node->GetUniqueId());
-
-  // When a node will be deleted, clear its parent from the cache as well, or
-  // the parent could erroneously report the cleared node as a child later on.
-  BrowserAccessibilityAndroid* parent_node =
-      static_cast<BrowserAccessibilityAndroid*>(
-          android_node->PlatformGetParent());
-  if (parent_node != nullptr) {
-    ClearNodeInfoCacheForGivenId(parent_node->GetUniqueId());
-  }
-
-  BrowserAccessibilityManager::OnNodeWillBeDeleted(tree, node);
-}
-
 std::unique_ptr<ui::BrowserAccessibility>
 BrowserAccessibilityManagerAndroid::CreateBrowserAccessibility(
     ui::AXNode* node) {
   return ui::BrowserAccessibility::Create(this, node);
 }
 
+void BrowserAccessibilityManagerAndroid::OnAtomicUpdateStarting(
+    ui::AXTree* tree,
+    const std::set<ui::AXNodeID>& deleting_nodes,
+    const std::set<ui::AXNodeID>& reparenting_nodes) {
+  WebContentsAccessibilityAndroid* wcax = GetWebContentsAXFromRootManager();
+  if (wcax) {
+    // This set needs to start fresh. This secondary cache is of requests to
+    // java to clear that primary cache of Android objects. The idea being only
+    // such such request is needed for each atomic update to the tree and node
+    // data.
+    nodes_already_cleared_.clear();
+
+    // Update the maximum number of nodes in the cache after each atomic update.
+    wcax->UpdateMaxNodesInCache();
+
+    for (ui::AXNodeID id : deleting_nodes) {
+      ui::BrowserAccessibility* wrapper = GetFromID(id);
+      if (!wrapper) {
+        continue;
+      }
+
+      BrowserAccessibilityAndroid* android_node =
+          static_cast<BrowserAccessibilityAndroid*>(wrapper);
+
+      ClearNodeInfoCacheForGivenId(android_node->GetUniqueId());
+
+      // When a node will be deleted, clear its parent from the cache as well,
+      // or the parent could erroneously report the cleared node as a child
+      // later on.
+      BrowserAccessibilityAndroid* parent_node =
+          static_cast<BrowserAccessibilityAndroid*>(
+              android_node->PlatformGetParent());
+      if (parent_node != nullptr) {
+        ClearNodeInfoCacheForGivenId(parent_node->GetUniqueId());
+      }
+    }
+  }
+
+  BrowserAccessibilityManager::OnAtomicUpdateStarting(tree, deleting_nodes,
+                                                      reparenting_nodes);
+}
+
 void BrowserAccessibilityManagerAndroid::OnAtomicUpdateFinished(
     ui::AXTree* tree,
     bool root_changed,
@@ -679,9 +684,6 @@
   // Reset content changed events counter every time we finish an atomic update.
   wcax->ResetContentChangedEventsCounter();
 
-  // Clear unordered_set of nodes cleared from the cache after atomic update.
-  nodes_already_cleared_.clear();
-
   // When the root changes, send the new root id and a navigate signal to Java.
   if (root_changed) {
     auto* root_manager = static_cast<BrowserAccessibilityManagerAndroid*>(
@@ -695,8 +697,26 @@
     wcax->HandleNavigate(root->GetUniqueId());
   }
 
-  // Update the maximum number of nodes in the cache after each atomic update.
-  wcax->UpdateMaxNodesInCache();
+  // Invalidate java-side cache for structural generated events. This
+  // encompasses less nodes than `changes`, but includes unignored retargeted
+  // event targets that isn't in `changes`. Eventually, we should prefer
+  // invalidations of generated events to those in
+  // BrowserAccessibility::OnDataChanged.
+  for (const auto& targeted_event : event_generator()) {
+    BrowserAccessibilityAndroid* wrapper =
+        static_cast<BrowserAccessibilityAndroid*>(
+            GetFromID(targeted_event.node_id));
+    CHECK(wrapper);
+
+    auto event_type = targeted_event.event_params->event;
+    if (event_type == ui::AXEventGenerator::Event::CHILDREN_CHANGED ||
+        event_type == ui::AXEventGenerator::Event::PARENT_CHANGED) {
+      // Structural changes in the unignored/platform tree requires the leaf
+      // cache be invalidated.
+      BrowserAccessibilityAndroid::ResetLeafCache();
+      ClearNodeInfoCacheForGivenId(wrapper->GetUniqueId());
+    }
+  }
 }
 
 WebContentsAccessibilityAndroid*
diff --git a/content/browser/accessibility/browser_accessibility_manager_android.h b/content/browser/accessibility/browser_accessibility_manager_android.h
index 0c4c7e2..6f36e6d 100644
--- a/content/browser/accessibility/browser_accessibility_manager_android.h
+++ b/content/browser/accessibility/browser_accessibility_manager_android.h
@@ -78,6 +78,10 @@
     allow_image_descriptions_for_testing_ = is_allowed;
   }
 
+  const std::unordered_set<int32_t>& nodes_already_cleared_for_test() const {
+    return nodes_already_cleared_;
+  }
+
   // By default, the tree is pruned for a better screen reading experience,
   // including:
   //   * If the node has only static text children
@@ -156,13 +160,15 @@
 
  private:
   // AXTreeObserver overrides.
+  void OnAtomicUpdateStarting(
+      ui::AXTree* tree,
+      const std::set<ui::AXNodeID>& deleting_nodes,
+      const std::set<ui::AXNodeID>& reparenting_nodes) override;
   void OnAtomicUpdateFinished(
       ui::AXTree* tree,
       bool root_changed,
       const std::vector<ui::AXTreeObserver::Change>& changes) override;
 
-  void OnNodeWillBeDeleted(ui::AXTree* tree, ui::AXNode* node) override;
-
   WebContentsAccessibilityAndroid* GetWebContentsAXFromRootManager();
 
   // This gives BrowserAccessibilityManager::Create access to the class
diff --git a/content/browser/accessibility/dump_accessibility_tree_browsertest.cc b/content/browser/accessibility/dump_accessibility_tree_browsertest.cc
index 16eabf2..e8e14cb 100644
--- a/content/browser/accessibility/dump_accessibility_tree_browsertest.cc
+++ b/content/browser/accessibility/dump_accessibility_tree_browsertest.cc
@@ -134,7 +134,7 @@
                                   "HeadingOffset");
   // Enable layout of canvas children with the layoutsubtree attribute.
   command_line->AppendSwitchASCII(switches::kEnableBlinkFeatures,
-                                  "CanvasElementDrawElement");
+                                  "CanvasDrawElement");
 }
 
 void DumpAccessibilityTreeTest::SetUpOnMainThread() {
diff --git a/content/browser/accessibility/web_contents_accessibility_android.cc b/content/browser/accessibility/web_contents_accessibility_android.cc
index 165412d..56b86d5 100644
--- a/content/browser/accessibility/web_contents_accessibility_android.cc
+++ b/content/browser/accessibility/web_contents_accessibility_android.cc
@@ -1016,6 +1016,8 @@
   return GetCanonicalJNIString(env, all_keys).AsLocalRef(env);
 }
 
+WebContentsAccessibilityAndroid::WebContentsAccessibilityAndroid() {}
+
 jint WebContentsAccessibilityAndroid::GetRootId(JNIEnv* env) {
   if (BrowserAccessibilityManagerAndroid* root_manager =
           GetRootBrowserAccessibilityManager()) {
diff --git a/content/browser/accessibility/web_contents_accessibility_android.h b/content/browser/accessibility/web_contents_accessibility_android.h
index c135373..a42e2a68 100644
--- a/content/browser/accessibility/web_contents_accessibility_android.h
+++ b/content/browser/accessibility/web_contents_accessibility_android.h
@@ -407,6 +407,10 @@
   base::WeakPtr<WebContentsAccessibilityAndroid> GetWeakPtr();
 
  private:
+  friend class MockWebContentsAccessibilityAndroid;
+
+  WebContentsAccessibilityAndroid();
+
   BrowserAccessibilityManagerAndroid* GetRootBrowserAccessibilityManager();
 
   BrowserAccessibilityAndroid* GetAXFromUniqueID(int32_t unique_id);
diff --git a/content/browser/date_time_chooser/ios/date_time_chooser_view_controller.mm b/content/browser/date_time_chooser/ios/date_time_chooser_view_controller.mm
index 786f4e7..cf06d0d 100644
--- a/content/browser/date_time_chooser/ios/date_time_chooser_view_controller.mm
+++ b/content/browser/date_time_chooser/ios/date_time_chooser_view_controller.mm
@@ -18,7 +18,7 @@
 // number of month. Otherwise, it's in milliseconds.
 @property(nonatomic, assign) NSInteger initTime;
 // Updated with the selected date in the date picker
-@property(nonatomic, assign) NSDate* selectedDate;
+@property(nonatomic, strong) NSDate* selectedDate;
 @end
 
 @implementation DateTimeChooserViewController
diff --git a/content/browser/devtools/protocol/preload_handler.cc b/content/browser/devtools/protocol/preload_handler.cc
index a7b4998..b158df20 100644
--- a/content/browser/devtools/protocol/preload_handler.cc
+++ b/content/browser/devtools/protocol/preload_handler.cc
@@ -543,8 +543,13 @@
       continue;
     }
 
+    std::optional<base::UnguessableToken> maybe_navigation_token =
+        document->GetDevToolsNavigationToken();
+    if (!maybe_navigation_token.has_value()) {
+      continue;
+    }
     const base::UnguessableToken initiator_devtools_navigation_token =
-        document->GetDevToolsNavigationToken().value();
+        maybe_navigation_token.value();
     const std::string initiating_frame_id =
         document->GetDevToolsFrameToken().ToString();
     for (const auto& [key, data] : preload_storage->prefetch_data_map()) {
diff --git a/content/browser/download/download_browsertest.cc b/content/browser/download/download_browsertest.cc
index c30be6ca..d65d7524 100644
--- a/content/browser/download/download_browsertest.cc
+++ b/content/browser/download/download_browsertest.cc
@@ -4120,6 +4120,8 @@
   std::unique_ptr<DownloadTestObserver> observer(CreateWaiter(shell(), 1));
   EXPECT_TRUE(NavigateToURL(shell(), referrer_url));
 
+  SimulateEndOfPaintHoldingOnPrimaryMainFrame(shell()->web_contents());
+
   // Alt-click the link.
   blink::WebMouseEvent mouse_event(
       blink::WebInputEvent::Type::kMouseDown, blink::WebInputEvent::kAltKey,
@@ -5538,6 +5540,7 @@
 
   // Load the download page and click on the link.
   EXPECT_TRUE(NavigateToURL(shell(), referrer_url));
+  SimulateEndOfPaintHoldingOnPrimaryMainFrame(shell()->web_contents());
   content::SimulateMouseClickOrTapElementWithId(shell()->web_contents(),
                                                 "downloadlink");
 
diff --git a/content/browser/interest_group/auction_runner.cc b/content/browser/interest_group/auction_runner.cc
index 39156da..78c69a83 100644
--- a/content/browser/interest_group/auction_runner.cc
+++ b/content/browser/interest_group/auction_runner.cc
@@ -192,6 +192,42 @@
   NotifyPromiseResolved(*auction_id, *config);
 }
 
+void AuctionRunner::ResolvedBuyerTkvSignalsPromise(
+    blink::mojom::AuctionAdConfigAuctionIdPtr auction_id,
+    const url::Origin& buyer,
+    const std::optional<std::string>& buyer_tkv_signals) {
+  if (state_ == State::kFailed) {
+    return;
+  }
+
+  blink::AuctionConfig* config =
+      LookupAuction(*owned_auction_config_, *auction_id);
+  if (!config) {
+    mojo::ReportBadMessage(
+        "Invalid auction ID in ResolvedPerBuyerSignalsPromise");
+    return;
+  }
+
+  auto tvk_signals_it =
+      config->non_shared_params.per_buyer_tkv_signals.find(buyer);
+  if (tvk_signals_it == config->non_shared_params.per_buyer_tkv_signals.end() ||
+      !tvk_signals_it->second.is_promise()) {
+    mojo::ReportBadMessage(
+        "ResolvedBuyerTkvSignalsPromise may only update a promise");
+    return;
+  }
+
+  tvk_signals_it->second =
+      blink::AuctionConfig::MaybePromiseJson::FromValue(buyer_tkv_signals);
+
+  // The order these two notifications are send in should not matter.
+  auction_.NotifyBuyerTkvSignalsPromiseResolved(
+      buyer, auction_id->is_main_auction()
+                 ? std::optional<uint32_t>()
+                 : auction_id->get_component_auction());
+  NotifyPromiseResolved(*auction_id, *config);
+}
+
 void AuctionRunner::ResolvedBuyerTimeoutsPromise(
     blink::mojom::AuctionAdConfigAuctionIdPtr auction_id,
     blink::mojom::AuctionAdConfigBuyerTimeoutField field,
diff --git a/content/browser/interest_group/auction_runner.h b/content/browser/interest_group/auction_runner.h
index aafc73a1..1091dbf 100644
--- a/content/browser/interest_group/auction_runner.h
+++ b/content/browser/interest_group/auction_runner.h
@@ -197,6 +197,10 @@
       blink::mojom::AuctionAdConfigAuctionIdPtr auction_id,
       const std::optional<base::flat_map<url::Origin, std::string>>&
           per_buyer_signals) override;
+  void ResolvedBuyerTkvSignalsPromise(
+      blink::mojom::AuctionAdConfigAuctionIdPtr auction_id,
+      const url::Origin& buyer,
+      const std::optional<std::string>& buyer_tkv_signals) override;
   void ResolvedBuyerTimeoutsPromise(
       blink::mojom::AuctionAdConfigAuctionIdPtr auction_id,
       blink::mojom::AuctionAdConfigBuyerTimeoutField field,
diff --git a/content/browser/interest_group/auction_runner_unittest.cc b/content/browser/interest_group/auction_runner_unittest.cc
index 0db1ac0..331ffdf 100644
--- a/content/browser/interest_group/auction_runner_unittest.cc
+++ b/content/browser/interest_group/auction_runner_unittest.cc
@@ -2472,7 +2472,7 @@
 
     std::unique_ptr<TrustedSignalsCacheImpl> trusted_signals_cache;
     if (!auction_process_manager_) {
-      trusted_signals_cache = GetTrustedSignalsCache();
+      trusted_signals_cache = TakeTrustedSignalsCache();
       auto same_process_auction_process_manager =
           std::make_unique<TestSameProcessAuctionProcessManager>(
               trusted_signals_cache.get());
@@ -3820,8 +3820,9 @@
 
   // Returns a TrustedSignalsCacheImpl to be used when setting up the next
   // auction.
-  virtual std::unique_ptr<TrustedSignalsCacheImpl> GetTrustedSignalsCache() {
-    // Use one by default. This should fail the test if it's unexpected used.
+  virtual std::unique_ptr<TrustedSignalsCacheImpl> TakeTrustedSignalsCache() {
+    // Use one that expects no requests by default. This will fail the test if
+    // it's unexpectedly used.
     return std::make_unique<MockTrustedSignalsCacheImpl>(
         &data_decoder_manager_);
   }
@@ -3856,7 +3857,8 @@
   std::optional<uint16_t> seller_experiment_group_id_;
   std::optional<uint16_t> all_buyer_experiment_group_id_;
   std::map<url::Origin, uint16_t> per_buyer_experiment_group_id_;
-  base::flat_map<url::Origin, std::string> per_buyer_tkv_signals_;
+  base::flat_map<url::Origin, blink::AuctionConfig::MaybePromiseJson>
+      per_buyer_tkv_signals_;
   uint16_t all_buyers_group_limit_ = std::numeric_limits<std::uint16_t>::max();
   std::optional<base::flat_map<std::string, double>>
       all_buyers_priority_signals_;
@@ -4047,7 +4049,23 @@
 
   bool UsingKVv2Signals() const override { return GetParam(); }
 
-  std::unique_ptr<TrustedSignalsCacheImpl> GetTrustedSignalsCache() override {
+  // Returns the MockTrustedSignalsCacheImpl, initializing it if needed. May
+  // only be called before an auction is started, as that will create a
+  // TestInterestGroupManagerImpl that owns the cache. Returned pointer may be
+  // held onto and used until the auction completes. Returns a pointer rather
+  // than a reference to remind callers that hang onto references to be careful
+  // about object lifetimes.
+  MockTrustedSignalsCacheImpl* GetTrustedSignalsCache() {
+    // Only makes sense to call this when using KVv2 signals.
+    CHECK(UsingKVv2Signals());
+    if (!trusted_signals_cache_impl_) {
+      trusted_signals_cache_impl_ =
+          std::make_unique<MockTrustedSignalsCacheImpl>(&data_decoder_manager_);
+    }
+    return trusted_signals_cache_impl_.get();
+  }
+
+  std::unique_ptr<TrustedSignalsCacheImpl> TakeTrustedSignalsCache() override {
     if (trusted_signals_cache_impl_) {
       // There shouldn't be a `trusted_signals_cache_impl_` if not using KVv2
       // signals. We create a MockTrustedSignalsCache in that case, anyways, but
@@ -4066,13 +4084,7 @@
   void AddBiddingSignalsCacheResult(
       MockTrustedSignalsCacheImpl::BidderRequestInfo bidder_request_info,
       TrustedSignalsFetcher::SignalsFetchResult signals_fetch_result) {
-    // Only makes sense to call this when using KVv2 signals.
-    CHECK(UsingKVv2Signals());
-    if (!trusted_signals_cache_impl_) {
-      trusted_signals_cache_impl_ =
-          std::make_unique<MockTrustedSignalsCacheImpl>(&data_decoder_manager_);
-    }
-    trusted_signals_cache_impl_->AddBidderSignalsResult(
+    GetTrustedSignalsCache()->AddBidderSignalsResult(
         std::move(bidder_request_info), std::move(signals_fetch_result));
   }
 
@@ -4131,13 +4143,7 @@
   void AddScoringSignalsCacheResult(
       MockTrustedSignalsCacheImpl::SellerRequestInfo seller_request_info,
       TrustedSignalsFetcher::SignalsFetchResult signals_fetch_result) {
-    // Only makes sense to call this when using KVv2 signals.
-    CHECK(UsingKVv2Signals());
-    if (!trusted_signals_cache_impl_) {
-      trusted_signals_cache_impl_ =
-          std::make_unique<MockTrustedSignalsCacheImpl>(&data_decoder_manager_);
-    }
-    trusted_signals_cache_impl_->AddSellerSignalsResult(
+    GetTrustedSignalsCache()->AddSellerSignalsResult(
         std::move(seller_request_info), std::move(signals_fetch_result));
   }
 
@@ -27980,8 +27986,7 @@
 // MockTrustedSignalsCache that exactly the expected signals requests were made
 // to the cache.
 TEST_P(AuctionRunnerTrustedSignalsTest, TrustedSignalsKVv2BuyerTKVSignals) {
-  // Only KVv2 request contains contextual data which is buyer_tkv_signals for
-  // bidding signals.
+  // Only KVv2 requests support `buyer_tkv_signals`.
   if (!UsingKVv2Signals()) {
     return;
   }
@@ -27990,7 +27995,8 @@
   feature_list.InitAndEnableFeature(
       blink::features::kFledgeTrustedSignalsKVv2ContextualData);
 
-  per_buyer_tkv_signals_[kBidder1] = "signals";
+  per_buyer_tkv_signals_[kBidder1] =
+      blink::AuctionConfig::MaybePromiseJson::FromValue("signals");
   auction_worklet::AddJavascriptResponse(
       &url_loader_factory_, kBidder1Url,
       MakeConstBidScript(1, "https://ad1.com/"));
@@ -28015,14 +28021,117 @@
   EXPECT_EQ(GURL("https://ad1.com/"), result_.ad_descriptor->url);
 }
 
-// Test that `buyer_tkv_signals` set with bidder2 will not show up in the
-// request of bidder1's trusted KVv2 bidding signals. This test completely
-// depends on the checks in MockTrustedSignalsCache that exactly the expected
-// signals requests were made to the cache.
+// Test that when the `buyer_tkv_signals` is a promise, the auction is delayed
+// until it's resolved, and respects the passed in value.
 TEST_P(AuctionRunnerTrustedSignalsTest,
-       TrustedSignalsKVv2BuyerTKVSignalsWithWrongBuyer) {
-  // Only KVv2 request contains contextual data which is buyer_tkv_signals for
-  // bidding signals.
+       TrustedSignalsKVv2BuyerTKVSignalsPromise) {
+  // Only KVv2 requests support `buyer_tkv_signals`.
+  if (!UsingKVv2Signals()) {
+    return;
+  }
+
+  base::test::ScopedFeatureList feature_list;
+  feature_list.InitAndEnableFeature(
+      blink::features::kFledgeTrustedSignalsKVv2ContextualData);
+
+  per_buyer_tkv_signals_[kBidder1] =
+      blink::AuctionConfig::MaybePromiseJson::FromPromise();
+  auction_worklet::AddJavascriptResponse(
+      &url_loader_factory_, kBidder1Url,
+      MakeConstBidScript(1, "https://ad1.com/"));
+  auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl,
+                                         kMinimumDecisionScript);
+
+  std::vector<StorageInterestGroup> bidders;
+  bidders.emplace_back(MakeInterestGroup(
+      kBidder1, kBidder1Name, kBidder1Url, kBidder1TrustedSignalsUrl,
+      {"k1", "k2"}, GURL("https://ad1.com"), /*ad_component_urls=*/std::nullopt,
+      coordinator_origin_));
+
+  // Get a pointer to the cache, so can add an entry after the auction starts.
+  auto* trusted_signals_cache = GetTrustedSignalsCache();
+
+  // The auction should not complete until the promise is resolved.
+  StartAuction(kSellerUrl, std::move(bidders));
+  task_environment()->RunUntilIdle();
+  EXPECT_FALSE(auction_complete_);
+
+  // Set up KVv2 signals request expectations.
+  auto bidder1_request_info = DefaultBidder1SignalsRequestInfo();
+  bidder1_request_info.compression_groups[0][0].buyer_tkv_signals = "signals";
+  // The actual response doesn't matter - this test depends on the logic in
+  // MockTrustedSignalsCache to verify all the expected requests were sent, with
+  // the correct parameters.
+  trusted_signals_cache->AddBidderSignalsResult(
+      std::move(bidder1_request_info), MakeBidder1CompressionGroupMap());
+
+  // Promise is resolved.
+  abortable_ad_auction_->ResolvedBuyerTkvSignalsPromise(
+      blink::mojom::AuctionAdConfigAuctionId::NewMainAuction(0), kBidder1,
+      "signals");
+
+  // The auction should complete successfully.
+  auction_run_loop_->Run();
+  EXPECT_EQ(GURL("https://ad1.com/"), result_.ad_descriptor->url);
+}
+
+// Test that when the `buyer_tkv_signals` is a promise, the auction is delayed
+// until it's rejected, and then sends the request with no signals.
+TEST_P(AuctionRunnerTrustedSignalsTest,
+       TrustedSignalsKVv2BuyerTKVSignalsPromiseRejected) {
+  // Only KVv2 requests support `buyer_tkv_signals`.
+  if (!UsingKVv2Signals()) {
+    return;
+  }
+
+  base::test::ScopedFeatureList feature_list;
+  feature_list.InitAndEnableFeature(
+      blink::features::kFledgeTrustedSignalsKVv2ContextualData);
+
+  per_buyer_tkv_signals_[kBidder1] =
+      blink::AuctionConfig::MaybePromiseJson::FromPromise();
+  auction_worklet::AddJavascriptResponse(
+      &url_loader_factory_, kBidder1Url,
+      MakeConstBidScript(1, "https://ad1.com/"));
+  auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl,
+                                         kMinimumDecisionScript);
+
+  std::vector<StorageInterestGroup> bidders;
+  bidders.emplace_back(MakeInterestGroup(
+      kBidder1, kBidder1Name, kBidder1Url, kBidder1TrustedSignalsUrl,
+      {"k1", "k2"}, GURL("https://ad1.com"), /*ad_component_urls=*/std::nullopt,
+      coordinator_origin_));
+
+  // Get a pointer to the cache, so can add an entry after the auction starts.
+  auto* trusted_signals_cache = GetTrustedSignalsCache();
+
+  // The auction should not complete until the promise is resolved.
+  StartAuction(kSellerUrl, std::move(bidders));
+  task_environment()->RunUntilIdle();
+  EXPECT_FALSE(auction_complete_);
+
+  // The actual response doesn't matter - this test depends on the logic in
+  // MockTrustedSignalsCache to verify all the expected requests were sent, with
+  // the correct parameters.
+  trusted_signals_cache->AddBidderSignalsResult(
+      DefaultBidder1SignalsRequestInfo(), MakeBidder1CompressionGroupMap());
+
+  // Promise is resolved.
+  abortable_ad_auction_->ResolvedBuyerTkvSignalsPromise(
+      blink::mojom::AuctionAdConfigAuctionId::NewMainAuction(0), kBidder1,
+      std::nullopt);
+
+  // The auction should complete successfully.
+  auction_run_loop_->Run();
+  EXPECT_EQ(GURL("https://ad1.com/"), result_.ad_descriptor->url);
+}
+
+// Test that a `buyer_tkv_signals` promise will delay auction completion, even
+// when no IG is using KVv2, to avoid leaking that fact to the renderer process.
+TEST_P(AuctionRunnerTrustedSignalsTest,
+       BuyerTKVSignalsWithPromiseButNoTrustedBiddingSignalsUrl) {
+  // This test doesn't actually use a trusted signals URL, so no need to run
+  // KVv1 and KVv2 variants.
   if (!UsingKVv2Signals()) {
     return;
   }
@@ -28033,7 +28142,101 @@
 
   // Set up per_buyer_tkv_signals with only bidder2's origin and "signals" will
   // not show up in bidder1's bidding signals request.
-  per_buyer_tkv_signals_[kBidder2] = "signals";
+  per_buyer_tkv_signals_[kBidder1] =
+      blink::AuctionConfig::MaybePromiseJson::FromPromise();
+  auction_worklet::AddJavascriptResponse(
+      &url_loader_factory_, kBidder1Url,
+      MakeConstBidScript(1, "https://ad1.com/"));
+  auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl,
+                                         kMinimumDecisionScript);
+
+  std::vector<StorageInterestGroup> bidders;
+  bidders.emplace_back(
+      MakeInterestGroup(kBidder1, kBidder1Name, kBidder1Url,
+                        /*trusted_bidding_signals_url=*/std::nullopt,
+                        {"k1", "k2"}, GURL("https://ad1.com")));
+
+  // The auction should not complete until the promise is resolved.
+  StartAuction(kSellerUrl, std::move(bidders));
+  task_environment()->RunUntilIdle();
+  EXPECT_FALSE(auction_complete_);
+
+  // Promise is resolved.
+  abortable_ad_auction_->ResolvedBuyerTkvSignalsPromise(
+      blink::mojom::AuctionAdConfigAuctionId::NewMainAuction(0), kBidder1,
+      std::nullopt);
+
+  // The auction should complete successfully.
+  auction_run_loop_->Run();
+  EXPECT_EQ(GURL("https://ad1.com/"), result_.ad_descriptor->url);
+}
+
+// Test that `buyer_tkv_signals` set with bidder2 will not show up in the
+// request of bidder1's trusted KVv2 bidding signals. This test completely
+// depends on the checks in MockTrustedSignalsCache that exactly the expected
+// signals requests were made to the cache.
+TEST_P(AuctionRunnerTrustedSignalsTest,
+       TrustedSignalsKVv2BuyerTKVSignalsWithPromiseForWrongBuyer) {
+  // Only KVv2 requests support `buyer_tkv_signals`.
+  if (!UsingKVv2Signals()) {
+    return;
+  }
+
+  base::test::ScopedFeatureList feature_list;
+  feature_list.InitAndEnableFeature(
+      blink::features::kFledgeTrustedSignalsKVv2ContextualData);
+
+  // Set up per_buyer_tkv_signals with only bidder2's origin and "signals" will
+  // not show up in bidder1's bidding signals request.
+  per_buyer_tkv_signals_[kBidder2] =
+      blink::AuctionConfig::MaybePromiseJson::FromPromise();
+  auction_worklet::AddJavascriptResponse(
+      &url_loader_factory_, kBidder1Url,
+      MakeConstBidScript(1, "https://ad1.com/"));
+  auction_worklet::AddJavascriptResponse(&url_loader_factory_, kSellerUrl,
+                                         kMinimumDecisionScript);
+
+  std::vector<StorageInterestGroup> bidders;
+  bidders.emplace_back(MakeInterestGroup(
+      kBidder1, kBidder1Name, kBidder1Url, kBidder1TrustedSignalsUrl,
+      {"k1", "k2"}, GURL("https://ad1.com"), /*ad_component_urls=*/std::nullopt,
+      coordinator_origin_));
+
+  AddDefaultBidder1SignalsResult();
+
+  // The auction should not complete until the promise is resolved.
+  StartAuction(kSellerUrl, std::move(bidders));
+  task_environment()->RunUntilIdle();
+  EXPECT_FALSE(auction_complete_);
+
+  // Promise is resolved.
+  abortable_ad_auction_->ResolvedBuyerTkvSignalsPromise(
+      blink::mojom::AuctionAdConfigAuctionId::NewMainAuction(0), kBidder2,
+      std::nullopt);
+
+  // The auction should complete successfully.
+  auction_run_loop_->Run();
+  EXPECT_EQ(GURL("https://ad1.com/"), result_.ad_descriptor->url);
+}
+
+// Test that `buyer_tkv_signals` set with a promise for bidder2 will not show up
+// in the request of bidder1's trusted KVv2 bidding signals, but the auction
+// will still be delayed until the promise is resolved, to avoid leaking data.
+TEST_P(AuctionRunnerTrustedSignalsTest,
+       TrustedSignalsKVv2BuyerTKVSignalsWithWrongBuyer) {
+  // Only KVv2 requests support `buyer_tkv_signals`.
+  if (!UsingKVv2Signals()) {
+    return;
+  }
+
+  base::test::ScopedFeatureList feature_list;
+  feature_list.InitAndEnableFeature(
+      blink::features::kFledgeTrustedSignalsKVv2ContextualData);
+
+  // Set up per_buyer_tkv_signals with only bidder2's origin and "signals" will
+  // not show up in bidder1's bidding signals request.
+  per_buyer_tkv_signals_[kBidder2] =
+      blink::AuctionConfig::MaybePromiseJson::FromValue("signals");
   auction_worklet::AddJavascriptResponse(
       &url_loader_factory_, kBidder1Url,
       MakeConstBidScript(1, "https://ad1.com/"));
@@ -28057,13 +28260,13 @@
 // the expected signals requests were made to the cache.
 TEST_P(AuctionRunnerTrustedSignalsTest,
        TrustedSignalsKVv2BuyerTKVSignalsFeatureDisabled) {
-  // Only KVv2 request contains contextual data which is buyer_tkv_signals for
-  // bidding signals.
+  // Only KVv2 requests support `buyer_tkv_signals`.
   if (!UsingKVv2Signals()) {
     return;
   }
 
-  per_buyer_tkv_signals_[kBidder1] = "signals";
+  per_buyer_tkv_signals_[kBidder1] =
+      blink::AuctionConfig::MaybePromiseJson::FromValue("signals");
   auction_worklet::AddJavascriptResponse(
       &url_loader_factory_, kBidder1Url,
       MakeConstBidScript(1, "https://ad1.com/"));
diff --git a/content/browser/interest_group/interest_group_auction.cc b/content/browser/interest_group/interest_group_auction.cc
index 230d99d..d6e5e76 100644
--- a/content/browser/interest_group/interest_group_auction.cc
+++ b/content/browser/interest_group/interest_group_auction.cc
@@ -2118,6 +2118,21 @@
         selected_buyer_and_seller_reporting_id, bid_state, auction_);
   }
 
+  void OnBuyerTkvPromiseResolved() {
+    CHECK(!GetTkvSignals()->is_promise());
+
+    // Call MaybeBeginGenerateBid() for all bid states that were waiting on the
+    // promise.
+    for (auto& bid_state : bid_states_) {
+      if (!bid_state->waiting_for_tkv_promise) {
+        continue;
+      }
+
+      bid_state->waiting_for_tkv_promise = false;
+      MaybeBeginGenerateBid(bid_state.get());
+    }
+  }
+
  private:
   // Sorts by descending priority, also grouping entries within each priority
   // band to permit context reuse if the executionMode allows it.
@@ -2203,6 +2218,8 @@
       // bidding signals from being requested entirely.
       bid_state->bidding_signals_handle.reset();
 
+      bid_state->waiting_for_tkv_promise = false;
+
       OnBeginGenerateBidCalled(bid_state);
     }
 
@@ -2282,10 +2299,26 @@
       bidder_process_received_ = true;
       MaybeStartCumulativeTimeoutTimer();
     }
+    MaybeBeginGenerateBid(bid_state);
+  }
+
+  void MaybeBeginGenerateBid(BidState* bid_state) {
+    DCHECK(!bid_state->waiting_for_tkv_promise);
 
     const blink::InterestGroup& interest_group =
         bid_state->bidder->interest_group;
 
+    // Delay call to BeginGenerateBid() if need signals but can't request
+    // them from the cache yet.
+    if (NeedsBiddingSignalsFromCache(interest_group)) {
+      const blink::AuctionConfig::MaybePromiseJson* tkv_signals =
+          GetTkvSignals();
+      if (tkv_signals && tkv_signals->is_promise()) {
+        bid_state->waiting_for_tkv_promise = true;
+        return;
+      }
+    }
+
     mojo::PendingAssociatedRemote<auction_worklet::mojom::GenerateBidClient>
         pending_remote;
     bid_state->generate_bid_client_receiver_id =
@@ -2348,16 +2381,22 @@
     FinishGenerateBidIfReady(bid_state);
   }
 
+  bool NeedsBiddingSignalsFromCache(
+      const blink::InterestGroup& interest_group) {
+    // Only need signals from the cache if there's a coordinator (indicating use
+    // of KVv2 signals), a trusted bidding signals URL, and the cache is enabled
+    // (and thus non-null).
+    return interest_group.trusted_bidding_signals_coordinator &&
+           interest_group.trusted_bidding_signals_url &&
+           auction_->interest_group_manager_->trusted_signals_cache();
+  }
+
   // Requests trusted bidding signals from the browser-side cache if needed.
   auction_worklet::mojom::TrustedSignalsCacheKeyPtr
   MaybeRequestBiddingSignalsFromCache(BidState& bid_state) {
     const blink::InterestGroup& interest_group =
         bid_state.bidder->interest_group;
-    // If the interest group is not using KVv2 bidding signals, or the
-    // TrustedSignalsCache is not enabled, return nullptr.
-    if (!interest_group.trusted_bidding_signals_coordinator ||
-        !interest_group.trusted_bidding_signals_url ||
-        !auction_->interest_group_manager_->trusted_signals_cache()) {
+    if (!NeedsBiddingSignalsFromCache(interest_group)) {
       return nullptr;
     }
 
@@ -2378,6 +2417,11 @@
                             std::move(optional_pair->second));
     }
 
+    const blink::AuctionConfig::MaybePromiseJson* tkv_signals = GetTkvSignals();
+    // This method must only be called once any applicable buyer TKV signals
+    // promise is resolved.
+    CHECK(!tkv_signals || !tkv_signals->is_promise());
+
     int partition_id;
     bid_state.bidding_signals_handle =
         auction_->interest_group_manager_->trusted_signals_cache()
@@ -2392,7 +2436,8 @@
                 *interest_group.trusted_bidding_signals_coordinator,
                 interest_group.trusted_bidding_signals_keys,
                 std::move(additional_params),
-                auction_->GetBuyerTKVSignals(owner_), partition_id);
+                tkv_signals ? tkv_signals->value() : std::nullopt,
+                partition_id);
     return auction_worklet::mojom::TrustedSignalsCacheKey::New(
         bid_state.bidding_signals_handle->compression_group_token(),
         partition_id);
@@ -2410,6 +2455,8 @@
       return;
     }
 
+    CHECK(!bid_state->waiting_for_tkv_promise);
+
     SubresourceUrlBuilder* url_builder =
         auction_->SubresourceUrlBuilderIfReady();
     if (url_builder) {
@@ -2589,6 +2636,7 @@
       const std::vector<std::string>& errors) {
     DCHECK(!state->made_bid);
     DCHECK_GT(num_outstanding_bids_, 0);
+    DCHECK(!state->waiting_for_tkv_promise);
 
     // We may not have a trace ID if we timed out before being delivered a
     // worklet.
@@ -3019,6 +3067,11 @@
     state.bidding_signals_handle.reset();
   }
 
+  const blink::AuctionConfig::MaybePromiseJson* GetTkvSignals() const {
+    // TODO(crbug.com/412588114): Consider caching a raw pointer to this.
+    return auction_->InterestGroupAuction::GetBuyerTKVSignals(owner_);
+  }
+
   size_t size_limit_;
 
   const raw_ptr<InterestGroupAuction> auction_;
@@ -3891,6 +3944,36 @@
   it->second->NotifyConfigPromisesResolved();
 }
 
+void InterestGroupAuction::NotifyBuyerTkvSignalsPromiseResolved(
+    const url::Origin& buyer,
+    std::optional<uint32_t> pos) {
+  if (pos.has_value()) {
+    // If `pos` has a value, this should be a top-level auction.
+    DCHECK(!parent_);
+    auto it = component_auctions_.find(*pos);
+
+    if (it == component_auctions_.end()) {
+      // It's OK if the component auction isn't found; that means it got dropped
+      // at database loading stage.
+      return;
+    }
+
+    it->second->NotifyBuyerTkvSignalsPromiseResolved(buyer, std::nullopt);
+    return;
+  }
+
+  // TODO(https://crbug.com/412588114): Maybe switch to a map, to avoid the
+  // linear search?
+  for (const auto& buyer_helper : buyer_helpers_) {
+    if (buyer_helper->owner() == buyer) {
+      buyer_helper->OnBuyerTkvPromiseResolved();
+      return;
+    }
+  }
+  // It's fine for there not to be a buyer helper for an origin. This can happen
+  // if a buyer has no interest groups that can participate in an auction.
+}
+
 void InterestGroupAuction::NotifyAdditionalBidsConfig(
     AdAuctionPageData& auction_page_data) {
   // An auction with additional bids can't have child auctions.
@@ -4790,19 +4873,19 @@
   return std::max(val, uint16_t{1});
 }
 
-std::optional<std::string> InterestGroupAuction::GetBuyerTKVSignals(
-    const url::Origin& owner) const {
+const blink::AuctionConfig::MaybePromiseJson*
+InterestGroupAuction::GetBuyerTKVSignals(const url::Origin& owner) const {
   if (!base::FeatureList::IsEnabled(
           blink::features::kFledgeTrustedSignalsKVv2ContextualData)) {
-    return std::nullopt;
+    return nullptr;
   }
 
   auto it = config_->non_shared_params.per_buyer_tkv_signals.find(owner);
-  if (it != config_->non_shared_params.per_buyer_tkv_signals.end()) {
-    return it->second;
+  if (it == config_->non_shared_params.per_buyer_tkv_signals.end()) {
+    return nullptr;
   }
 
-  return std::nullopt;
+  return &it->second;
 }
 
 std::optional<uint16_t> InterestGroupAuction::GetBuyerExperimentId(
diff --git a/content/browser/interest_group/interest_group_auction.h b/content/browser/interest_group/interest_group_auction.h
index 89751b9..725f306a 100644
--- a/content/browser/interest_group/interest_group_auction.h
+++ b/content/browser/interest_group/interest_group_auction.h
@@ -321,6 +321,12 @@
     // True if the bid is created from parsing B&A server response.
     bool is_from_server_response = false;
 
+    // True if this BidState has received a bidder worklet but now needs to wait
+    // on the bidder's `per_buyer_tkv_signals` promise being resolved before
+    // sending a KVv2 signals fetch and calling BeginGenerateBid() on the bidder
+    // worklet.
+    bool waiting_for_tkv_promise = false;
+
     // forDebuggingOnly reports that have been filtered (also sampled) by the
     // B&A server.
     std::map<url::Origin, std::vector<GURL>>
@@ -623,6 +629,22 @@
   // a parent auction.
   void NotifyComponentConfigPromisesResolved(uint32_t pos);
 
+  // Called by AuctionRunner when a buyer's TKV signals promise has been
+  // resolved or rejected. `pos` is nullopt if this is a promise in the
+  // top-level auction, and the index of a component auction if it's the buyer's
+  // TKV signals in a component auction.
+  //
+  // AuctionConfig must already have been updated to reflect the result of the
+  // promise before calling.
+  //
+  // This is a separate method because it delayed GenerateBid() calls for the
+  // interest groups of `buyer` groups using TKVv2, not just the
+  // FinishedGenerateBid() calls, like other promises.
+  //
+  // Assumes that `pos` has already been range-checked.
+  void NotifyBuyerTkvSignalsPromiseResolved(const url::Origin& buyer,
+                                            std::optional<uint32_t> pos);
+
   // Called by AuctionRunner when the promise providing the additional_bids
   // array has been resolved, if one exists. Unlike other similar methods,
   // `auction_page_data` may be null.
@@ -1211,8 +1233,9 @@
   uint16_t GetBuyerMultiBidLimit(const url::Origin& buyer);
 
   // Gets the buyer `per-buyer-tkv-signals` in `config` for interest group
-  // buyer.
-  std::optional<std::string> GetBuyerTKVSignals(const url::Origin& buyer) const;
+  // buyer. Returns nullptr if no such signals exist.
+  const blink::AuctionConfig::MaybePromiseJson* GetBuyerTKVSignals(
+      const url::Origin& buyer) const;
 
   // -----------------------------------
   // Methods not associated with a phase
diff --git a/content/browser/interest_group/interest_group_browsertest.cc b/content/browser/interest_group/interest_group_browsertest.cc
index 2383ed931..f7efa82 100644
--- a/content/browser/interest_group/interest_group_browsertest.cc
+++ b/content/browser/interest_group/interest_group_browsertest.cc
@@ -6454,43 +6454,17 @@
 
   AttachInterestGroupObserver();
 
-  EXPECT_EQ(base::StringPrintf(
-                "TypeError: Failed to execute 'runAdAuction' on 'Navigator': "
-                "perBuyerTKVSignals for AuctionAdConfig with seller '%s' must "
-                "be a JSON-serializable object.",
-                test_origin.Serialize().c_str()),
-            RunAuctionAndWait(JsReplace(R"({
+  EXPECT_EQ(
+      "TypeError: Failed to execute 'runAdAuction' on 'Navigator': Failed to "
+      "read the 'perBuyerTKVSignals' property from 'AuctionAdConfig': Only "
+      "objects can be converted to record<K,V> types",
+      RunAuctionAndWait(JsReplace(R"({
       seller: $1,
       decisionLogicURL: $2,
-      perBuyerTKVSignals: {'https://test.com': function() {}},
+      perBuyerTKVSignals: 52,
       interestGroupBuyers: []
   })",
-                                        test_origin, decision_url)));
-  WaitForAccessObserved({});
-}
-
-IN_PROC_BROWSER_TEST_F(InterestGroupBrowserTest,
-                       RunAdAuctionUndefinedPerBuyerTKVSignals) {
-  GURL test_url = embedded_https_test_server().GetURL("a.test", "/echo");
-  url::Origin test_origin = url::Origin::Create(test_url);
-  GURL decision_url = embedded_https_test_server().GetURL(
-      "a.test", "/interest_group/decision_logic.js");
-  ASSERT_TRUE(NavigateToURL(shell(), test_url));
-
-  AttachInterestGroupObserver();
-
-  EXPECT_EQ(base::StringPrintf(
-                "TypeError: Failed to execute 'runAdAuction' on 'Navigator': "
-                "perBuyerTKVSignals for AuctionAdConfig with seller '%s' must "
-                "be a JSON-serializable object.",
-                test_origin.Serialize().c_str()),
-            RunAuctionAndWait(JsReplace(R"({
-      seller: $1,
-      decisionLogicURL: $2,
-      perBuyerTKVSignals: {'https://test.com': undefined},
-      interestGroupBuyers: []
-  })",
-                                        test_origin, decision_url)));
+                                  test_origin, decision_url)));
   WaitForAccessObserved({});
 }
 
@@ -29726,6 +29700,20 @@
     InterestGroupTrustedSignalsKVv2ContextualDataBrowserTest,
     testing::Values(true));
 
+// Test that providing an invalid `buyer_tkv_signals` value for a buyer results
+// in the auction still running, but providing no contextual data.
+IN_PROC_BROWSER_TEST_P(InterestGroupTrustedSignalsKVv2ContextualDataBrowserTest,
+                       BuyerTkvSignalsRunAdAuctionBadPerBuyerData) {
+  TestPerBuyerTKVSignals(/*expected_bidding_key=*/kNoContextualDataValue,
+                         /*buyer_tkv_signals=*/"function(){}");
+}
+
+IN_PROC_BROWSER_TEST_P(InterestGroupTrustedSignalsKVv2ContextualDataBrowserTest,
+                       PerBuyerTKVSignalsIsUndefined) {
+  TestPerBuyerTKVSignals(/*expected_bidding_key=*/kNoContextualDataValue,
+                         /*buyer_tkv_signals=*/"undefined");
+}
+
 IN_PROC_BROWSER_TEST_P(InterestGroupTrustedSignalsKVv2ContextualDataBrowserTest,
                        PerBuyerTKVSignalsIsInteger) {
   TestPerBuyerTKVSignals(/*expected_bidding_key=*/"100",
@@ -29750,6 +29738,130 @@
                          /*buyer_tkv_signals=*/"null");
 }
 
+// Passing in a rejected promise as a `perBuyerTKVSignal` value currently fails
+// the auction.
+//
+// TODO(crbug.com/412588114): Make a rejected promise result in sending no
+// signals instead.
+IN_PROC_BROWSER_TEST_P(InterestGroupTrustedSignalsKVv2ContextualDataBrowserTest,
+                       PerBuyerTKVSignalsAlreadyRejectedPromise) {
+  GURL test_url =
+      embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
+  ASSERT_TRUE(NavigateToURL(shell(), test_url));
+  url::Origin test_origin = url::Origin::Create(test_url);
+  // Buyer without any joined interest groups.
+  url::Origin other_buyer_origin =
+      embedded_https_test_server().GetOrigin("b.test");
+  GURL ad_url = GURL("https://ad.test/");
+
+  // Join an interest group without a trusted bidding signals URL. Rejecting the
+  // promise should still fail the auction.
+  EXPECT_EQ(kSuccess,
+            JoinInterestGroupAndVerify(
+                blink::TestInterestGroupBuilder(
+                    /*owner=*/test_origin,
+                    /*name=*/"cars")
+                    .SetBiddingUrl(embedded_https_test_server().GetURL(
+                        "a.test", "/interest_group/bidding_logic.js"))
+                    .SetAds(/*ads=*/{{{ad_url, /*metadata=*/std::nullopt}}})
+                    .Build()));
+
+  for (const auto& origin_with_signals : {test_origin, other_buyer_origin}) {
+    SCOPED_TRACE(origin_with_signals.Serialize());
+    std::string auction_config = JsReplace(
+        R"({
+          seller: $1,
+          decisionLogicURL: $2,
+          interestGroupBuyers: [$1, $3],
+          perBuyerTKVSignals:
+              {$4: new Promise((resolve, reject) => { reject(); })},
+        })",
+        test_origin,
+        embedded_https_test_server().GetURL(
+            "a.test", "/interest_group/decision_logic.js"),
+        other_buyer_origin, origin_with_signals);
+    EXPECT_EQ(
+        "TypeError: Failed to execute 'runAdAuction' on 'Navigator': Promise "
+        "argument rejected or resolved to invalid value.",
+        RunAuctionAndWait(auction_config));
+  }
+}
+
+// Rejecting a promise passed in as a `perBuyerTKVSignal` currently fails the
+// auction.
+//
+// TODO(crbug.com/412588114): Make a rejected promise result in sending no
+// signals instead.
+IN_PROC_BROWSER_TEST_P(InterestGroupTrustedSignalsKVv2ContextualDataBrowserTest,
+                       BuyerTkvSignalsRunAdAuctionAndThenRejectPromise) {
+  GURL test_url =
+      embedded_https_test_server().GetURL("a.test", "/page_with_iframe.html");
+  ASSERT_TRUE(NavigateToURL(shell(), test_url));
+  url::Origin test_origin = url::Origin::Create(test_url);
+  // Buyer without any joined interest groups.
+  url::Origin other_buyer_origin =
+      embedded_https_test_server().GetOrigin("b.test");
+  GURL ad_url = GURL("https://ad.test/");
+
+  // Join an interest group without a trusted bidding signals URL. Rejecting the
+  // promise should still fail the auction.
+  EXPECT_EQ(kSuccess,
+            JoinInterestGroupAndVerify(
+                blink::TestInterestGroupBuilder(
+                    /*owner=*/test_origin,
+                    /*name=*/"cars")
+                    .SetBiddingUrl(embedded_https_test_server().GetURL(
+                        "a.test", "/interest_group/bidding_logic.js"))
+                    .SetAds(/*ads=*/{{{ad_url, /*metadata=*/std::nullopt}}})
+                    .Build()));
+
+  for (const auto& origin_with_signals : {test_origin, other_buyer_origin}) {
+    SCOPED_TRACE(origin_with_signals.Serialize());
+    std::string auction_config = JsReplace(
+        R"({
+          seller: $1,
+          decisionLogicURL: $2,
+          interestGroupBuyers: [$1, $3],
+          perBuyerTKVSignals: {$4: new Promise((resolve, reject) =>
+                                  { setTimeout(() => {reject()}, 100) })},
+        })",
+        test_origin,
+        embedded_https_test_server().GetURL(
+            "a.test", "/interest_group/decision_logic.js"),
+        other_buyer_origin, origin_with_signals);
+    EXPECT_EQ(
+        "TypeError: Failed to execute 'runAdAuction' on 'Navigator': Promise "
+        "argument rejected or resolved to invalid value.",
+        RunAuctionAndWait(auction_config));
+  }
+}
+
+// Test that providing an invalid `buyer_tkv_signals` value for a buyer via a
+// promise that's resolved while an auction is ongoing results in the auction
+// still running, but providing no contextual data.
+IN_PROC_BROWSER_TEST_P(InterestGroupTrustedSignalsKVv2ContextualDataBrowserTest,
+                       BuyerTkvSignalsRunAdAuctionWithPromiseWithBadData) {
+  TestPerBuyerTKVSignals(/*expected_bidding_key=*/kNoContextualDataValue,
+                         /*buyer_tkv_signals=*/
+                         R"(new Promise((resolve, reject) => {
+                             setTimeout(() => { resolve(function(){}); },
+                                        100);
+                         }))");
+}
+
+// Test the case of providing a valid `buyer_tkv_signals` value after the
+// auction starts.
+IN_PROC_BROWSER_TEST_P(InterestGroupTrustedSignalsKVv2ContextualDataBrowserTest,
+                       BuyerTkvSignalsRunAdAuctionWithPromise) {
+  TestPerBuyerTKVSignals(/*expected_bidding_key=*/"\"contextual data\"",
+                         /*buyer_tkv_signals=*/
+                         R"(new Promise((resolve, reject) => {
+                             setTimeout(
+                                 () => { resolve("contextual data"); },
+                                 100);
+                         }))");
+}
+
 class DisableKVv2ContextualDataBrowserTest
     : public InterestGroupTrustedSignalsKVv2ContextualDataBrowserTest {
  public:
diff --git a/content/browser/renderer_host/render_frame_host_impl.cc b/content/browser/renderer_host/render_frame_host_impl.cc
index c66512a..bfbbb55 100644
--- a/content/browser/renderer_host/render_frame_host_impl.cc
+++ b/content/browser/renderer_host/render_frame_host_impl.cc
@@ -445,6 +445,8 @@
 
 const char kDotGoogleDotCom[] = ".google.com";
 
+const char kCrashReportingGroupName[] = "crash-reporting";
+
 using RoutingIDFrameMap =
     absl::flat_hash_map<GlobalRenderFrameHostId, RenderFrameHostImpl*>;
 base::LazyInstance<RoutingIDFrameMap>::DestructorAtExit g_routing_id_frame_map =
@@ -15776,9 +15778,16 @@
   if (GURL::SchemeIsCryptographic(origin.scheme()) &&
       navigation_request->response() &&
       navigation_request->response()->parsed_headers->reporting_endpoints) {
+    base::flat_map<std::string, std::string> endpoints =
+        navigation_request->response()
+            ->parsed_headers->reporting_endpoints.value();
+    if (base::FeatureList::IsEnabled(
+            blink::features::kOverrideCrashReportingEndpoint) &&
+        endpoints.find(kCrashReportingGroupName) != endpoints.end()) {
+      crash_reporting_group_ = kCrashReportingGroupName;
+    }
     GetStoragePartition()->GetNetworkContext()->SetDocumentReportingEndpoints(
-        GetReportingSource(), origin, isolation_info_,
-        *(navigation_request->response()->parsed_headers->reporting_endpoints));
+        GetReportingSource(), origin, isolation_info_, endpoints);
   }
 
   // We move the PolicyContainerHost of |navigation_request| into the
@@ -15903,7 +15912,7 @@
 
   // Send the crash report to the Reporting API.
   GetProcess()->GetStoragePartition()->GetNetworkContext()->QueueReport(
-      /*type=*/"crash", /*group=*/"default", last_committed_url_,
+      /*type=*/"crash", crash_reporting_group_, last_committed_url_,
       GetReportingSource(), isolation_info_.network_anonymization_key(),
       std::move(body));
 }
diff --git a/content/browser/renderer_host/render_frame_host_impl.h b/content/browser/renderer_host/render_frame_host_impl.h
index a687da2a..9093d52 100644
--- a/content/browser/renderer_host/render_frame_host_impl.h
+++ b/content/browser/renderer_host/render_frame_host_impl.h
@@ -5512,6 +5512,12 @@
   // nonce returned is unique.
   base::Uuid base_auction_nonce_;
 
+  // The default group for crash reports is `default`. However, if
+  // `Reporting-Endpoints` response header specifies `crash-reporting`, crash
+  // reports will be grouped under `crash-reporting`.
+  // See for https://github.com/WICG/crash-reporting/issues/24 more details.
+  std::string crash_reporting_group_ = "default";
+
   base::OnceClosure on_process_before_unload_completed_for_testing_;
 
   // WeakPtrFactories are the last members, to ensure they are destroyed before
diff --git a/content/browser/webid/fedcm_mappers.cc b/content/browser/webid/fedcm_mappers.cc
new file mode 100644
index 0000000..be298fcf
--- /dev/null
+++ b/content/browser/webid/fedcm_mappers.cc
@@ -0,0 +1,176 @@
+// Copyright 2025 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "content/browser/webid/fedcm_mappers.h"
+
+#include <string>
+#include <vector>
+
+#include "content/public/browser/identity_request_dialog_controller.h"
+#include "third_party/blink/public/mojom/devtools/inspector_issue.mojom.h"
+#include "third_party/blink/public/mojom/webid/federated_auth_request.mojom.h"
+
+using blink::mojom::FederatedAuthRequestResult;
+using blink::mojom::RequestTokenStatus;
+
+namespace content {
+
+std::vector<std::string> DisclosureFieldsToStringList(
+    const std::vector<IdentityRequestDialogDisclosureField>& fields) {
+  std::vector<std::string> list;
+  for (auto field : fields) {
+    switch (field) {
+      case IdentityRequestDialogDisclosureField::kName:
+        list.push_back(kFedCmDefaultFieldName);
+        break;
+      case IdentityRequestDialogDisclosureField::kEmail:
+        list.push_back(kFedCmDefaultFieldEmail);
+        break;
+      case IdentityRequestDialogDisclosureField::kPicture:
+        list.push_back(kFedCmDefaultFieldPicture);
+        break;
+      case IdentityRequestDialogDisclosureField::kPhoneNumber:
+        list.push_back(kFedCmFieldPhoneNumber);
+        break;
+      case IdentityRequestDialogDisclosureField::kUsername:
+        list.push_back(kFedCmFieldUsername);
+        break;
+    }
+  }
+  return list;
+}
+
+RequestTokenStatus FederatedAuthRequestResultToRequestTokenStatus(
+    FederatedAuthRequestResult result) {
+  // Avoids exposing to renderer detailed error messages which may leak cross
+  // site information to the API call site.
+  switch (result) {
+    case FederatedAuthRequestResult::kSuccess: {
+      return RequestTokenStatus::kSuccess;
+    }
+    case FederatedAuthRequestResult::kTooManyRequests: {
+      return RequestTokenStatus::kErrorTooManyRequests;
+    }
+    case FederatedAuthRequestResult::kCanceled: {
+      return RequestTokenStatus::kErrorCanceled;
+    }
+    case FederatedAuthRequestResult::kShouldEmbargo:
+    case FederatedAuthRequestResult::kIdpNotPotentiallyTrustworthy:
+    case FederatedAuthRequestResult::kDisabledInSettings:
+    case FederatedAuthRequestResult::kDisabledInFlags:
+    case FederatedAuthRequestResult::kWellKnownHttpNotFound:
+    case FederatedAuthRequestResult::kWellKnownNoResponse:
+    case FederatedAuthRequestResult::kWellKnownInvalidResponse:
+    case FederatedAuthRequestResult::kWellKnownListEmpty:
+    case FederatedAuthRequestResult::kWellKnownInvalidContentType:
+    case FederatedAuthRequestResult::kConfigNotInWellKnown:
+    case FederatedAuthRequestResult::kWellKnownTooBig:
+    case FederatedAuthRequestResult::kConfigHttpNotFound:
+    case FederatedAuthRequestResult::kConfigNoResponse:
+    case FederatedAuthRequestResult::kConfigInvalidResponse:
+    case FederatedAuthRequestResult::kConfigInvalidContentType:
+    case FederatedAuthRequestResult::kClientMetadataHttpNotFound:
+    case FederatedAuthRequestResult::kClientMetadataNoResponse:
+    case FederatedAuthRequestResult::kClientMetadataInvalidResponse:
+    case FederatedAuthRequestResult::kClientMetadataInvalidContentType:
+    case FederatedAuthRequestResult::kAccountsHttpNotFound:
+    case FederatedAuthRequestResult::kAccountsNoResponse:
+    case FederatedAuthRequestResult::kAccountsInvalidResponse:
+    case FederatedAuthRequestResult::kAccountsListEmpty:
+    case FederatedAuthRequestResult::kAccountsInvalidContentType:
+    case FederatedAuthRequestResult::kIdTokenHttpNotFound:
+    case FederatedAuthRequestResult::kIdTokenNoResponse:
+    case FederatedAuthRequestResult::kIdTokenInvalidResponse:
+    case FederatedAuthRequestResult::kIdTokenIdpErrorResponse:
+    case FederatedAuthRequestResult::kIdTokenCrossSiteIdpErrorResponse:
+    case FederatedAuthRequestResult::kIdTokenInvalidContentType:
+    case FederatedAuthRequestResult::kRpPageNotVisible:
+    case FederatedAuthRequestResult::kSilentMediationFailure:
+    case FederatedAuthRequestResult::kThirdPartyCookiesBlocked:
+    case FederatedAuthRequestResult::kNotSignedInWithIdp:
+    case FederatedAuthRequestResult::kMissingTransientUserActivation:
+    case FederatedAuthRequestResult::kReplacedByActiveMode:
+    case FederatedAuthRequestResult::kInvalidFieldsSpecified:
+    case FederatedAuthRequestResult::kRelyingPartyOriginIsOpaque:
+    case FederatedAuthRequestResult::kTypeNotMatching:
+    case FederatedAuthRequestResult::kUiDismissedNoEmbargo:
+    case FederatedAuthRequestResult::kCorsError:
+    case FederatedAuthRequestResult::kSuppressedBySegmentationPlatform:
+    case FederatedAuthRequestResult::kError: {
+      return RequestTokenStatus::kError;
+    }
+  }
+}
+
+MetricsEndpointErrorCode FederatedAuthRequestResultToMetricsEndpointErrorCode(
+    blink::mojom::FederatedAuthRequestResult result) {
+  switch (result) {
+    case FederatedAuthRequestResult::kSuccess: {
+      return MetricsEndpointErrorCode::kNone;
+    }
+    case FederatedAuthRequestResult::kTooManyRequests:
+    case FederatedAuthRequestResult::kMissingTransientUserActivation:
+    case FederatedAuthRequestResult::kRelyingPartyOriginIsOpaque:
+    case FederatedAuthRequestResult::kInvalidFieldsSpecified:
+    case FederatedAuthRequestResult::kCanceled: {
+      return MetricsEndpointErrorCode::kRpFailure;
+    }
+    case FederatedAuthRequestResult::kAccountsInvalidResponse:
+    case FederatedAuthRequestResult::kAccountsListEmpty:
+    case FederatedAuthRequestResult::kAccountsInvalidContentType: {
+      return MetricsEndpointErrorCode::kAccountsEndpointInvalidResponse;
+    }
+    case FederatedAuthRequestResult::kIdTokenInvalidResponse:
+    case FederatedAuthRequestResult::kIdTokenIdpErrorResponse:
+    case FederatedAuthRequestResult::kIdTokenCrossSiteIdpErrorResponse:
+    case FederatedAuthRequestResult::kIdTokenInvalidContentType:
+    case FederatedAuthRequestResult::kCorsError: {
+      return MetricsEndpointErrorCode::kTokenEndpointInvalidResponse;
+    }
+    case FederatedAuthRequestResult::kShouldEmbargo:
+    case FederatedAuthRequestResult::kUiDismissedNoEmbargo:
+    case FederatedAuthRequestResult::kDisabledInFlags:
+    case FederatedAuthRequestResult::kDisabledInSettings:
+    case FederatedAuthRequestResult::kThirdPartyCookiesBlocked:
+    case FederatedAuthRequestResult::kRpPageNotVisible:
+    case FederatedAuthRequestResult::kReplacedByActiveMode:
+    case FederatedAuthRequestResult::kNotSignedInWithIdp: {
+      return MetricsEndpointErrorCode::kUserFailure;
+    }
+    case FederatedAuthRequestResult::kWellKnownHttpNotFound:
+    case FederatedAuthRequestResult::kWellKnownNoResponse:
+    case FederatedAuthRequestResult::kConfigHttpNotFound:
+    case FederatedAuthRequestResult::kConfigNoResponse:
+    case FederatedAuthRequestResult::kClientMetadataHttpNotFound:
+    case FederatedAuthRequestResult::kClientMetadataNoResponse:
+    case FederatedAuthRequestResult::kAccountsHttpNotFound:
+    case FederatedAuthRequestResult::kAccountsNoResponse:
+    case FederatedAuthRequestResult::kIdTokenHttpNotFound:
+    case FederatedAuthRequestResult::kIdTokenNoResponse: {
+      return MetricsEndpointErrorCode::kIdpServerUnavailable;
+    }
+    case FederatedAuthRequestResult::kConfigNotInWellKnown:
+    case FederatedAuthRequestResult::kWellKnownTooBig: {
+      return MetricsEndpointErrorCode::kManifestError;
+    }
+    case FederatedAuthRequestResult::kWellKnownListEmpty:
+    case FederatedAuthRequestResult::kWellKnownInvalidResponse:
+    case FederatedAuthRequestResult::kConfigInvalidResponse:
+    case FederatedAuthRequestResult::kClientMetadataInvalidResponse:
+    case FederatedAuthRequestResult::kWellKnownInvalidContentType:
+    case FederatedAuthRequestResult::kConfigInvalidContentType:
+    case FederatedAuthRequestResult::kClientMetadataInvalidContentType: {
+      return MetricsEndpointErrorCode::kIdpServerInvalidResponse;
+    }
+    case FederatedAuthRequestResult::kIdpNotPotentiallyTrustworthy:
+    case FederatedAuthRequestResult::kError:
+    case FederatedAuthRequestResult::kSilentMediationFailure:
+    case FederatedAuthRequestResult::kTypeNotMatching:
+    case FederatedAuthRequestResult::kSuppressedBySegmentationPlatform: {
+      return MetricsEndpointErrorCode::kOther;
+    }
+  }
+}
+
+}  // namespace content
diff --git a/content/browser/webid/fedcm_mappers.h b/content/browser/webid/fedcm_mappers.h
new file mode 100644
index 0000000..df0fd76
--- /dev/null
+++ b/content/browser/webid/fedcm_mappers.h
@@ -0,0 +1,65 @@
+// Copyright 2025 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CONTENT_BROWSER_FEDCM_MAPPERS_H_
+#define CONTENT_BROWSER_FEDCM_MAPPERS_H_
+
+#include <string>
+#include <vector>
+
+#include "content/public/browser/identity_request_dialog_controller.h"
+#include "third_party/blink/public/mojom/devtools/inspector_issue.mojom.h"
+#include "third_party/blink/public/mojom/webid/federated_auth_request.mojom.h"
+
+namespace content {
+
+// This header file defines functions which convert between FedCM types. It also
+// defines some constants used in some of these conversions.
+
+inline constexpr char kFedCmDefaultFieldName[] = "name";
+inline constexpr char kFedCmDefaultFieldEmail[] = "email";
+inline constexpr char kFedCmDefaultFieldPicture[] = "picture";
+inline constexpr char kFedCmFieldPhoneNumber[] = "tel";
+inline constexpr char kFedCmFieldUsername[] = "username";
+
+// Error codes sent to the metrics endpoint.
+// Enum is part of public FedCM API. Do not renumber error codes.
+// The error codes are not consecutive to make adding error codes easier in
+// the future.
+enum class MetricsEndpointErrorCode {
+  kNone = 0,  // Success
+  kOther = 1,
+  // Errors triggered by how RP calls FedCM API.
+  kRpFailure = 100,
+  // User Failures.
+  kUserFailure = 200,
+  // Generic IDP Failures.
+  kIdpServerInvalidResponse = 300,
+  kIdpServerUnavailable = 301,
+  kManifestError = 302,
+  // Specific IDP Failures.
+  kAccountsEndpointInvalidResponse = 401,
+  kTokenEndpointInvalidResponse = 402,
+};
+
+// Converts a list of IdentityRequestDialogDisclosureField to a list of strings.
+// May return an empty list.
+std::vector<std::string> DisclosureFieldsToStringList(
+    const std::vector<IdentityRequestDialogDisclosureField>& fields);
+
+// Converts a FederatedAuthRequestResult, which is a browser type for the
+// result, to a RequestTokenStatus, which is a renderer type, e.g. the one to be
+// exposed to web developers.
+blink::mojom::RequestTokenStatus FederatedAuthRequestResultToRequestTokenStatus(
+    blink::mojom::FederatedAuthRequestResult result);
+
+// Converts a FederatedAuthRequestResult, which is a browser type for the
+// result, to a MetricsEndpointErrorCode, which is a type used in the metrics
+// endpoint error code.
+MetricsEndpointErrorCode FederatedAuthRequestResultToMetricsEndpointErrorCode(
+    blink::mojom::FederatedAuthRequestResult result);
+
+}  // namespace content
+
+#endif  // CONTENT_BROWSER_FEDCM_MAPPERS_H_
diff --git a/content/browser/webid/fedcm_url_computations.cc b/content/browser/webid/fedcm_url_computations.cc
new file mode 100644
index 0000000..db9e078
--- /dev/null
+++ b/content/browser/webid/fedcm_url_computations.cc
@@ -0,0 +1,161 @@
+// Copyright 2025 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "content/browser/webid/fedcm_url_computations.h"
+
+#include <string>
+
+#include "base/containers/contains.h"
+#include "base/strings/escape.h"
+#include "base/strings/strcat.h"
+#include "content/browser/webid/fedcm_mappers.h"
+#include "content/browser/webid/flags.h"
+#include "content/browser/webid/sd_jwt.h"
+#include "content/public/browser/render_frame_host.h"
+#include "third_party/blink/public/mojom/webid/federated_auth_request.mojom.h"
+
+using RpMode = blink::mojom::RpMode;
+
+namespace content {
+
+namespace {
+
+bool IsRequestingDefaultPermissions(const std::vector<std::string>& fields) {
+  return base::Contains(fields, kFedCmDefaultFieldName) &&
+         base::Contains(fields, kFedCmDefaultFieldEmail) &&
+         base::Contains(fields, kFedCmDefaultFieldPicture);
+}
+
+}  // namespace
+
+std::string ComputeUrlEncodedTokenPostDataForIssuers(
+    const std::string& account_id,
+    const sdjwt::Jwk& holder_key,
+    const std::string& format) {
+  return base::StrCat(
+      {"account_id=", base::EscapeUrlEncodedData(account_id, /*use_plus=*/true),
+       "&holder_key=",
+       base::EscapeUrlEncodedData(*holder_key.Serialize(),
+                                  /*use_plus=*/true),
+       "&format=", base::EscapeUrlEncodedData(format, /*use_plus=*/true)});
+}
+
+std::string ComputeUrlEncodedTokenPostData(
+    RenderFrameHost& render_frame_host,
+    const std::string& client_id,
+    const std::string& nonce,
+    const std::string& account_id,
+    bool is_auto_reauthn,
+    const RpMode& rp_mode,
+    const std::optional<std::vector<std::string>>& fields,
+    const std::vector<std::string>& disclosure_shown_for,
+    const std::string& params_json,
+    const std::optional<std::string>& type) {
+  std::string query;
+  if (!client_id.empty()) {
+    query +=
+        "client_id=" + base::EscapeUrlEncodedData(client_id, /*use_plus=*/true);
+  }
+
+  if (!nonce.empty()) {
+    if (!query.empty()) {
+      query += "&";
+    }
+    query += "nonce=" + base::EscapeUrlEncodedData(nonce, /*use_plus=*/true);
+  }
+
+  if (!account_id.empty()) {
+    if (!query.empty()) {
+      query += "&";
+    }
+    query += "account_id=" +
+             base::EscapeUrlEncodedData(account_id, /*use_plus=*/true);
+  }
+  // For new users signing up, we show some disclosure text to remind them about
+  // data sharing between IDP and RP. For returning users signing in, such
+  // disclosure text is not necessary. This field indicates in the request
+  // whether the user has been shown such disclosure text.
+  std::string disclosure_text_shown_param =
+      base::ToString(IsRequestingDefaultPermissions(disclosure_shown_for));
+  if (!query.empty()) {
+    query += "&";
+  }
+  query += "disclosure_text_shown=" + disclosure_text_shown_param;
+
+  // Shares with IdP that whether the identity credential was automatically
+  // selected. This could help developers to better comprehend the token
+  // request and segment metrics accordingly.
+  std::string is_auto_selected = base::ToString(is_auto_reauthn);
+  if (!query.empty()) {
+    query += "&";
+  }
+  query += "is_auto_selected=" + is_auto_selected;
+
+  // Shares with IdP the type of the request.
+  std::string rp_mode_str = rp_mode == RpMode::kActive ? "active" : "passive";
+  if (!query.empty()) {
+    query += "&";
+  }
+  query += "mode=" + rp_mode_str;
+
+  std::vector<std::string> fields_to_use;
+  if (fields) {
+    fields_to_use = *fields;
+  } else {
+    fields_to_use = {kFedCmDefaultFieldName, kFedCmDefaultFieldEmail,
+                     kFedCmDefaultFieldPicture};
+  }
+  if (!fields_to_use.empty()) {
+    query += "&fields=" +
+             base::EscapeUrlEncodedData(base::JoinString(fields_to_use, ","),
+                                        /*use_plus=*/true);
+  }
+
+  if (!disclosure_shown_for.empty()) {
+    query +=
+        "&disclosure_shown_for=" +
+        base::EscapeUrlEncodedData(base::JoinString(disclosure_shown_for, ","),
+                                   /*use_plus=*/true);
+  }
+
+  if (!params_json.empty()) {
+    query +=
+        "&params=" + base::EscapeUrlEncodedData(params_json, /*use_plus=*/true);
+  }
+  if (IsFedCmIdPRegistrationEnabled() && type) {
+    query += "&type=" + base::EscapeUrlEncodedData(*type, /*use_plus=*/true);
+  }
+  return query;
+}
+
+void MaybeAppendQueryParameters(
+    const IdentityProviderLoginUrlInfo& idp_login_info,
+    GURL* login_url) {
+  if (idp_login_info.login_hint.empty() && idp_login_info.domain_hint.empty()) {
+    return;
+  }
+  std::string old_query = login_url->query();
+  if (!old_query.empty()) {
+    old_query += "&";
+  }
+  std::string new_query_string = old_query;
+  if (!idp_login_info.login_hint.empty()) {
+    new_query_string +=
+        "login_hint=" + base::EscapeUrlEncodedData(idp_login_info.login_hint,
+                                                   /*use_plus=*/false);
+  }
+  if (!idp_login_info.domain_hint.empty()) {
+    if (!new_query_string.empty()) {
+      new_query_string += "&";
+    }
+    new_query_string +=
+        "domain_hint=" + base::EscapeUrlEncodedData(idp_login_info.domain_hint,
+                                                    /*use_plus=*/false);
+  }
+  GURL::Replacements replacements;
+  replacements.SetQueryStr(new_query_string);
+  *login_url = login_url->ReplaceComponents(replacements);
+}
+
+}  // namespace content
diff --git a/content/browser/webid/fedcm_url_computations.h b/content/browser/webid/fedcm_url_computations.h
new file mode 100644
index 0000000..91f3c541
--- /dev/null
+++ b/content/browser/webid/fedcm_url_computations.h
@@ -0,0 +1,50 @@
+// Copyright 2025 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CONTENT_BROWSER_WEBID_FEDCM_URL_COMPUTATIONS_H_
+#define CONTENT_BROWSER_WEBID_FEDCM_URL_COMPUTATIONS_H_
+
+#include <optional>
+#include <string>
+#include <vector>
+
+#include "content/browser/webid/sd_jwt.h"
+#include "content/public/browser/render_frame_host.h"
+#include "third_party/blink/public/mojom/webid/federated_auth_request.mojom-forward.h"
+
+namespace content {
+
+// This file contains functions that compute URLs that are used in FedCM.
+
+// Computes the URL-encoded POST data for the token endpoint for issuers.
+std::string ComputeUrlEncodedTokenPostDataForIssuers(
+    const std::string& account_id,
+    const sdjwt::Jwk& holder_key,
+    const std::string& format);
+
+// Computes the URL-encoded POST data for the token endpoint.
+std::string ComputeUrlEncodedTokenPostData(
+    RenderFrameHost& render_frame_host,
+    const std::string& client_id,
+    const std::string& nonce,
+    const std::string& account_id,
+    bool is_auto_reauthn,
+    const blink::mojom::RpMode& rp_mode,
+    const std::optional<std::vector<std::string>>& fields,
+    const std::vector<std::string>& disclosure_shown_for,
+    const std::string& params_json,
+    const std::optional<std::string>& type);
+
+struct IdentityProviderLoginUrlInfo {
+  std::string login_hint;
+  std::string domain_hint;
+};
+
+void MaybeAppendQueryParameters(
+    const IdentityProviderLoginUrlInfo& idp_login_info,
+    GURL* login_url);
+
+}  // namespace content
+
+#endif  // CONTENT_BROWSER_WEBID_FEDCM_URL_COMPUTATIONS_H_
diff --git a/content/browser/webid/federated_auth_request_impl.cc b/content/browser/webid/federated_auth_request_impl.cc
index 7317dcf8..5d65989 100644
--- a/content/browser/webid/federated_auth_request_impl.cc
+++ b/content/browser/webid/federated_auth_request_impl.cc
@@ -30,6 +30,8 @@
 #include "content/browser/renderer_host/render_frame_host_impl.h"
 #include "content/browser/web_contents/web_contents_impl.h"
 #include "content/browser/webid/fake_identity_request_dialog_controller.h"
+#include "content/browser/webid/fedcm_mappers.h"
+#include "content/browser/webid/fedcm_url_computations.h"
 #include "content/browser/webid/federated_auth_disconnect_request.h"
 #include "content/browser/webid/federated_auth_request_page_data.h"
 #include "content/browser/webid/federated_auth_user_info_request.h"
@@ -106,282 +108,8 @@
 static constexpr double kRejectionLogNormalSigma = 1.4;
 #endif  // BUILDFLAG(IS_ANDROID)
 
-static constexpr char kDefaultFieldName[] = "name";
-static constexpr char kDefaultFieldEmail[] = "email";
-static constexpr char kDefaultFieldPicture[] = "picture";
-static constexpr char kFieldPhoneNumber[] = "tel";
-static constexpr char kFieldUsername[] = "username";
-
 static constexpr char kVcSdJwt[] = "vc+sd-jwt";
 
-bool IsRequestingDefaultPermissions(const std::vector<std::string>& fields) {
-  return base::Contains(fields, kDefaultFieldName) &&
-         base::Contains(fields, kDefaultFieldEmail) &&
-         base::Contains(fields, kDefaultFieldPicture);
-}
-
-std::vector<std::string> DisclosureFieldsToStringList(
-    const std::vector<IdentityRequestDialogDisclosureField>& fields) {
-  std::vector<std::string> list;
-  for (auto field : fields) {
-    switch (field) {
-      case IdentityRequestDialogDisclosureField::kName:
-        list.push_back(kDefaultFieldName);
-        break;
-      case IdentityRequestDialogDisclosureField::kEmail:
-        list.push_back(kDefaultFieldEmail);
-        break;
-      case IdentityRequestDialogDisclosureField::kPicture:
-        list.push_back(kDefaultFieldPicture);
-        break;
-      case IdentityRequestDialogDisclosureField::kPhoneNumber:
-        list.push_back(kFieldPhoneNumber);
-        break;
-      case IdentityRequestDialogDisclosureField::kUsername:
-        list.push_back(kFieldUsername);
-        break;
-    }
-  }
-  return list;
-}
-
-std::string ComputeUrlEncodedTokenPostDataForIssuers(
-    const std::string& account_id,
-    const sdjwt::Jwk& holder_key,
-    const std::string& format) {
-  return base::StrCat(
-      {"account_id=", base::EscapeUrlEncodedData(account_id, /*use_plus=*/true),
-       "&holder_key=",
-       base::EscapeUrlEncodedData(*holder_key.Serialize(),
-                                  /*use_plus=*/true),
-       "&format=", base::EscapeUrlEncodedData(format, /*use_plus=*/true)});
-}
-
-std::string ComputeUrlEncodedTokenPostData(
-    RenderFrameHost& render_frame_host,
-    const std::string& client_id,
-    const std::string& nonce,
-    const std::string& account_id,
-    bool is_auto_reauthn,
-    const RpMode& rp_mode,
-    const std::optional<std::vector<std::string>>& fields,
-    const std::vector<std::string>& disclosure_shown_for,
-    const std::string& params_json,
-    const std::optional<std::string>& type) {
-  std::string query;
-  if (!client_id.empty()) {
-    query +=
-        "client_id=" + base::EscapeUrlEncodedData(client_id, /*use_plus=*/true);
-  }
-
-  if (!nonce.empty()) {
-    if (!query.empty()) {
-      query += "&";
-    }
-    query += "nonce=" + base::EscapeUrlEncodedData(nonce, /*use_plus=*/true);
-  }
-
-  if (!account_id.empty()) {
-    if (!query.empty()) {
-      query += "&";
-    }
-    query += "account_id=" +
-             base::EscapeUrlEncodedData(account_id, /*use_plus=*/true);
-  }
-  // For new users signing up, we show some disclosure text to remind them about
-  // data sharing between IDP and RP. For returning users signing in, such
-  // disclosure text is not necessary. This field indicates in the request
-  // whether the user has been shown such disclosure text.
-  std::string disclosure_text_shown_param =
-      base::ToString(IsRequestingDefaultPermissions(disclosure_shown_for));
-  if (!query.empty()) {
-    query += "&";
-  }
-  query += "disclosure_text_shown=" + disclosure_text_shown_param;
-
-  // Shares with IdP that whether the identity credential was automatically
-  // selected. This could help developers to better comprehend the token
-  // request and segment metrics accordingly.
-  std::string is_auto_selected = base::ToString(is_auto_reauthn);
-  if (!query.empty()) {
-    query += "&";
-  }
-  query += "is_auto_selected=" + is_auto_selected;
-
-  // Shares with IdP the type of the request.
-  std::string rp_mode_str = rp_mode == RpMode::kActive ? "active" : "passive";
-  if (!query.empty()) {
-    query += "&";
-  }
-  query += "mode=" + rp_mode_str;
-
-  std::vector<std::string> fields_to_use;
-  if (fields) {
-    fields_to_use = *fields;
-  } else {
-    fields_to_use = {kDefaultFieldName, kDefaultFieldEmail,
-                     kDefaultFieldPicture};
-  }
-  if (!fields_to_use.empty()) {
-    query += "&fields=" +
-             base::EscapeUrlEncodedData(base::JoinString(fields_to_use, ","),
-                                        /*use_plus=*/true);
-  }
-
-  if (!disclosure_shown_for.empty()) {
-    query +=
-        "&disclosure_shown_for=" +
-        base::EscapeUrlEncodedData(base::JoinString(disclosure_shown_for, ","),
-                                   /*use_plus=*/true);
-  }
-
-  if (!params_json.empty()) {
-    query +=
-        "&params=" + base::EscapeUrlEncodedData(params_json, /*use_plus=*/true);
-  }
-  if (IsFedCmIdPRegistrationEnabled() && type) {
-    query += "&type=" + base::EscapeUrlEncodedData(*type, /*use_plus=*/true);
-  }
-  return query;
-}
-
-RequestTokenStatus FederatedAuthRequestResultToRequestTokenStatus(
-    FederatedAuthRequestResult result) {
-  // Avoids exposing to renderer detailed error messages which may leak cross
-  // site information to the API call site.
-  switch (result) {
-    case FederatedAuthRequestResult::kSuccess: {
-      return RequestTokenStatus::kSuccess;
-    }
-    case FederatedAuthRequestResult::kTooManyRequests: {
-      return RequestTokenStatus::kErrorTooManyRequests;
-    }
-    case FederatedAuthRequestResult::kCanceled: {
-      return RequestTokenStatus::kErrorCanceled;
-    }
-    case FederatedAuthRequestResult::kShouldEmbargo:
-    case FederatedAuthRequestResult::kIdpNotPotentiallyTrustworthy:
-    case FederatedAuthRequestResult::kDisabledInSettings:
-    case FederatedAuthRequestResult::kDisabledInFlags:
-    case FederatedAuthRequestResult::kWellKnownHttpNotFound:
-    case FederatedAuthRequestResult::kWellKnownNoResponse:
-    case FederatedAuthRequestResult::kWellKnownInvalidResponse:
-    case FederatedAuthRequestResult::kWellKnownListEmpty:
-    case FederatedAuthRequestResult::kWellKnownInvalidContentType:
-    case FederatedAuthRequestResult::kConfigNotInWellKnown:
-    case FederatedAuthRequestResult::kWellKnownTooBig:
-    case FederatedAuthRequestResult::kConfigHttpNotFound:
-    case FederatedAuthRequestResult::kConfigNoResponse:
-    case FederatedAuthRequestResult::kConfigInvalidResponse:
-    case FederatedAuthRequestResult::kConfigInvalidContentType:
-    case FederatedAuthRequestResult::kClientMetadataHttpNotFound:
-    case FederatedAuthRequestResult::kClientMetadataNoResponse:
-    case FederatedAuthRequestResult::kClientMetadataInvalidResponse:
-    case FederatedAuthRequestResult::kClientMetadataInvalidContentType:
-    case FederatedAuthRequestResult::kAccountsHttpNotFound:
-    case FederatedAuthRequestResult::kAccountsNoResponse:
-    case FederatedAuthRequestResult::kAccountsInvalidResponse:
-    case FederatedAuthRequestResult::kAccountsListEmpty:
-    case FederatedAuthRequestResult::kAccountsInvalidContentType:
-    case FederatedAuthRequestResult::kIdTokenHttpNotFound:
-    case FederatedAuthRequestResult::kIdTokenNoResponse:
-    case FederatedAuthRequestResult::kIdTokenInvalidResponse:
-    case FederatedAuthRequestResult::kIdTokenIdpErrorResponse:
-    case FederatedAuthRequestResult::kIdTokenCrossSiteIdpErrorResponse:
-    case FederatedAuthRequestResult::kIdTokenInvalidContentType:
-    case FederatedAuthRequestResult::kRpPageNotVisible:
-    case FederatedAuthRequestResult::kSilentMediationFailure:
-    case FederatedAuthRequestResult::kThirdPartyCookiesBlocked:
-    case FederatedAuthRequestResult::kNotSignedInWithIdp:
-    case FederatedAuthRequestResult::kMissingTransientUserActivation:
-    case FederatedAuthRequestResult::kReplacedByActiveMode:
-    case FederatedAuthRequestResult::kInvalidFieldsSpecified:
-    case FederatedAuthRequestResult::kRelyingPartyOriginIsOpaque:
-    case FederatedAuthRequestResult::kTypeNotMatching:
-    case FederatedAuthRequestResult::kUiDismissedNoEmbargo:
-    case FederatedAuthRequestResult::kCorsError:
-    case FederatedAuthRequestResult::kSuppressedBySegmentationPlatform:
-    case FederatedAuthRequestResult::kError: {
-      return RequestTokenStatus::kError;
-    }
-  }
-}
-
-IdpNetworkRequestManager::MetricsEndpointErrorCode
-FederatedAuthRequestResultToMetricsEndpointErrorCode(
-    blink::mojom::FederatedAuthRequestResult result) {
-  switch (result) {
-    case FederatedAuthRequestResult::kSuccess: {
-      return IdpNetworkRequestManager::MetricsEndpointErrorCode::kNone;
-    }
-    case FederatedAuthRequestResult::kTooManyRequests:
-    case FederatedAuthRequestResult::kMissingTransientUserActivation:
-    case FederatedAuthRequestResult::kRelyingPartyOriginIsOpaque:
-    case FederatedAuthRequestResult::kInvalidFieldsSpecified:
-    case FederatedAuthRequestResult::kCanceled: {
-      return IdpNetworkRequestManager::MetricsEndpointErrorCode::kRpFailure;
-    }
-    case FederatedAuthRequestResult::kAccountsInvalidResponse:
-    case FederatedAuthRequestResult::kAccountsListEmpty:
-    case FederatedAuthRequestResult::kAccountsInvalidContentType: {
-      return IdpNetworkRequestManager::MetricsEndpointErrorCode::
-          kAccountsEndpointInvalidResponse;
-    }
-    case FederatedAuthRequestResult::kIdTokenInvalidResponse:
-    case FederatedAuthRequestResult::kIdTokenIdpErrorResponse:
-    case FederatedAuthRequestResult::kIdTokenCrossSiteIdpErrorResponse:
-    case FederatedAuthRequestResult::kIdTokenInvalidContentType:
-    case FederatedAuthRequestResult::kCorsError: {
-      return IdpNetworkRequestManager::MetricsEndpointErrorCode::
-          kTokenEndpointInvalidResponse;
-    }
-    case FederatedAuthRequestResult::kShouldEmbargo:
-    case FederatedAuthRequestResult::kUiDismissedNoEmbargo:
-    case FederatedAuthRequestResult::kDisabledInFlags:
-    case FederatedAuthRequestResult::kDisabledInSettings:
-    case FederatedAuthRequestResult::kThirdPartyCookiesBlocked:
-    case FederatedAuthRequestResult::kRpPageNotVisible:
-    case FederatedAuthRequestResult::kReplacedByActiveMode:
-    case FederatedAuthRequestResult::kNotSignedInWithIdp: {
-      return IdpNetworkRequestManager::MetricsEndpointErrorCode::kUserFailure;
-    }
-    case FederatedAuthRequestResult::kWellKnownHttpNotFound:
-    case FederatedAuthRequestResult::kWellKnownNoResponse:
-    case FederatedAuthRequestResult::kConfigHttpNotFound:
-    case FederatedAuthRequestResult::kConfigNoResponse:
-    case FederatedAuthRequestResult::kClientMetadataHttpNotFound:
-    case FederatedAuthRequestResult::kClientMetadataNoResponse:
-    case FederatedAuthRequestResult::kAccountsHttpNotFound:
-    case FederatedAuthRequestResult::kAccountsNoResponse:
-    case FederatedAuthRequestResult::kIdTokenHttpNotFound:
-    case FederatedAuthRequestResult::kIdTokenNoResponse: {
-      return IdpNetworkRequestManager::MetricsEndpointErrorCode::
-          kIdpServerUnavailable;
-    }
-    case FederatedAuthRequestResult::kConfigNotInWellKnown:
-    case FederatedAuthRequestResult::kWellKnownTooBig: {
-      return IdpNetworkRequestManager::MetricsEndpointErrorCode::kManifestError;
-    }
-    case FederatedAuthRequestResult::kWellKnownListEmpty:
-    case FederatedAuthRequestResult::kWellKnownInvalidResponse:
-    case FederatedAuthRequestResult::kConfigInvalidResponse:
-    case FederatedAuthRequestResult::kClientMetadataInvalidResponse:
-    case FederatedAuthRequestResult::kWellKnownInvalidContentType:
-    case FederatedAuthRequestResult::kConfigInvalidContentType:
-    case FederatedAuthRequestResult::kClientMetadataInvalidContentType: {
-      return IdpNetworkRequestManager::MetricsEndpointErrorCode::
-          kIdpServerInvalidResponse;
-    }
-    case FederatedAuthRequestResult::kIdpNotPotentiallyTrustworthy:
-    case FederatedAuthRequestResult::kError:
-    case FederatedAuthRequestResult::kSilentMediationFailure:
-    case FederatedAuthRequestResult::kTypeNotMatching:
-    case FederatedAuthRequestResult::kSuppressedBySegmentationPlatform: {
-      return IdpNetworkRequestManager::MetricsEndpointErrorCode::kOther;
-    }
-  }
-}
-
 // The time from when the accounts dialog is shown to when a user explicitly
 // closes it follows normal distribution. To make the random failures
 // indistinguishable from user declines, we use lognormal distribution to
@@ -414,36 +142,6 @@
          frame->GetVisibilityState() == content::PageVisibilityState::kVisible;
 }
 
-void MaybeAppendQueryParameters(
-    const FederatedAuthRequestImpl::IdentityProviderLoginUrlInfo&
-        idp_login_info,
-    GURL* login_url) {
-  if (idp_login_info.login_hint.empty() && idp_login_info.domain_hint.empty()) {
-    return;
-  }
-  std::string old_query = login_url->query();
-  if (!old_query.empty()) {
-    old_query += "&";
-  }
-  std::string new_query_string = old_query;
-  if (!idp_login_info.login_hint.empty()) {
-    new_query_string +=
-        "login_hint=" + base::EscapeUrlEncodedData(idp_login_info.login_hint,
-                                                   /*use_plus=*/false);
-  }
-  if (!idp_login_info.domain_hint.empty()) {
-    if (!new_query_string.empty()) {
-      new_query_string += "&";
-    }
-    new_query_string +=
-        "domain_hint=" + base::EscapeUrlEncodedData(idp_login_info.domain_hint,
-                                                    /*use_plus=*/false);
-  }
-  GURL::Replacements replacements;
-  replacements.SetQueryStr(new_query_string);
-  *login_url = login_url->ReplaceComponents(replacements);
-}
-
 std::vector<uint8_t> Sha256(std::string_view data) {
   auto hash = crypto::hash::Sha256(base::as_byte_span(data));
   std::vector<uint8_t> result{hash.begin(), hash.end()};
@@ -1461,16 +1159,16 @@
 
   std::vector<IdentityRequestDialogDisclosureField> list;
   for (const auto& field : *fields) {
-    if (field == kDefaultFieldName) {
+    if (field == kFedCmDefaultFieldName) {
       list.push_back(IdentityRequestDialogDisclosureField::kName);
-    } else if (field == kDefaultFieldEmail) {
+    } else if (field == kFedCmDefaultFieldEmail) {
       list.push_back(IdentityRequestDialogDisclosureField::kEmail);
-    } else if (field == kDefaultFieldPicture) {
+    } else if (field == kFedCmDefaultFieldPicture) {
       list.push_back(IdentityRequestDialogDisclosureField::kPicture);
     } else if (IsFedCmAlternativeIdentifiersEnabled()) {
-      if (field == kFieldPhoneNumber) {
+      if (field == kFedCmFieldPhoneNumber) {
         list.push_back(IdentityRequestDialogDisclosureField::kPhoneNumber);
-      } else if (field == kFieldUsername) {
+      } else if (field == kFedCmFieldUsername) {
         list.push_back(IdentityRequestDialogDisclosureField::kUsername);
       }
     }
@@ -2996,8 +2694,9 @@
       idp_infos_[config_url]->provider;
   DCHECK(provider);
 
-  std::vector<std::string> fields = {kDefaultFieldName, kDefaultFieldEmail,
-                                     kDefaultFieldPicture};
+  std::vector<std::string> fields = {kFedCmDefaultFieldName,
+                                     kFedCmDefaultFieldEmail,
+                                     kFedCmDefaultFieldPicture};
   if (provider->fields) {
     fields = *provider->fields;
   }
@@ -3212,7 +2911,7 @@
       // selecting any IDP.
       network_manager_->SendFailedTokenRequestMetrics(
           metrics_endpoint, did_show_ui_,
-          IdpNetworkRequestManager::MetricsEndpointErrorCode::kUserFailure);
+          MetricsEndpointErrorCode::kUserFailure);
     }
   }
 }
diff --git a/content/browser/webid/federated_auth_request_impl.h b/content/browser/webid/federated_auth_request_impl.h
index f92ab13..a4b9e96 100644
--- a/content/browser/webid/federated_auth_request_impl.h
+++ b/content/browser/webid/federated_auth_request_impl.h
@@ -15,6 +15,7 @@
 #include "base/memory/scoped_refptr.h"
 #include "base/time/time.h"
 #include "content/browser/webid/fedcm_metrics.h"
+#include "content/browser/webid/fedcm_url_computations.h"
 #include "content/browser/webid/federated_provider_fetcher.h"
 #include "content/browser/webid/identity_registry.h"
 #include "content/browser/webid/identity_registry_delegate.h"
@@ -195,11 +196,6 @@
     std::optional<bool> client_matches_top_frame_origin;
   };
 
-  struct IdentityProviderLoginUrlInfo {
-    std::string login_hint;
-    std::string domain_hint;
-  };
-
   // For use by the devtools protocol for browser automation.
   IdentityRequestDialogController* GetDialogController() {
     return request_dialog_controller_.get();
diff --git a/content/browser/webid/idp_network_request_manager.cc b/content/browser/webid/idp_network_request_manager.cc
index bc05a97..102591a 100644
--- a/content/browser/webid/idp_network_request_manager.cc
+++ b/content/browser/webid/idp_network_request_manager.cc
@@ -690,7 +690,7 @@
         case blink::mojom::RpMode::kActive:
           selected_mode_dict = modes_dict->FindDict(kActiveModeKey);
           break;
-      };
+      }
     }
     if (selected_mode_dict) {
       supports_add_account =
diff --git a/content/browser/webid/idp_network_request_manager.h b/content/browser/webid/idp_network_request_manager.h
index 43e50efae..bb9851c 100644
--- a/content/browser/webid/idp_network_request_manager.h
+++ b/content/browser/webid/idp_network_request_manager.h
@@ -11,6 +11,7 @@
 #include <vector>
 
 #include "base/functional/callback.h"
+#include "content/browser/webid/fedcm_mappers.h"
 #include "content/common/content_export.h"
 #include "content/public/browser/identity_request_account.h"
 #include "content/public/browser/identity_request_dialog_controller.h"
@@ -151,26 +152,6 @@
     std::optional<IdentityCredentialTokenError> error;
   };
 
-  // Error codes sent to the metrics endpoint.
-  // Enum is part of public FedCM API. Do not renumber error codes.
-  // The error codes are not consecutive to make adding error codes easier in
-  // the future.
-  enum class MetricsEndpointErrorCode {
-    kNone = 0,  // Success
-    kOther = 1,
-    // Errors triggered by how RP calls FedCM API.
-    kRpFailure = 100,
-    // User Failures.
-    kUserFailure = 200,
-    // Generic IDP Failures.
-    kIdpServerInvalidResponse = 300,
-    kIdpServerUnavailable = 301,
-    kManifestError = 302,
-    // Specific IDP Failures.
-    kAccountsEndpointInvalidResponse = 401,
-    kTokenEndpointInvalidResponse = 402,
-  };
-
   enum class DisconnectResponse {
     kSuccess,
     kError,
diff --git a/content/browser/webrtc/resources/BUILD.gn b/content/browser/webrtc/resources/BUILD.gn
index 719d1da..6db49ca 100644
--- a/content/browser/webrtc/resources/BUILD.gn
+++ b/content/browser/webrtc/resources/BUILD.gn
@@ -22,6 +22,7 @@
     "stats_rates_calculator.js",
     "stats_table.js",
     "tab_view.js",
+    "sdp_utils.js",
     "timeline_graph_view.js",
     "user_media_table.js",
     "webrtc_internals.js",
diff --git a/content/browser/webrtc/resources/peer_connection_update_table.js b/content/browser/webrtc/resources/peer_connection_update_table.js
index a1f4edd..5bcd880 100644
--- a/content/browser/webrtc/resources/peer_connection_update_table.js
+++ b/content/browser/webrtc/resources/peer_connection_update_table.js
@@ -3,6 +3,7 @@
 // found in the LICENSE file.
 
 import {$} from 'chrome://resources/js/util.js';
+import * as SDPUtils from './sdp_utils.js';
 
 const MAX_NUMBER_OF_STATE_CHANGES_DISPLAYED = 10;
 const MAX_NUMBER_OF_EXPANDED_MEDIASECTIONS = 10;
@@ -183,28 +184,31 @@
         };
         valueContainer.appendChild(copyBtn);
 
+        let lastSections;
+        const lastOfferAnswer = this.getLastOfferAnswer_(tableElement);
+        if (lastOfferAnswer && lastOfferAnswer !== update.value) {
+          lastSections = SDPUtils.splitSections(
+              lastOfferAnswer.substring(6).split(', sdp: ')[1]);
+        }
         // Fold the SDP sections.
-        const sections = sdp.split('\nm=')
-          .map((part, index) => (index > 0 ?
-            'm=' + part : part).trim() + '\r\n');
+        const sections = SDPUtils.splitSections(sdp);
         summary.textContent +=
           ' (type: "' + type + '", ' + sections.length + ' sections)';
-        sections.forEach(section => {
-          const lines = section.trim().split('\n');
+        sections.forEach((section, index) => {
+          const lines = SDPUtils.splitLines(section);
           // Extract the mid attribute.
           const mid = lines
               .filter(line => line.startsWith('a=mid:'))
               .map(line => line.substring(6))[0];
-          // Extract the direction.
-          const direction = lines
-              .map(line => line.substring(2).trim())
-              .find(line => ['sendrecv', 'sendonly',
-                    'recvonly', 'inactive'].includes(line)) || 'sendrecv';
+          const direction = SDPUtils.getDirection(section, sections[0]);
 
           const sectionDetails = document.createElement('details');
-          // Fold by default for large SDP.
+          const rejected = index !== 0 &&
+              SDPUtils.parseMLine(lines[0]).port === 0;
+          // Fold by default for large SDP, inactive SDP or rejected m-lines.
           sectionDetails.open =
-            sections.length <= MAX_NUMBER_OF_EXPANDED_MEDIASECTIONS;
+            sections.length <= MAX_NUMBER_OF_EXPANDED_MEDIASECTIONS &&
+            direction !== 'inactive' && !rejected;
           sectionDetails.textContent = lines.slice(1).join('\n');
 
           const sectionSummary = document.createElement('summary');
@@ -213,6 +217,12 @@
             ' (' + (lines.length - 1) + ' more lines)' +
             (section.startsWith('m=') ? ' direction=' + direction : '') +
             (mid ? ' mid=' + mid : '');
+          if (lastSections && lastSections[index] !== sections[index]) {
+            // Open munged sections by default and give visual feedback.
+            sectionDetails.open = true;
+            sectionSummary.textContent += ' munged';
+            sectionSummary.style.backgroundColor = '#FBCEB1';
+          }
           sectionDetails.appendChild(sectionSummary);
 
           valueContainer.appendChild(sectionDetails);
diff --git a/content/browser/webrtc/resources/sdp_utils.js b/content/browser/webrtc/resources/sdp_utils.js
new file mode 100644
index 0000000..5477d50
--- /dev/null
+++ b/content/browser/webrtc/resources/sdp_utils.js
@@ -0,0 +1,49 @@
+// Copyright 2025 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Reduced copy of
+// third_party/blink/web_tests/external/wpt/webrtc/third_party/sdp/sdp.js
+
+// Splits SDP into lines, dealing with both CRLF and LF.
+export function splitLines(blob) {
+  return blob.trim().split('\n').map(line => line.trim());
+}
+
+// Splits SDP into sections, including the sessionpart.
+export function splitSections(blob) {
+  const parts = blob.split('\nm=');
+  return parts.map((part, index) => (index > 0 ?
+    'm=' + part : part).trim() + '\r\n');
+}
+
+// Gets the direction from the mediaSection or the sessionpart.
+export function getDirection(mediaSection, sessionpart) {
+  // Look for sendrecv, sendonly, recvonly, inactive, default to sendrecv.
+  const lines = splitLines(mediaSection);
+  for (let i = 0; i < lines.length; i++) {
+    switch (lines[i]) {
+      case 'a=sendrecv':
+      case 'a=sendonly':
+      case 'a=recvonly':
+      case 'a=inactive':
+        return lines[i].substring(2);
+    }
+  }
+  if (sessionpart) {
+    return getDirection(sessionpart);
+  }
+  return 'sendrecv';
+}
+
+// Parses a m= line.
+export function parseMLine(mediaSection) {
+  const lines = splitLines(mediaSection);
+  const parts = lines[0].substring(2).split(' ');
+  return {
+    kind: parts[0],
+    port: parseInt(parts[1], 10),
+    protocol: parts[2],
+    fmt: parts.slice(3).join(' '),
+  };
+}
diff --git a/content/public/android/javatests/src/org/chromium/content/browser/accessibility/WebContentsAccessibilityTreeTest.java b/content/public/android/javatests/src/org/chromium/content/browser/accessibility/WebContentsAccessibilityTreeTest.java
index c1a45fcd..44b2c76bf 100644
--- a/content/public/android/javatests/src/org/chromium/content/browser/accessibility/WebContentsAccessibilityTreeTest.java
+++ b/content/public/android/javatests/src/org/chromium/content/browser/accessibility/WebContentsAccessibilityTreeTest.java
@@ -1510,28 +1510,28 @@
 
     @Test
     @SmallTest
-    @CommandLineFlags.Add({"enable-blink-features=CanvasElementDrawElement"})
+    @CommandLineFlags.Add({"enable-blink-features=CanvasDrawElement"})
     public void test_canvasComplexFallback() {
         performHtmlTest("canvas-complex-fallback.html");
     }
 
     @Test
     @SmallTest
-    @CommandLineFlags.Add({"enable-blink-features=CanvasElementDrawElement"})
+    @CommandLineFlags.Add({"enable-blink-features=CanvasDrawElement"})
     public void test_canvasInteractiveFallback() {
         performHtmlTest("canvas-interactive-fallback.html");
     }
 
     @Test
     @SmallTest
-    @CommandLineFlags.Add({"enable-blink-features=CanvasElementDrawElement"})
+    @CommandLineFlags.Add({"enable-blink-features=CanvasDrawElement"})
     public void test_canvasFallback() {
         performHtmlTest("canvas-fallback.html");
     }
 
     @Test
     @SmallTest
-    @CommandLineFlags.Add({"enable-blink-features=CanvasElementDrawElement"})
+    @CommandLineFlags.Add({"enable-blink-features=CanvasDrawElement"})
     public void test_canvas() {
         performHtmlTest("canvas.html");
     }
diff --git a/content/public/browser/service_process_host_passkeys.h b/content/public/browser/service_process_host_passkeys.h
index 0080842..ef8ae263 100644
--- a/content/public/browser/service_process_host_passkeys.h
+++ b/content/public/browser/service_process_host_passkeys.h
@@ -17,7 +17,7 @@
 }  // namespace video_effects
 
 namespace screen_ai {
-class ScreenAIServiceRouter;
+class ScreenAIServiceHandler;
 }  // namespace screen_ai
 
 namespace on_device_translation {
@@ -36,7 +36,7 @@
 
   // Service launchers using `ServiceProcessHost::Options::WithPreloadLibraries`
   // should be added here and must be reviewed by the security team.
-  friend class screen_ai::ScreenAIServiceRouter;
+  friend class screen_ai::ScreenAIServiceHandler;
   friend video_effects::mojom::VideoEffectsService*
   video_effects::GetVideoEffectsService();
   friend class on_device_translation::OnDeviceTranslationServiceController;
diff --git a/content/shell/browser/bluetooth/ios/shell_bluetooth_chooser_mediator.h b/content/shell/browser/bluetooth/ios/shell_bluetooth_chooser_mediator.h
index 2612de1..aed696d 100644
--- a/content/shell/browser/bluetooth/ios/shell_bluetooth_chooser_mediator.h
+++ b/content/shell/browser/bluetooth/ios/shell_bluetooth_chooser_mediator.h
@@ -22,7 +22,7 @@
     content::ShellBluetoothChooserIOS* bluetoothChooser;
 
 // Consumer that is configured by this mediator.
-@property(nonatomic, assign) id<ShellBluetoothDeviceListConsumer> consumer;
+@property(nonatomic, weak) id<ShellBluetoothDeviceListConsumer> consumer;
 
 - (instancetype)initWithBluetoothChooser:
     (content::ShellBluetoothChooserIOS*)bluetoothChooser;
diff --git a/content/test/data/fuzzer_corpus/ad_auction_service_mojolpm_fuzzer/basic_auction.textproto b/content/test/data/fuzzer_corpus/ad_auction_service_mojolpm_fuzzer/basic_auction.textproto
index fd3b932..10c7660 100644
--- a/content/test/data/fuzzer_corpus/ad_auction_service_mojolpm_fuzzer/basic_auction.textproto
+++ b/content/test/data/fuzzer_corpus/ad_auction_service_mojolpm_fuzzer/basic_auction.textproto
@@ -177,6 +177,8 @@
               m_per_buyer_signals {
                 old: 1
               }
+              m_per_buyer_tkv_signals {
+              }
               m_buyer_timeouts {
                 old: 1
               }
diff --git a/crypto/sha2.cc b/crypto/sha2.cc
index 1560295..f3b0028 100644
--- a/crypto/sha2.cc
+++ b/crypto/sha2.cc
@@ -19,16 +19,8 @@
   return digest;
 }
 
-void SHA256HashString(std::string_view str, void* output, size_t len) {
-  std::unique_ptr<SecureHash> ctx(SecureHash::Create(SecureHash::SHA256));
-  ctx->Update(str.data(), str.length());
-  ctx->Finish(output, len);
-}
-
 std::string SHA256HashString(std::string_view str) {
-  std::string output(kSHA256Length, 0);
-  SHA256HashString(str, std::data(output), output.size());
-  return output;
+  return std::string(base::as_string_view(SHA256Hash(base::as_byte_span(str))));
 }
 
 }  // namespace crypto
diff --git a/crypto/sha2.h b/crypto/sha2.h
index aa3daca..9262d79 100644
--- a/crypto/sha2.h
+++ b/crypto/sha2.h
@@ -33,13 +33,6 @@
 // string.
 CRYPTO_EXPORT std::string SHA256HashString(std::string_view str);
 
-// Computes the SHA-256 hash of the input string 'str' and stores the first
-// 'len' bytes of the hash in the output buffer 'output'.  If 'len' > 32,
-// only 32 bytes (the full hash) are stored in the 'output' buffer.
-CRYPTO_EXPORT void SHA256HashString(std::string_view str,
-                                    void* output,
-                                    size_t len);
-
 }  // namespace crypto
 
 #endif  // CRYPTO_SHA2_H_
diff --git a/crypto/sha2_unittest.cc b/crypto/sha2_unittest.cc
index 3226360a..539adc0 100644
--- a/crypto/sha2_unittest.cc
+++ b/crypto/sha2_unittest.cc
@@ -2,11 +2,6 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#ifdef UNSAFE_BUFFERS_BUILD
-// TODO(crbug.com/351564777): Remove this and convert code to safer constructs.
-#pragma allow_unsafe_buffers
-#endif
-
 #include "crypto/sha2.h"
 
 #include <stddef.h>
@@ -17,95 +12,71 @@
 #include "testing/gtest/include/gtest/gtest.h"
 
 TEST(Sha256Test, Empty) {
-  const std::string empty;
-  std::string hash = crypto::SHA256HashString(empty);
-  auto expected = std::to_array<int>({
+  const auto hash = crypto::SHA256Hash(base::span<const uint8_t>());
+  const auto expected = std::to_array<uint8_t>({
       0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14, 0x9a, 0xfb, 0xf4,
       0xc8, 0x99, 0x6f, 0xb9, 0x24, 0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b,
       0x93, 0x4c, 0xa4, 0x95, 0x99, 0x1b, 0x78, 0x52, 0xb8, 0x55,
   });
-  ASSERT_EQ(hash.length(), crypto::kSHA256Length);
-  for (size_t i = 0; i < crypto::kSHA256Length; i++) {
-    EXPECT_EQ(expected[i], static_cast<uint8_t>(hash[i]));
-  }
+
+  static_assert(hash.size() == expected.size());
+  EXPECT_EQ(base::as_byte_span(expected), base::as_byte_span(hash));
 }
 
 TEST(Sha256Test, Test1) {
   // Example B.1 from FIPS 180-2: one-block message.
-  std::string input1 = "abc";
-  auto expected1 = std::to_array<int>({
+  const std::string input = "abc";
+  const auto hash = crypto::SHA256Hash(base::as_byte_span(input));
+  const auto expected = std::to_array<uint8_t>({
       0xba, 0x78, 0x16, 0xbf, 0x8f, 0x01, 0xcf, 0xea, 0x41, 0x41, 0x40,
       0xde, 0x5d, 0xae, 0x22, 0x23, 0xb0, 0x03, 0x61, 0xa3, 0x96, 0x17,
       0x7a, 0x9c, 0xb4, 0x10, 0xff, 0x61, 0xf2, 0x00, 0x15, 0xad,
   });
 
-  uint8_t output1[crypto::kSHA256Length];
-  crypto::SHA256HashString(input1, output1, sizeof(output1));
-  for (size_t i = 0; i < crypto::kSHA256Length; i++)
-    EXPECT_EQ(expected1[i], static_cast<int>(output1[i]));
-
-  uint8_t output_truncated1[4];  // 4 bytes == 32 bits
-  crypto::SHA256HashString(input1,
-                           output_truncated1, sizeof(output_truncated1));
-  for (size_t i = 0; i < sizeof(output_truncated1); i++)
-    EXPECT_EQ(expected1[i], static_cast<int>(output_truncated1[i]));
+  static_assert(hash.size() == expected.size());
+  EXPECT_EQ(base::as_byte_span(expected), base::as_byte_span(hash));
 }
 
 TEST(Sha256Test, Test1_String) {
   // Same as the above, but using the wrapper that returns a std::string.
   // Example B.1 from FIPS 180-2: one-block message.
-  std::string input1 = "abc";
-  auto expected1 = std::to_array<int>({
+  const std::string input = "abc";
+  const std::string hash = crypto::SHA256HashString(input);
+  const auto expected = std::to_array<uint8_t>({
       0xba, 0x78, 0x16, 0xbf, 0x8f, 0x01, 0xcf, 0xea, 0x41, 0x41, 0x40,
       0xde, 0x5d, 0xae, 0x22, 0x23, 0xb0, 0x03, 0x61, 0xa3, 0x96, 0x17,
       0x7a, 0x9c, 0xb4, 0x10, 0xff, 0x61, 0xf2, 0x00, 0x15, 0xad,
   });
 
-  std::string output1 = crypto::SHA256HashString(input1);
-  ASSERT_EQ(crypto::kSHA256Length, output1.size());
-  for (size_t i = 0; i < crypto::kSHA256Length; i++)
-    EXPECT_EQ(expected1[i], static_cast<uint8_t>(output1[i]));
+  ASSERT_EQ(crypto::kSHA256Length, hash.size());
+  EXPECT_EQ(base::as_byte_span(expected), base::as_byte_span(hash));
 }
 
 TEST(Sha256Test, Test2) {
   // Example B.2 from FIPS 180-2: multi-block message.
-  std::string input2 =
+  const std::string input2 =
       "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq";
-  auto expected2 = std::to_array<int>({
+  const auto hash = crypto::SHA256Hash(base::as_byte_span(input2));
+  const auto expected = std::to_array<uint8_t>({
       0x24, 0x8d, 0x6a, 0x61, 0xd2, 0x06, 0x38, 0xb8, 0xe5, 0xc0, 0x26,
       0x93, 0x0c, 0x3e, 0x60, 0x39, 0xa3, 0x3c, 0xe4, 0x59, 0x64, 0xff,
       0x21, 0x67, 0xf6, 0xec, 0xed, 0xd4, 0x19, 0xdb, 0x06, 0xc1,
   });
 
-  uint8_t output2[crypto::kSHA256Length];
-  crypto::SHA256HashString(input2, output2, sizeof(output2));
-  for (size_t i = 0; i < crypto::kSHA256Length; i++)
-    EXPECT_EQ(expected2[i], static_cast<int>(output2[i]));
-
-  uint8_t output_truncated2[6];
-  crypto::SHA256HashString(input2,
-                           output_truncated2, sizeof(output_truncated2));
-  for (size_t i = 0; i < sizeof(output_truncated2); i++)
-    EXPECT_EQ(expected2[i], static_cast<int>(output_truncated2[i]));
+  static_assert(expected.size() == hash.size());
+  EXPECT_EQ(base::as_byte_span(expected), base::as_byte_span(hash));
 }
 
 TEST(Sha256Test, Test3) {
   // Example B.3 from FIPS 180-2: long message.
-  std::string input3(1000000, 'a');  // 'a' repeated a million times
-  auto expected3 = std::to_array<int>({
+  const std::string input3(1000000, 'a');  // 'a' repeated a million times
+  const auto hash = crypto::SHA256Hash(base::as_byte_span(input3));
+  const auto expected = std::to_array<uint8_t>({
       0xcd, 0xc7, 0x6e, 0x5c, 0x99, 0x14, 0xfb, 0x92, 0x81, 0xa1, 0xc7,
       0xe2, 0x84, 0xd7, 0x3e, 0x67, 0xf1, 0x80, 0x9a, 0x48, 0xa4, 0x97,
       0x20, 0x0e, 0x04, 0x6d, 0x39, 0xcc, 0xc7, 0x11, 0x2c, 0xd0,
   });
 
-  uint8_t output3[crypto::kSHA256Length];
-  crypto::SHA256HashString(input3, output3, sizeof(output3));
-  for (size_t i = 0; i < crypto::kSHA256Length; i++)
-    EXPECT_EQ(expected3[i], static_cast<int>(output3[i]));
-
-  uint8_t output_truncated3[12];
-  crypto::SHA256HashString(input3,
-                           output_truncated3, sizeof(output_truncated3));
-  for (size_t i = 0; i < sizeof(output_truncated3); i++)
-    EXPECT_EQ(expected3[i], static_cast<int>(output_truncated3[i]));
+  static_assert(expected.size() == hash.size());
+  EXPECT_EQ(base::as_byte_span(expected), base::as_byte_span(hash));
 }
diff --git a/docs/website b/docs/website
index 406e2b2..f730458 160000
--- a/docs/website
+++ b/docs/website
@@ -1 +1 @@
-Subproject commit 406e2b2832f3d734a2ba4f8fa93fb78deecb16d2
+Subproject commit f73045886d28209ea87d06c41eafcf521906486d
diff --git a/extensions/browser/service_worker/service_worker_task_queue.h b/extensions/browser/service_worker/service_worker_task_queue.h
index 4ee0444..7a209d4 100644
--- a/extensions/browser/service_worker/service_worker_task_queue.h
+++ b/extensions/browser/service_worker/service_worker_task_queue.h
@@ -156,7 +156,7 @@
   // Browser process worker state of an activated extension.
   enum class BrowserState {
     // Initial state, not started.
-    kInitial,
+    kNotStarted,
     // Worker has completed starting at least once (i.e. has seen
     // DidStartWorkerForScope).
     kStarted,
@@ -193,7 +193,7 @@
     }
     void Reset() {
       worker_id_.reset();
-      browser_state_ = BrowserState::kInitial;
+      browser_state_ = BrowserState::kNotStarted;
       renderer_state_ = RendererState::kNotActive;
     }
 
@@ -205,7 +205,7 @@
     const std::optional<WorkerId>& worker_id() const { return worker_id_; }
 
    private:
-    BrowserState browser_state_ = BrowserState::kInitial;
+    BrowserState browser_state_ = BrowserState::kNotStarted;
     RendererState renderer_state_ = RendererState::kNotActive;
 
     // Contains the worker's WorkerId associated with this WorkerState, once we
diff --git a/internal b/internal
index d177ab1..2514606 160000
--- a/internal
+++ b/internal
@@ -1 +1 @@
-Subproject commit d177ab19a801ad5337736483002b4c7a2ca6c5c4
+Subproject commit 2514606e5e8e4ce2af8ebbddaeff318b9a987568
diff --git a/ios/chrome/app/resources/Settings.bundle/Experimental.plist b/ios/chrome/app/resources/Settings.bundle/Experimental.plist
index c767834..09e7bbf 100644
--- a/ios/chrome/app/resources/Settings.bundle/Experimental.plist
+++ b/ios/chrome/app/resources/Settings.bundle/Experimental.plist
@@ -860,6 +860,20 @@
 			<key>AutocorrectionType</key>
 			<string>No</string>
 		</dict>
+			<dict>
+			<key>Type</key>
+			<string>PSTextFieldSpecifier</string>
+			<key>Title</key>
+			<string>Lens Result Panel GWS URL</string>
+			<key>Key</key>
+			<string>LensResultPanelGwsURL</string>
+			<key>DefaultValue</key>
+			<string></string>
+			<key>KeyboardType</key>
+			<string>URL</string>
+			<key>AutocorrectionType</key>
+			<string>No</string>
+		</dict>
 	</array>
 </dict>
 </plist>
diff --git a/ios/chrome/browser/authentication/ui_bundled/authentication_flow/authentication_flow.mm b/ios/chrome/browser/authentication/ui_bundled/authentication_flow/authentication_flow.mm
index 2682d090..d405c3e 100644
--- a/ios/chrome/browser/authentication/ui_bundled/authentication_flow/authentication_flow.mm
+++ b/ios/chrome/browser/authentication/ui_bundled/authentication_flow/authentication_flow.mm
@@ -682,10 +682,23 @@
         _unsyncedDataTypes.value());
     _browserForAuthenticationFlowInProfile = _browser;
     CHECK(!_signInInProfileCompletion);
-    id<AuthenticationFlowRequestHelper> requestHelper =
+    PostSignInActionSet postSignInActions = _postSignInActions;
+    id<SystemIdentity> identityToSignIn = _identityToSignIn;
+    signin_metrics::AccessPoint accessPoint = _accessPoint;
+    // In case of sign-in in same profile, we can reuse the same browser.
+    raw_ptr<Browser> browser = _browser;
+    // In case of same profile signin, the request helper simply allows
+    // to update the view that started the authentication. If it gets
+    // deallocated, it means the view is closed, so it’s acceptable
+    // not to call its method.
+    __weak id<AuthenticationFlowRequestHelper> requestHelper =
         [self takeRequestHelper];
+    // Not using a call call to a method on self, because self will be
+    // deallocated by the time the `signinCompletion` is executed.
     _signInInProfileCompletion = ^(SigninCoordinatorResult result) {
       [requestHelper authenticationFlowDidSignInInSameProfileWithResult:result];
+      CompletePostSignInActions(postSignInActions, identityToSignIn, browser,
+                                accessPoint);
     };
     [self continueFlow];
     return;
@@ -709,7 +722,9 @@
   [_performer switchToProfileWithIdentity:_identityToSignIn
                                sceneState:sceneState
                                    reason:reason
-                            requestHelper:[self takeRequestHelper]];
+                            requestHelper:[self takeRequestHelper]
+                        postSignInActions:_postSignInActions
+                              accessPoint:_accessPoint];
 }
 
 // Hands the sign-in flow over to `AuthenticationFlowInProfile`. This step is
diff --git a/ios/chrome/browser/authentication/ui_bundled/authentication_flow/authentication_flow_in_profile.mm b/ios/chrome/browser/authentication/ui_bundled/authentication_flow/authentication_flow_in_profile.mm
index 5d7de86c..09ce692 100644
--- a/ios/chrome/browser/authentication/ui_bundled/authentication_flow/authentication_flow_in_profile.mm
+++ b/ios/chrome/browser/authentication/ui_bundled/authentication_flow/authentication_flow_in_profile.mm
@@ -372,8 +372,6 @@
   signin_ui::SigninCompletionCallback signInCompletion = _signInCompletion;
   _signInCompletion = nil;
   signInCompletion(SigninCoordinatorResult::SigninCoordinatorResultSuccess);
-  CompletePostSignInActions(_postSignInActions, _identityToSignIn, _browser,
-                            _accessPoint);
   [self continueFlow];
 }
 
@@ -405,7 +403,10 @@
   [_performer switchToProfileWithName:personalProfileName
                            sceneState:sceneState
                                reason:ChangeProfileReason::kAuthenticationError
-            changeProfileContinuation:DoNothingContinuation()];
+            changeProfileContinuation:DoNothingContinuation()
+                    postSignInActions:_postSignInActions
+                         withIdentity:_identityToSignIn
+                          accessPoint:_accessPoint];
 }
 
 - (void)failureCompleteFlowStep {
diff --git a/ios/chrome/browser/authentication/ui_bundled/authentication_flow/authentication_flow_performer.h b/ios/chrome/browser/authentication/ui_bundled/authentication_flow/authentication_flow_performer.h
index bee4057..88f5809 100644
--- a/ios/chrome/browser/authentication/ui_bundled/authentication_flow/authentication_flow_performer.h
+++ b/ios/chrome/browser/authentication/ui_bundled/authentication_flow/authentication_flow_performer.h
@@ -105,13 +105,18 @@
                          sceneState:(SceneState*)sceneState
                              reason:(ChangeProfileReason)reason
                       requestHelper:
-                          (id<AuthenticationFlowRequestHelper>)requestHelper;
+                          (id<AuthenticationFlowRequestHelper>)requestHelper
+                  postSignInActions:(PostSignInActionSet)postSignInActions
+                        accessPoint:(signin_metrics::AccessPoint)accessPoint;
 
 // Switches to the profile with `profileName`, for `sceneIdentifier`.
 - (void)switchToProfileWithName:(const std::string&)profileName
                      sceneState:(SceneState*)sceneState
                          reason:(ChangeProfileReason)reason
-      changeProfileContinuation:(ChangeProfileContinuation)continuation;
+      changeProfileContinuation:(ChangeProfileContinuation)continuation
+              postSignInActions:(PostSignInActionSet)postSignInActions
+                   withIdentity:(id<SystemIdentity>)identity
+                    accessPoint:(signin_metrics::AccessPoint)accessPoint;
 
 // Converts the personal profile to a managed one and attaches `identity` to it.
 - (void)makePersonalProfileManagedWithIdentity:(id<SystemIdentity>)identity;
diff --git a/ios/chrome/browser/authentication/ui_bundled/authentication_flow/authentication_flow_performer.mm b/ios/chrome/browser/authentication/ui_bundled/authentication_flow/authentication_flow_performer.mm
index 86695c2..1267420 100644
--- a/ios/chrome/browser/authentication/ui_bundled/authentication_flow/authentication_flow_performer.mm
+++ b/ios/chrome/browser/authentication/ui_bundled/authentication_flow/authentication_flow_performer.mm
@@ -208,6 +208,27 @@
       showSnackbarMessageOverBrowserToolbar:snackbar_title];
 }
 
+void CompletePostSignInActionsContinuationImpl(
+    PostSignInActionSet post_signin_actions,
+    id<SystemIdentity> identity,
+    signin_metrics::AccessPoint access_point,
+    SceneState* scene_state,
+    base::OnceClosure closure) {
+  Browser* browser =
+      scene_state.browserProviderInterface.currentBrowserProvider.browser;
+  CompletePostSignInActions(post_signin_actions, identity, browser,
+                            access_point);
+  std::move(closure).Run();
+}
+
+ChangeProfileContinuation CompletePostSigninActionsContinuation(
+    PostSignInActionSet post_signin_actions,
+    id<SystemIdentity> identity,
+    signin_metrics::AccessPoint access_point) {
+  return base::BindOnce(&CompletePostSignInActionsContinuationImpl,
+                        post_signin_actions, identity, access_point);
+}
+
 }  // namespace
 
 void CompletePostSignInActions(PostSignInActionSet post_signin_actions,
@@ -420,10 +441,14 @@
                          sceneState:(SceneState*)sceneState
                              reason:(ChangeProfileReason)reason
                       requestHelper:
-                          (id<AuthenticationFlowRequestHelper>)requestHelper {
+                          (id<AuthenticationFlowRequestHelper>)requestHelper
+                  postSignInActions:(PostSignInActionSet)postSignInActions
+                        accessPoint:(signin_metrics::AccessPoint)accessPoint {
   CHECK(AreSeparateProfilesForManagedAccountsEnabled());
   CHECK(requestHelper);
-  ChangeProfileContinuation continuation =
+  // The continuation specific to the place where the authentication was
+  // launched.
+  ChangeProfileContinuation requestHelperContinuation =
       [requestHelper authenticationFlowWillChangeProfile];
 
   std::optional<std::string> profileName =
@@ -444,19 +469,30 @@
   [self switchToProfileWithName:*profileName
                      sceneState:sceneState
                          reason:reason
-      changeProfileContinuation:std::move(continuation)];
+      changeProfileContinuation:std::move(requestHelperContinuation)
+              postSignInActions:postSignInActions
+                   withIdentity:identity
+                    accessPoint:accessPoint];
 }
 
 - (void)switchToProfileWithName:(const std::string&)profileName
                      sceneState:(SceneState*)sceneState
                          reason:(ChangeProfileReason)reason
-      changeProfileContinuation:(ChangeProfileContinuation)continuation {
+      changeProfileContinuation:
+          (ChangeProfileContinuation)requestHelperContinuation
+              postSignInActions:(PostSignInActionSet)postSignInActions
+                   withIdentity:(id<SystemIdentity>)identity
+                    accessPoint:(signin_metrics::AccessPoint)accessPoint {
   CHECK(AreSeparateProfilesForManagedAccountsEnabled());
-
+  ChangeProfileContinuation postSignInContinuation =
+      CompletePostSigninActionsContinuation(postSignInActions, identity,
+                                            accessPoint);
   ChangeProfileContinuation authenticationFlowContinuation =
       [self authenticationFlowContinuation];
   ChangeProfileContinuation fullContinuation = ChainChangeProfileContinuations(
-      std::move(authenticationFlowContinuation), std::move(continuation));
+      std::move(authenticationFlowContinuation),
+      ChainChangeProfileContinuations(std::move(requestHelperContinuation),
+                                      std::move(postSignInContinuation)));
   [_changeProfileHandler changeProfile:profileName
                               forScene:sceneState
                                 reason:reason
diff --git a/ios/chrome/browser/authentication/ui_bundled/authentication_flow/authentication_flow_unittest.mm b/ios/chrome/browser/authentication/ui_bundled/authentication_flow/authentication_flow_unittest.mm
index 77ee361..9e8c632 100644
--- a/ios/chrome/browser/authentication/ui_bundled/authentication_flow/authentication_flow_unittest.mm
+++ b/ios/chrome/browser/authentication/ui_bundled/authentication_flow/authentication_flow_unittest.mm
@@ -270,6 +270,7 @@
               signin_metrics::AccessPoint access_point,
               bool adds_history_screen_post_profile_switch = true) {
     signin_result_ = signin::Tribool::kUnknown;
+
     // Can't use a RunLoop multiple times, create a new one.
     run_loop_ = std::make_unique<base::RunLoop>();
 
@@ -277,6 +278,14 @@
                              /*shouldHandOverToFlowInProfile=*/YES);
 
     NSString* hosted_domain = GetHostedDomainFromEmail(identity.userEmail);
+    const bool should_switch_profile =
+        hosted_domain.length && AreSeparateProfilesForManagedAccountsEnabled();
+
+    PostSignInActionSet postSignInActions;
+    if (should_switch_profile && adds_history_screen_post_profile_switch) {
+      postSignInActions.Put(
+          PostSignInAction::kShowHistorySyncScreenAfterProfileSwitch);
+    }
     auto fetchManagedStatusCallback = ^(NSInvocation*) {
       [authentication_flow_ didFetchManagedStatus:hosted_domain];
     };
@@ -352,7 +361,9 @@
                                  sceneState:personal_browser_->GetSceneState()
                                      reason:ChangeProfileReason::
                                                 kManagedAccountSignIn
-                              requestHelper:requestHelperChecker])
+                              requestHelper:requestHelperChecker
+                          postSignInActions:postSignInActions
+                                accessPoint:access_point])
             .andDo(switchToProfileWithIdentityCallback);
       }
       auto registerUserPolicyCallback = ^(NSInvocation*) {
@@ -375,9 +386,6 @@
           .andDo(fetchUserPolicyCallback);
     }
 
-    const bool should_switch_profile =
-        hosted_domain.length && AreSeparateProfilesForManagedAccountsEnabled();
-
     // If switching (to a managed profile), there's no explicit call to sign in,
     // since AuthenticationService does it internally.
     if (!should_switch_profile) {
@@ -386,12 +394,6 @@
                                  currentProfile:personal_profile_.get()]);
     }
 
-    PostSignInActionSet postSignInActions;
-    if (should_switch_profile && adds_history_screen_post_profile_switch) {
-      postSignInActions.Put(
-          PostSignInAction::kShowHistorySyncScreenAfterProfileSwitch);
-    }
-
     [authentication_flow_ startSignIn];
     // The completion block should not be called synchronously.
     EXPECT_EQ(signin::Tribool::kUnknown, signin_result_);
diff --git a/ios/chrome/browser/collaboration/model/messaging/instant_messaging_service.h b/ios/chrome/browser/collaboration/model/messaging/instant_messaging_service.h
index 37f877c..429a313 100644
--- a/ios/chrome/browser/collaboration/model/messaging/instant_messaging_service.h
+++ b/ios/chrome/browser/collaboration/model/messaging/instant_messaging_service.h
@@ -5,7 +5,10 @@
 #ifndef IOS_CHROME_BROWSER_COLLABORATION_MODEL_MESSAGING_INSTANT_MESSAGING_SERVICE_H_
 #define IOS_CHROME_BROWSER_COLLABORATION_MODEL_MESSAGING_INSTANT_MESSAGING_SERVICE_H_
 
+#import <set>
+
 #import "base/memory/raw_ptr.h"
+#import "base/uuid.h"
 #import "components/collaboration/public/messaging/messaging_backend_service.h"
 #import "components/keyed_service/core/keyed_service.h"
 
@@ -31,6 +34,8 @@
       collaboration::messaging::InstantMessage message,
       MessagingBackendService::InstantMessageDelegate::SuccessCallback
           success_callback) override;
+  void HideInstantaneousMessage(
+      const std::set<base::Uuid>& message_ids) override;
 
  private:
   // Shows a collaboration group infobar for the given `instant_message`.
diff --git a/ios/chrome/browser/collaboration/model/messaging/instant_messaging_service.mm b/ios/chrome/browser/collaboration/model/messaging/instant_messaging_service.mm
index f9fb6b56..84450ce 100644
--- a/ios/chrome/browser/collaboration/model/messaging/instant_messaging_service.mm
+++ b/ios/chrome/browser/collaboration/model/messaging/instant_messaging_service.mm
@@ -4,7 +4,10 @@
 
 #import "ios/chrome/browser/collaboration/model/messaging/instant_messaging_service.h"
 
+#import <set>
+
 #import "base/functional/callback.h"
+#import "base/uuid.h"
 #import "ios/chrome/browser/collaboration/model/messaging/infobar/collaboration_group_infobar_delegate.h"
 #import "ios/chrome/browser/infobars/model/infobar_ios.h"
 #import "ios/chrome/browser/infobars/model/infobar_manager_impl.h"
@@ -36,6 +39,11 @@
   std::move(success_callback).Run(message_displayed);
 }
 
+void InstantMessagingService::HideInstantaneousMessage(
+    const std::set<base::Uuid>& message_ids) {
+  // TODO(crbug.com/416265501) Implement this.
+}
+
 bool InstantMessagingService::ShowCollaborationGroupInfobar(
     collaboration::messaging::InstantMessage instant_message) {
   bool infobar_displayed =
diff --git a/ios/chrome/browser/lens_overlay/model/BUILD.gn b/ios/chrome/browser/lens_overlay/model/BUILD.gn
index 6476d5e3..6c3470c9 100644
--- a/ios/chrome/browser/lens_overlay/model/BUILD.gn
+++ b/ios/chrome/browser/lens_overlay/model/BUILD.gn
@@ -55,6 +55,7 @@
   deps = [
     "//components/google/core/common",
     "//components/lens",
+    "//ios/chrome/browser/shared/public/features:system_flags",
     "//ios/web/public/navigation",
     "//net",
     "//url",
diff --git a/ios/chrome/browser/lens_overlay/model/lens_overlay_url_utils.mm b/ios/chrome/browser/lens_overlay/model/lens_overlay_url_utils.mm
index ac70014d..ba3ff83f 100644
--- a/ios/chrome/browser/lens_overlay/model/lens_overlay_url_utils.mm
+++ b/ios/chrome/browser/lens_overlay/model/lens_overlay_url_utils.mm
@@ -8,11 +8,19 @@
 
 #import "components/google/core/common/google_util.h"
 #import "components/lens/lens_url_utils.h"
+#import "ios/chrome/browser/shared/public/features/system_flags.h"
 #import "net/base/url_util.h"
 
 namespace lens {
 
 bool IsGoogleHostURL(const GURL& url) {
+  // Only available for debug builds.
+  if (experimental_flags::GetLensResultPanelGwsURL() != nil) {
+    return google_util::IsGoogleDomainUrl(
+        url, google_util::ALLOW_SUBDOMAIN,
+        google_util::ALLOW_NON_STANDARD_PORTS);
+  }
+
   return google_util::IsGoogleDomainUrl(
       url, google_util::DISALLOW_SUBDOMAIN,
       google_util::DISALLOW_NON_STANDARD_PORTS);
diff --git a/ios/chrome/browser/shared/public/features/system_flags.h b/ios/chrome/browser/shared/public/features/system_flags.h
index 2c679f0b..5de741b2 100644
--- a/ios/chrome/browser/shared/public/features/system_flags.h
+++ b/ios/chrome/browser/shared/public/features/system_flags.h
@@ -185,6 +185,9 @@
 // Enables the AI menu, which is a tool for debugging LLM queries.
 bool EnableAIPrototypingMenu();
 
+// Gets GWS URL base used to generate Lens result panel URLs. Returns nil if
+// there is no alternative URL specified.
+NSString* GetLensResultPanelGwsURL();
 }  // namespace experimental_flags
 
 #endif  // IOS_CHROME_BROWSER_SHARED_PUBLIC_FEATURES_SYSTEM_FLAGS_H_
diff --git a/ios/chrome/browser/shared/public/features/system_flags.mm b/ios/chrome/browser/shared/public/features/system_flags.mm
index a6fb3708..d94e45e 100644
--- a/ios/chrome/browser/shared/public/features/system_flags.mm
+++ b/ios/chrome/browser/shared/public/features/system_flags.mm
@@ -66,6 +66,7 @@
 NSString* const kInactiveTabsDemoMode = @"InactiveTabsDemoMode";
 NSString* const kInactiveTabsTestMode = @"InactiveTabsTestMode";
 NSString* const kAsyncStartupOverrideResponse = @"AsyncStartupOverrideResponse";
+NSString* const kLensResultPanelGwsURL = @"LensResultPanelGwsURL";
 }  // namespace
 
 namespace experimental_flags {
@@ -355,4 +356,9 @@
       boolForKey:@"EnableAIPrototypingMenu"];
 }
 
+NSString* GetLensResultPanelGwsURL() {
+  return [[NSUserDefaults standardUserDefaults]
+      stringForKey:kLensResultPanelGwsURL];
+}
+
 }  // namespace experimental_flags
diff --git a/ios/chrome/common/credential_provider/memory_credential_store.mm b/ios/chrome/common/credential_provider/memory_credential_store.mm
index 039313e..8b0bf2a 100644
--- a/ios/chrome/common/credential_provider/memory_credential_store.mm
+++ b/ios/chrome/common/credential_provider/memory_credential_store.mm
@@ -41,7 +41,7 @@
   __block NSArray<id<Credential>>* credentials;
   __weak __typeof(self) weakSelf = self;
   dispatch_sync(self.workingQueue, ^{
-    credentials = [weakSelf.memoryStorage allValues];
+    credentials = [weakSelf allMemoryStorageValues];
   });
   return credentials;
 }
@@ -51,7 +51,7 @@
   CHECK(completion);
   __weak __typeof(self) weakSelf = self;
   dispatch_async(self.workingQueue, ^{
-    completion([weakSelf.memoryStorage allValues]);
+    completion([weakSelf allMemoryStorageValues]);
   });
 }
 
@@ -121,4 +121,11 @@
   return [[NSMutableDictionary alloc] init];
 }
 
+#pragma mark - Private
+
+// Returns all values from the `memoryStorage` dictionary.
+- (NSArray<ArchivableCredential*>*)allMemoryStorageValues {
+  return [self.memoryStorage allValues];
+}
+
 @end
diff --git a/media/formats/webm/webm_projection_parser.cc b/media/formats/webm/webm_projection_parser.cc
index 8603e66..66a74cd 100644
--- a/media/formats/webm/webm_projection_parser.cc
+++ b/media/formats/webm/webm_projection_parser.cc
@@ -142,4 +142,13 @@
   return true;
 }
 
+VideoTransformation WebMProjectionParser::GetVideoTransformation() const {
+  DCHECK(Validate());
+  CHECK_GE(pose_yaw_, -180.0);
+  CHECK_LE(pose_yaw_, 180.0);
+  constexpr double kYawMirrorThreshold = 1.0;
+  return media::VideoTransformation(
+      pose_roll_, std::abs(std::abs(pose_yaw_) - 180.0) < kYawMirrorThreshold);
+}
+
 }  // namespace media
diff --git a/media/formats/webm/webm_projection_parser.h b/media/formats/webm/webm_projection_parser.h
index 52568fe..253abc3 100644
--- a/media/formats/webm/webm_projection_parser.h
+++ b/media/formats/webm/webm_projection_parser.h
@@ -7,6 +7,7 @@
 
 #include "base/memory/raw_ptr.h"
 #include "media/base/media_log.h"
+#include "media/base/video_transformation.h"
 #include "media/formats/webm/webm_parser.h"
 
 namespace media {
@@ -24,6 +25,8 @@
   void Reset();
   bool Validate() const;
 
+  VideoTransformation GetVideoTransformation() const;
+
  private:
   friend class WebMProjectionParserTest;
 
diff --git a/media/formats/webm/webm_video_client.cc b/media/formats/webm/webm_video_client.cc
index 0f97cf6a..49f1ad0 100644
--- a/media/formats/webm/webm_video_client.cc
+++ b/media/formats/webm/webm_video_client.cc
@@ -76,8 +76,10 @@
   display_unit_ = -1;
   alpha_mode_ = -1;
   colour_parsed_ = false;
+  colour_parser_.Reset();
   stereo_mode_ = -1;
   projection_parsed_ = false;
+  projection_parser_.Reset();
 }
 
 bool WebMVideoClient::InitializeConfig(
@@ -97,6 +99,9 @@
     is_8bit = color_metadata.BitsPerChannel <= 8;
   }
 
+  VideoTransformation transformation =
+      projection_parsed_ ? projection_parser_.GetVideoTransformation()
+                         : kNoTransformation;
   VideoCodec video_codec = VideoCodec::kUnknown;
   VideoCodecProfile profile = VIDEO_CODEC_PROFILE_UNKNOWN;
   if (codec_id == "V_VP8") {
@@ -163,7 +168,7 @@
                      alpha_mode_ == 1
                          ? VideoDecoderConfig::AlphaMode::kHasAlpha
                          : VideoDecoderConfig::AlphaMode::kIsOpaque,
-                     color_space, kNoTransformation, coded_size, visible_rect,
+                     color_space, transformation, coded_size, visible_rect,
                      natural_size, codec_private, encryption_scheme);
 
   return config->IsValidConfig();
diff --git a/media/formats/webm/webm_video_client_unittest.cc b/media/formats/webm/webm_video_client_unittest.cc
index a8de5a6..f5b78a7 100644
--- a/media/formats/webm/webm_video_client_unittest.cc
+++ b/media/formats/webm/webm_video_client_unittest.cc
@@ -198,6 +198,90 @@
   OnUInt(kWebMIdStereoMode, 1);
 }
 
+TEST_F(WebMVideoClientTest, VerifyTransformationFromProjection) {
+  const auto perform_projection_test =
+      [&](double roll, double yaw,
+          media::VideoTransformation expected_transformation) {
+        SCOPED_TRACE(
+            testing::Message()
+            << "roll: " << roll << ", yaw: " << yaw
+            << ", expected_rotation: " << expected_transformation.rotation
+            << ", expected_mirrored: " << expected_transformation.mirrored);
+
+        webm_video_client_.Reset();
+        OnUInt(kWebMIdPixelWidth, kCodedSize.width());
+        OnUInt(kWebMIdPixelHeight, kCodedSize.height());
+
+        WebMParserClient* projection_parser_client =
+            OnListStart(kWebMIdProjection);
+        ASSERT_NE(projection_parser_client, nullptr);
+        ASSERT_TRUE(projection_parser_client->OnUInt(kWebMIdProjectionType,
+                                                     0));  // 0 for rectangular
+
+        ASSERT_TRUE(
+            projection_parser_client->OnFloat(kWebMIdProjectionPoseYaw, yaw));
+        ASSERT_TRUE(
+            projection_parser_client->OnFloat(kWebMIdProjectionPosePitch, 0.0));
+        ASSERT_TRUE(
+            projection_parser_client->OnFloat(kWebMIdProjectionPoseRoll, roll));
+        OnListEnd(kWebMIdProjection);
+
+        VideoDecoderConfig config;
+        EXPECT_TRUE(webm_video_client_.InitializeConfig(
+            "V_VP9", {}, EncryptionScheme::kUnencrypted, &config));
+
+        EXPECT_EQ(config.video_transformation().rotation,
+                  expected_transformation.rotation);
+        EXPECT_EQ(config.video_transformation().mirrored,
+                  expected_transformation.mirrored);
+      };
+
+  const auto verify_roll = [&](double roll_degrees,
+                               VideoRotation expected_rotation_enum) {
+    perform_projection_test(roll_degrees, /*yaw=*/0.0,
+                            media::VideoTransformation(expected_rotation_enum));
+  };
+
+  // Test cases for roll values, checking snapping to 0, 90, 180, 270 degrees.
+  // VIDEO_ROTATION_0
+  verify_roll(0.0, VIDEO_ROTATION_0);
+  verify_roll(44.9, VIDEO_ROTATION_0);
+  verify_roll(-44.9, VIDEO_ROTATION_0);
+
+  // VIDEO_ROTATION_90
+  verify_roll(90.0, VIDEO_ROTATION_90);
+  verify_roll(45.0, VIDEO_ROTATION_90);
+  verify_roll(134.9, VIDEO_ROTATION_90);
+
+  // VIDEO_ROTATION_180
+  verify_roll(180.0, VIDEO_ROTATION_180);
+  verify_roll(135.0, VIDEO_ROTATION_180);
+  verify_roll(-180.0, VIDEO_ROTATION_180);
+
+  // VIDEO_ROTATION_270
+  verify_roll(-90.0, VIDEO_ROTATION_270);
+  verify_roll(-45.1, VIDEO_ROTATION_270);
+
+  const auto verify_yaw = [&](double yaw_degrees, bool expected_mirrored) {
+    perform_projection_test(
+        0.0, yaw_degrees,
+        media::VideoTransformation(VIDEO_ROTATION_0, expected_mirrored));
+  };
+
+  // Yaw mirror threshold is 1.0. Mirrored if abs(abs(yaw) - 180) < 1.0.
+  constexpr double kYawMirrorThreshold = 1.0;
+
+  // Test cases for yaw values (mirroring)
+  constexpr bool kMirrored = true;
+  verify_yaw(180.0, kMirrored);
+  verify_yaw(-180.0, kMirrored);
+  verify_yaw(0.0, !kMirrored);
+  verify_yaw(1.0, !kMirrored);
+  verify_yaw(-1.0, !kMirrored);
+  verify_yaw(180.0 - kYawMirrorThreshold, !kMirrored);
+  verify_yaw(-180.0 + kYawMirrorThreshold, !kMirrored);
+}
+
 INSTANTIATE_TEST_SUITE_P(All,
                          WebMVideoClientTest,
                          ::testing::ValuesIn(kCodecTestParams));
diff --git a/mojo/core/channel.cc b/mojo/core/channel.cc
index 82ee5b1a..4f61e6f 100644
--- a/mojo/core/channel.cc
+++ b/mojo/core/channel.cc
@@ -464,7 +464,7 @@
 
 const void* Channel::Message::extra_header() const {
   DCHECK(!is_legacy_message());
-  return reinterpret_cast<const uint8_t*>(data()) + sizeof(Header);
+  return data_span().subspan(sizeof(Header)).data();
 }
 
 void* Channel::Message::mutable_extra_header() {
@@ -582,7 +582,8 @@
   // performance issue when dealing with large messages. Any sanitizer errors
   // complaining about an uninitialized read in the payload area should be
   // treated as an error and fixed.
-  memset(mutable_data(), 0, header_size + extra_header_size);
+  std::ranges::fill(mutable_data_span().first(header_size + extra_header_size),
+                    0);
 
   DCHECK(base::IsValueInRangeForNumericType<uint32_t>(size_));
   legacy_header()->num_bytes = static_cast<uint32_t>(size_);
@@ -728,7 +729,8 @@
   }
 
   auto message = base::WrapUnique(new TrivialMessage);
-  memset(message->mutable_data(), 0, sizeof(TrivialMessage::data_));
+  std::ranges::fill(
+      message->mutable_data_span().first(sizeof(TrivialMessage::data_)), 0);
 
   DCHECK(base::IsValueInRangeForNumericType<uint32_t>(size));
   message->size_ = size;
@@ -1091,11 +1093,10 @@
     extra_header_size = header->num_header_bytes - sizeof(Message::Header);
     extra_header = extra_header_size ? header + 1 : nullptr;
     payload_size = header->num_bytes - header->num_header_bytes;
-    payload =
-        payload_size
-            ? reinterpret_cast<Message::Header*>(
-                  const_cast<char*>(buffer.data()) + header->num_header_bytes)
-            : nullptr;
+    payload = payload_size
+                  ? reinterpret_cast<Message::Header*>(const_cast<char*>(
+                        buffer.subspan(header->num_header_bytes).data()))
+                  : nullptr;
   } else {
     payload_size = legacy_header->num_bytes - sizeof(Message::LegacyHeader);
     payload = payload_size
diff --git a/mojo/core/ipcz_driver/invitation.cc b/mojo/core/ipcz_driver/invitation.cc
index 001b146..b353e4d 100644
--- a/mojo/core/ipcz_driver/invitation.cc
+++ b/mojo/core/ipcz_driver/invitation.cc
@@ -2,18 +2,14 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#ifdef UNSAFE_BUFFERS_BUILD
-// TODO(crbug.com/351564777): Remove this and convert code to safer constructs.
-#pragma allow_unsafe_buffers
-#endif
-
 #include "mojo/core/ipcz_driver/invitation.h"
 
-#include <string.h>
-
 #include <algorithm>
+#include <array>
 #include <cstdint>
+#include <string>
 
+#include "base/numerics/byte_conversions.h"
 #include "build/build_config.h"
 #include "mojo/core/ipcz_api.h"
 #include "mojo/core/ipcz_driver/base_shared_memory_service.h"
@@ -63,8 +59,7 @@
   }
 
   // Otherwise interpret the first 4 bytes as an integer.
-  uint32_t index;
-  memcpy(&index, name.data(), sizeof(uint32_t));
+  uint32_t index = base::U32FromLittleEndian(name.first<4u>());
   if (index < Invitation::kMaxAttachments) {
     // The resulting index is small enough to fit within the normal index range,
     // so assume case (b) above:
@@ -314,9 +309,10 @@
   // Note that we reserve the first initial portal for internal use, hence the
   // additional (kMaxAttachments + 1) portal here. Portals corresponding to
   // application-provided attachments begin at index 1.
-  IpczHandle portals[kMaxAttachments + 1];
-  IpczResult result = GetIpczAPI().ConnectNode(
-      GetIpczNode(), transport, num_attachments_ + 1, flags, nullptr, portals);
+  std::array<IpczHandle, kMaxAttachments + 1> portals;
+  IpczResult result =
+      GetIpczAPI().ConnectNode(GetIpczNode(), transport, num_attachments_ + 1,
+                               flags, nullptr, portals.data());
   if (result != IPCZ_RESULT_OK) {
     return result;
   }
@@ -403,7 +399,7 @@
   // Note that we reserve the first portal slot for internal use, hence an
   // the additional (kMaxAttachments + 1) portal here. Portals corresponding to
   // application-provided attachments begin at index 1.
-  IpczHandle portals[kMaxAttachments + 1];
+  std::array<IpczHandle, kMaxAttachments + 1> portals;
   IpczDriverHandle transport = CreateTransportForMojoEndpoint(
       {.source = is_isolated ? Transport::kBroker : Transport::kNonBroker,
        .destination = Transport::kBroker},
@@ -432,8 +428,9 @@
         std::move(remote_process));
   }
 
-  IpczResult result = GetIpczAPI().ConnectNode(
-      GetIpczNode(), transport, kMaxAttachments + 1, flags, nullptr, portals);
+  IpczResult result =
+      GetIpczAPI().ConnectNode(GetIpczNode(), transport, kMaxAttachments + 1,
+                               flags, nullptr, portals.data());
   CHECK_EQ(result, IPCZ_RESULT_OK);
 
   BaseSharedMemoryService::CreateClient(ScopedIpczHandle(portals[0]));
diff --git a/mojo/public/mojom/base/BUILD.gn b/mojo/public/mojom/base/BUILD.gn
index 9770d52..1754975 100644
--- a/mojo/public/mojom/base/BUILD.gn
+++ b/mojo/public/mojom/base/BUILD.gn
@@ -16,6 +16,7 @@
     "big_string.mojom",
     "binder.mojom",
     "byte_string.mojom",
+    "empty.mojom",
     "error.mojom",
     "file.mojom",
     "file_error.mojom",
diff --git a/mojo/public/mojom/base/empty.mojom b/mojo/public/mojom/base/empty.mojom
new file mode 100644
index 0000000..dd29d293
--- /dev/null
+++ b/mojo/public/mojom/base/empty.mojom
@@ -0,0 +1,10 @@
+// Copyright 2025 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+module mojo_base.mojom;
+
+// A struct that contains nothing. This can be useful in places where
+// mojo has to return something, but the interface has nothing interesting
+// to return.
+struct Empty {};
diff --git a/net/http/http_stream_pool_attempt_manager.cc b/net/http/http_stream_pool_attempt_manager.cc
index 9a957ac..a2b6aafb 100644
--- a/net/http/http_stream_pool_attempt_manager.cc
+++ b/net/http/http_stream_pool_attempt_manager.cc
@@ -281,8 +281,8 @@
     // already took the ownership of the idle stream socket. If we don't create
     // an HttpBasicStream here, another call of this method might exceed the
     // per-group limit.
-    CreateTextBasedStreamAndNotify(std::move(stream_socket), reuse_type,
-                                   LoadTimingInfo::ConnectTiming());
+    CreateTextBasedStreamAndMaybeNotify(std::move(stream_socket), reuse_type,
+                                        LoadTimingInfo::ConnectTiming());
     return;
   }
 
@@ -478,8 +478,8 @@
     if (stream_socket) {
       const StreamSocketHandle::SocketReuseType reuse_type =
           GetReuseTypeFromIdleStreamSocket(*stream_socket);
-      CreateTextBasedStreamAndNotify(std::move(stream_socket), reuse_type,
-                                     LoadTimingInfo::ConnectTiming());
+      CreateTextBasedStreamAndMaybeNotify(std::move(stream_socket), reuse_type,
+                                          LoadTimingInfo::ConnectTiming());
       return;
     }
   }
@@ -1571,7 +1571,7 @@
   job->OnPreconnectComplete(rv);
 }
 
-void HttpStreamPool::AttemptManager::CreateTextBasedStreamAndNotify(
+void HttpStreamPool::AttemptManager::CreateTextBasedStreamAndMaybeNotify(
     std::unique_ptr<StreamSocket> stream_socket,
     StreamSocketHandle::SocketReuseType reuse_type,
     LoadTimingInfo::ConnectTiming connect_timing) {
@@ -1585,6 +1585,15 @@
       << "active=" << group_->ActiveStreamSocketCount()
       << ", limit=" << pool()->max_stream_sockets_per_group();
 
+  if (jobs_.empty()) {
+    // The ownership of the underlying `stream_socket` of `http_stream` will be
+    // moved to the group as an idle stream.
+    // TODO(crbug.com/396998469): Better to move `stream_socket` directly to
+    // the group as an idle stream without creating an HttpStream. Currently we
+    // depends on the fact that the group processes pending preconnects when
+    // the handle of the HttpStream is returned to the group.
+    return;
+  }
   NotifyStreamReady(std::move(http_stream), negotiated_protocol);
   // `this` may be deleted.
 }
@@ -1658,11 +1667,7 @@
     std::unique_ptr<HttpStream> stream,
     NextProto negotiated_protocol) {
   Job* job = ExtractFirstJobToNotify();
-  if (!job) {
-    // The ownership of the stream will be moved to the group as `stream` is
-    // going to be destructed.
-    return;
-  }
+  CHECK(job);
   TRACE_EVENT_INSTANT("net.stream", "AttemptManager::NotifyStreamReady", track_,
                       NetLogWithSourceToFlow(job->request_net_log()),
                       "negotiated_protocol", negotiated_protocol);
@@ -1835,8 +1840,8 @@
                                          group_->ActiveStreamSocketCount() + 1);
 
   CHECK_NE(stream_socket->GetNegotiatedProtocol(), NextProto::kProtoHTTP2);
-  CreateTextBasedStreamAndNotify(std::move(stream_socket), reuse_type,
-                                 std::move(connect_timing));
+  CreateTextBasedStreamAndMaybeNotify(std::move(stream_socket), reuse_type,
+                                      std::move(connect_timing));
 }
 
 void HttpStreamPool::AttemptManager::OnTcpBasedAttemptSlow(
diff --git a/net/http/http_stream_pool_attempt_manager.h b/net/http/http_stream_pool_attempt_manager.h
index 0ed60b6..adf31d9 100644
--- a/net/http/http_stream_pool_attempt_manager.h
+++ b/net/http/http_stream_pool_attempt_manager.h
@@ -441,8 +441,9 @@
   void NotifyJobOfPreconnectCompleteLater(Job* job, int rv);
   void NotifyJobOfPreconnectComplete(Job* job, int rv);
 
-  // Creates a text based stream and notifies the highest priority job.
-  void CreateTextBasedStreamAndNotify(
+  // Creates a text based stream. Notifies the highest priority job if there are
+  // waiting jobs. Otherwise, `stream_socket` becomes an idle stream.
+  void CreateTextBasedStreamAndMaybeNotify(
       std::unique_ptr<StreamSocket> stream_socket,
       StreamSocketHandle::SocketReuseType reuse_type,
       LoadTimingInfo::ConnectTiming connect_timing);
diff --git a/net/http/transport_security_state_static.pins b/net/http/transport_security_state_static.pins
index 6a67473..22973c08 100644
--- a/net/http/transport_security_state_static.pins
+++ b/net/http/transport_security_state_static.pins
@@ -43,9 +43,9 @@
 #   hash function for preloaded entries again (we have already done so once).
 #
 
-# Last updated: 2025-05-09 12:53 UTC
+# Last updated: 2025-05-11 12:55 UTC
 PinsListTimestamp
-1746795212
+1746968105
 
 TestSPKI
 sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
diff --git a/net/http/transport_security_state_static_pins.json b/net/http/transport_security_state_static_pins.json
index 25751673..a1e0820 100644
--- a/net/http/transport_security_state_static_pins.json
+++ b/net/http/transport_security_state_static_pins.json
@@ -31,7 +31,7 @@
 // the 'static_spki_hashes' and 'bad_static_spki_hashes' fields in 'pinsets'
 // refer to, and the timestamp at which the pins list was last updated.
 //
-// Last updated: 2025-05-09 12:53 UTC
+// Last updated: 2025-05-11 12:55 UTC
 //
 {
   "pinsets": [
diff --git a/net/third_party/quiche/src b/net/third_party/quiche/src
index 41bba51..bbe16c1 160000
--- a/net/third_party/quiche/src
+++ b/net/third_party/quiche/src
@@ -1 +1 @@
-Subproject commit 41bba51b1ae0aa30ff7faf5ac5aa28feb1588afd
+Subproject commit bbe16c18d2fc0ea4bc261b20dbc28fe53e32bf13
diff --git a/remoting/remoting_options.gni b/remoting/remoting_options.gni
index df6aa27..8890e150 100644
--- a/remoting/remoting_options.gni
+++ b/remoting/remoting_options.gni
@@ -8,7 +8,8 @@
 
 remoting_use_x11 = ozone_platform_x11 && (is_linux && !is_castos)
 
-enable_remoting_client = is_chromeos || (is_linux && !is_castos)
+enable_remoting_client =
+    is_chromeos || (is_linux && !is_castos) || is_mac || is_win
 
 enable_remoting_host = is_win || is_mac || is_chromeos || remoting_use_x11
 
diff --git a/services/network/accept_ch_frame_interceptor.cc b/services/network/accept_ch_frame_interceptor.cc
index 03d5fa7a..6cbf99d1 100644
--- a/services/network/accept_ch_frame_interceptor.cc
+++ b/services/network/accept_ch_frame_interceptor.cc
@@ -80,6 +80,8 @@
   // Find client hints that are in the ACCEPT_CH frame that were not already
   // included in the request
   const auto hints = ComputeAcceptCHFrameHints(accept_ch_frame, headers);
+  base::UmaHistogramBoolean("Net.URLLoader.AcceptCH.RunObserverCall",
+                            !hints.empty());
   if (hints.empty()) {
     return net::OK;
   }
diff --git a/services/on_device_model/ml/chrome_ml_api.h b/services/on_device_model/ml/chrome_ml_api.h
index 1e577ac..0b414347 100644
--- a/services/on_device_model/ml/chrome_ml_api.h
+++ b/services/on_device_model/ml/chrome_ml_api.h
@@ -65,6 +65,10 @@
   // Matching `file_id` tells the backend that the data also matches.
   std::optional<uint32_t> file_id;
 
+  // File holding the weight cache. The file will be owned by the inference
+  // library and closed upon model destruction.
+  PlatformFile cache_file;
+
   // Null-terminated model path pointing to the model to use. Only kApuBackend
   // provides this field. Other backends provide model through the
   // `weights_file` field.
diff --git a/services/on_device_model/ml/chrome_ml_types.h b/services/on_device_model/ml/chrome_ml_types.h
index 2653f81..591b9e7e 100644
--- a/services/on_device_model/ml/chrome_ml_types.h
+++ b/services/on_device_model/ml/chrome_ml_types.h
@@ -14,6 +14,9 @@
 
 namespace ml {
 
+inline constexpr uint32_t kMinTopK = 1;
+inline constexpr float kMinTemperature = 0.0f;
+
 enum class Token {
   // Prefix for system text.
   kSystem,
diff --git a/services/on_device_model/ml/on_device_model_executor.cc b/services/on_device_model/ml/on_device_model_executor.cc
index 421beb7..65574e97 100644
--- a/services/on_device_model/ml/on_device_model_executor.cc
+++ b/services/on_device_model/ml/on_device_model_executor.cc
@@ -541,6 +541,11 @@
     data.model_path = weights_path_str.data();
     data.sentencepiece_model_path = sp_model_path_str.data();
   }
+  // TODO(crbug.com/400998489): Cache files are experimental for now.
+  if (optimization_guide::features::ForceCpuBackendForOnDeviceModel() &&
+      assets.cache.IsValid()) {
+    data.cache_file = assets.cache.TakePlatformFile();
+  }
   ChromeMLModelDescriptor descriptor = {
       .backend_type = params->backend_type,
       .model_data = &data,
diff --git a/services/on_device_model/ml/session_accessor.cc b/services/on_device_model/ml/session_accessor.cc
index 8704e5e..ee2148c7 100644
--- a/services/on_device_model/ml/session_accessor.cc
+++ b/services/on_device_model/ml/session_accessor.cc
@@ -7,19 +7,20 @@
 #include "base/compiler_specific.h"
 #include "components/optimization_guide/core/optimization_guide_features.h"
 #include "services/on_device_model/ml/chrome_ml.h"
+#include "services/on_device_model/ml/chrome_ml_types.h"
 
 namespace ml {
 
 namespace {
 
 float GetTemperature(std::optional<float> temperature) {
-  return std::max(0.0f, temperature.value_or(0.0f));
+  return std::max(kMinTemperature, temperature.value_or(kMinTemperature));
 }
 
 uint32_t GetTopK(std::optional<uint32_t> top_k) {
   return std::min(static_cast<uint32_t>(
                       optimization_guide::features::GetOnDeviceModelMaxTopK()),
-                  std::max(1u, top_k.value_or(1)));
+                  std::max(kMinTopK, top_k.value_or(kMinTopK)));
 }
 
 }  // namespace
diff --git a/services/on_device_model/public/cpp/model_assets.cc b/services/on_device_model/public/cpp/model_assets.cc
index eb3ed72c..ace32c1 100644
--- a/services/on_device_model/public/cpp/model_assets.cc
+++ b/services/on_device_model/public/cpp/model_assets.cc
@@ -44,10 +44,14 @@
 #if BUILDFLAG(IS_FUCHSIA)
 constexpr uint32_t kWeightsFlags =
     base::File::FLAG_OPEN | base::File::FLAG_READ | base::File::FLAG_WRITE;
+constexpr uint32_t kCacheFlags = kWeightsFlags;
 #else
 constexpr uint32_t kWeightsFlags =
     base::File::FLAG_OPEN | base::File::FLAG_READ | base::File::FLAG_ASYNC |
     base::File::FLAG_WIN_SEQUENTIAL_SCAN;
+constexpr uint32_t kCacheFlags = base::File::FLAG_OPEN | base::File::FLAG_READ |
+                                 base::File::FLAG_ASYNC |
+                                 base::File::FLAG_WRITE;
 #endif
 
 // Attempts to make sure `file` will be read from disk quickly when needed.
@@ -135,9 +139,17 @@
 
 ModelAssets::ModelAssets(mojo::DefaultConstruct::Tag tag) : weights(tag) {}
 
-ModelAssets::ModelAssets(const ModelAssets& other) = default;
+ModelAssets::ModelAssets(const ModelAssets& other)
+    : weights(other.weights),
+      sp_model_path(other.sp_model_path),
+      cache(other.cache.Duplicate()) {}
 
-ModelAssets& ModelAssets::operator=(const ModelAssets& other) = default;
+ModelAssets& ModelAssets::operator=(const ModelAssets& other) {
+  weights = other.weights;
+  sp_model_path = other.sp_model_path;
+  cache = other.cache.Duplicate();
+  return *this;
+}
 
 ModelAssets::ModelAssets(ModelAssets&&) = default;
 ModelAssets& ModelAssets::operator=(ModelAssets&&) = default;
@@ -148,13 +160,19 @@
     PrefetchFile(paths.weights);
   }
 
-  if (paths.weights.empty() ||
-      base::FeatureList::IsEnabled(
-          kForceLoadOnDeviceModelFromFilePathForTesting)) {
-    return ModelAssets::FromPath(std::move(paths.weights));
+  auto assets =
+      paths.weights.empty() ||
+              base::FeatureList::IsEnabled(
+                  kForceLoadOnDeviceModelFromFilePathForTesting)
+          ? ModelAssets::FromPath(std::move(paths.weights))
+          : ModelAssets::FromFile(base::File(paths.weights, kWeightsFlags));
+
+  if (!paths.cache.empty()) {
+    PrefetchFile(paths.cache);
+    assets.cache = base::File(paths.cache, kCacheFlags);
   }
 
-  return ModelAssets::FromFile(base::File(paths.weights, kWeightsFlags));
+  return assets;
 }
 
 AdaptationAssetPaths::AdaptationAssetPaths() = default;
diff --git a/services/on_device_model/public/cpp/model_assets.h b/services/on_device_model/public/cpp/model_assets.h
index 71f2788..93c2dcf 100644
--- a/services/on_device_model/public/cpp/model_assets.h
+++ b/services/on_device_model/public/cpp/model_assets.h
@@ -25,6 +25,7 @@
 
   base::FilePath weights;
   base::FilePath sp_model;
+  base::FilePath cache;
 };
 
 class COMPONENT_EXPORT(ON_DEVICE_MODEL_CPP) ModelFile {
@@ -70,6 +71,7 @@
 
   ModelFile weights;
   base::FilePath sp_model_path;
+  base::File cache;
 };
 
 // Helper to open files for ModelAssets given their containing paths.
diff --git a/services/on_device_model/public/cpp/model_assets_mojom_traits.cc b/services/on_device_model/public/cpp/model_assets_mojom_traits.cc
index 48537d9f..1446483 100644
--- a/services/on_device_model/public/cpp/model_assets_mojom_traits.cc
+++ b/services/on_device_model/public/cpp/model_assets_mojom_traits.cc
@@ -7,6 +7,7 @@
 #include <optional>
 
 #include "base/files/file_path.h"
+#include "mojo/public/cpp/base/file_mojom_traits.h"
 #include "mojo/public/cpp/base/file_path_mojom_traits.h"
 #include "services/on_device_model/public/cpp/model_assets.h"
 #include "services/on_device_model/public/mojom/on_device_model_service.mojom-shared.h"
@@ -22,7 +23,8 @@
   // optional.
   std::optional<base::FilePath> sp_model_path;
   bool ok = data.ReadWeights(&assets->weights) &&
-            data.ReadSpModelPath(&sp_model_path);
+            data.ReadSpModelPath(&sp_model_path) &&
+            data.ReadCache(&assets->cache);
   if (!ok) {
     return false;
   }
diff --git a/services/on_device_model/public/cpp/model_assets_mojom_traits.h b/services/on_device_model/public/cpp/model_assets_mojom_traits.h
index 2565feb0..ed33195 100644
--- a/services/on_device_model/public/cpp/model_assets_mojom_traits.h
+++ b/services/on_device_model/public/cpp/model_assets_mojom_traits.h
@@ -29,6 +29,10 @@
     return std::move(assets.sp_model_path);
   }
 
+  static base::File cache(on_device_model::ModelAssets& assets) {
+    return std::move(assets.cache);
+  }
+
   static bool Read(on_device_model::mojom::ModelAssetsDataView data,
                    on_device_model::ModelAssets* assets);
 };
diff --git a/services/on_device_model/public/cpp/service_client.cc b/services/on_device_model/public/cpp/service_client.cc
index a9d2245..2295cbe 100644
--- a/services/on_device_model/public/cpp/service_client.cc
+++ b/services/on_device_model/public/cpp/service_client.cc
@@ -56,7 +56,7 @@
 
 void ServiceClient::RemovePendingUsage() {
   pending_uses_--;
-  if (pending_uses_ == 0) {
+  if (pending_uses_ == 0 && remote_) {
     remote_.reset_on_idle_timeout(base::TimeDelta());
   }
 }
diff --git a/services/on_device_model/public/cpp/test_support/BUILD.gn b/services/on_device_model/public/cpp/test_support/BUILD.gn
index 5ac48e6..03d8a48 100644
--- a/services/on_device_model/public/cpp/test_support/BUILD.gn
+++ b/services/on_device_model/public/cpp/test_support/BUILD.gn
@@ -14,6 +14,7 @@
   public_deps = [
     "//base",
     "//mojo/public/cpp/bindings",
+    "//services/on_device_model/ml:api",
     "//services/on_device_model/public/cpp",
     "//services/on_device_model/public/mojom",
     "//third_party/re2",
diff --git a/services/on_device_model/public/cpp/test_support/fake_service.cc b/services/on_device_model/public/cpp/test_support/fake_service.cc
index 7b2ebeb..dab87f8 100644
--- a/services/on_device_model/public/cpp/test_support/fake_service.cc
+++ b/services/on_device_model/public/cpp/test_support/fake_service.cc
@@ -110,8 +110,8 @@
 
 FakeOnDeviceSession::FakeOnDeviceSession(FakeOnDeviceServiceSettings* settings,
                                          FakeOnDeviceModel* model,
-                                         const Capabilities& capabilities)
-    : settings_(settings), model_(model), capabilities_(capabilities) {}
+                                         mojom::SessionParamsPtr params)
+    : settings_(settings), model_(model), params_(std::move(params)) {}
 
 FakeOnDeviceSession::~FakeOnDeviceSession() = default;
 
@@ -146,7 +146,8 @@
 
 void FakeOnDeviceSession::GetSizeInTokens(mojom::InputPtr input,
                                           GetSizeInTokensCallback callback) {
-  std::move(callback).Run(0);
+  std::move(callback).Run(
+      OnDeviceInputToString(*input, params_->capabilities).size());
 }
 
 void FakeOnDeviceSession::Score(const std::string& text,
@@ -194,6 +195,11 @@
         "Adaptation model: " + model_->data().adaptation_model_weight + "\n";
     remote->OnResponse(std::move(chunk));
   }
+  if (!model_->data().cache_weight.empty()) {
+    auto chunk = mojom::ResponseChunk::New();
+    chunk->text = "Cache weight: " + model_->data().cache_weight + "\n";
+    remote->OnResponse(std::move(chunk));
+  }
 
   if (priority_ == on_device_model::mojom::Priority::kBackground) {
     auto chunk = mojom::ResponseChunk::New();
@@ -217,12 +223,20 @@
   int output_token_count = 0;
   if (settings_->model_execute_result.empty()) {
     for (const auto& context : context_) {
-      std::string text = CtxToString(*context, capabilities_);
+      std::string text = CtxToString(*context, params_->capabilities);
       output_token_count += text.size();
       auto chunk = mojom::ResponseChunk::New();
       chunk->text = "Context: " + text + "\n";
       remote->OnResponse(std::move(chunk));
     }
+    if (params_->top_k != ml::kMinTopK ||
+        params_->temperature != ml::kMinTemperature) {
+      auto chunk = mojom::ResponseChunk::New();
+      chunk->text += "TopK: " + base::NumberToString(params_->top_k) +
+                     ", Temp: " + base::NumberToString(params_->temperature) +
+                     "\n";
+      remote->OnResponse(std::move(chunk));
+    }
   } else {
     for (const auto& text : settings_->model_execute_result) {
       output_token_count += text.size();
@@ -244,7 +258,7 @@
     return;
   }
   uint32_t input_tokens = static_cast<uint32_t>(
-      OnDeviceInputToString(*options->input, capabilities_).size());
+      OnDeviceInputToString(*options->input, params_->capabilities).size());
   uint32_t max_tokens =
       options->max_tokens > 0 ? options->max_tokens : input_tokens;
   uint32_t tokens_processed = std::min(input_tokens, max_tokens);
@@ -257,7 +271,7 @@
 void FakeOnDeviceSession::CloneImpl(
     mojo::PendingReceiver<on_device_model::mojom::Session> session) {
   auto new_session =
-      std::make_unique<FakeOnDeviceSession>(settings_, model_, capabilities_);
+      std::make_unique<FakeOnDeviceSession>(settings_, model_, params_.Clone());
   for (const auto& c : context_) {
     new_session->context_.push_back(c->Clone());
   }
@@ -277,12 +291,11 @@
 void FakeOnDeviceModel::StartSession(
     mojo::PendingReceiver<mojom::Session> session,
     mojom::SessionParamsPtr params) {
-  Capabilities capabilities;
-  if (params) {
-    capabilities = params->capabilities;
+  if (!params) {
+    params = mojom::SessionParams::New();
   }
   AddSession(std::move(session), std::make_unique<FakeOnDeviceSession>(
-                                     settings_, this, capabilities));
+                                     settings_, this, std::move(params)));
 }
 
 void FakeOnDeviceModel::AddSession(
@@ -389,6 +402,9 @@
   }
   FakeOnDeviceModel::Data data;
   data.base_weight = ReadFile(params->assets.weights.file());
+  if (params->assets.cache.IsValid()) {
+    data.cache_weight = ReadFile(params->assets.cache);
+  }
   auto test_model = std::make_unique<FakeOnDeviceModel>(
       settings_, std::move(data), params->performance_hint);
   model_receivers_.Add(std::move(test_model), std::move(model));
diff --git a/services/on_device_model/public/cpp/test_support/fake_service.h b/services/on_device_model/public/cpp/test_support/fake_service.h
index c797250f..7e23dc06 100644
--- a/services/on_device_model/public/cpp/test_support/fake_service.h
+++ b/services/on_device_model/public/cpp/test_support/fake_service.h
@@ -73,7 +73,7 @@
  public:
   explicit FakeOnDeviceSession(FakeOnDeviceServiceSettings* settings,
                                FakeOnDeviceModel* model,
-                               const Capabilities& capabilities);
+                               mojom::SessionParamsPtr params);
   ~FakeOnDeviceSession() override;
 
   // mojom::Session:
@@ -110,7 +110,7 @@
   std::string adaptation_model_weight_;
   std::vector<mojom::AppendOptionsPtr> context_;
   raw_ptr<FakeOnDeviceModel> model_;
-  Capabilities capabilities_;
+  mojom::SessionParamsPtr params_;
   on_device_model::mojom::Priority priority_ =
       on_device_model::mojom::Priority::kForeground;
 
@@ -122,6 +122,7 @@
   struct Data {
     std::string base_weight = "";
     std::string adaptation_model_weight = "";
+    std::string cache_weight = "";
   };
   explicit FakeOnDeviceModel(FakeOnDeviceServiceSettings* settings,
                              Data&& data,
diff --git a/services/on_device_model/public/mojom/on_device_model_service.mojom b/services/on_device_model/public/mojom/on_device_model_service.mojom
index 7758a2ef..02dd0b5 100644
--- a/services/on_device_model/public/mojom/on_device_model_service.mojom
+++ b/services/on_device_model/public/mojom/on_device_model_service.mojom
@@ -15,7 +15,7 @@
 union ModelFile {
   // This should be used with the GPU backend.
   //
-  // TODO(b/313919363): This should be a ReadOnlyFile.
+  // TODO(crbug.com/313919363): This should be a ReadOnlyFile.
   mojo_base.mojom.File file;
 
   // This should be used with the APU backend.
@@ -29,6 +29,11 @@
 
   // Currently the only usage of sp model will be passed by file path.
   mojo_base.mojom.FilePath? sp_model_path;
+
+  // File which may be used to store a backend-specific cache of the model
+  // weights or to load previously-cached model weights from. This file must be
+  // writable to allow the backend to write out its custom data.
+  mojo_base.mojom.File? cache;
 };
 
 // Type of the backend to run the model.
diff --git a/styleguide/web/web.md b/styleguide/web/web.md
index 19c4b25..fb15cfa54 100644
--- a/styleguide/web/web.md
+++ b/styleguide/web/web.md
@@ -322,7 +322,7 @@
 
 * Prefer `event.preventDefault()` to `return false` from event handlers.
 
-* Prefer `this.addEventListener('foo-changed', this.onFooChanged_.bind(this));`
+* Prefer `this.addEventListener('foo-changed', this.onFooChanged.bind(this));`
   instead of always using an arrow function wrapper, when it makes the code less
   verbose without compromising type safety (for example in TypeScript files).
 
@@ -517,7 +517,7 @@
   property computation methods, and in element instance methods called from
   HTML.
 
-  The signature of the `computeBar_()` function in the TS file does not matter,
+  The signature of the `computeBar()` function in the TS file does not matter,
   so omit parameters there, as they would be unused. What matters is for the
   call site to declare the right properties as dependencies, so that the
   binding correctly triggers whenever it changes.
@@ -526,11 +526,11 @@
   static get properties() {
     return {
       foo: {type: Number, value: 42},
-      bar: {type: Boolean, computed: 'computeBar_(foo)'},
+      bar: {type: Boolean, computed: 'computeBar(foo)'},
     };
   }
 
-  private computeBar_(): boolean {
+  private computeBar(): boolean {
     return this.derive(this.foo);
   }
   ```
diff --git a/testing/buildbot/filters/accessibility-linux.browser_tests.filter b/testing/buildbot/filters/accessibility-linux.browser_tests.filter
index b7f6069..94c37fe 100644
--- a/testing/buildbot/filters/accessibility-linux.browser_tests.filter
+++ b/testing/buildbot/filters/accessibility-linux.browser_tests.filter
@@ -1,239 +1,145 @@
-# These tests are run on the main builders and don't need to also be
-# run here.
+# Tests that are equally flaky on other bots (either globally, or on Linux bots),
+# which we want to skip to reduce noise from failures not related to accessibility.
+# Periodically check to see if they are still flaky via the links provided.
+# https://ci.chromium.org/ui/test/chromium/ninja%3A%2F%2Fchrome%2Ftest%3Abrowser_tests%2FAdsPageLoadMetricsObserverResourceBrowserTest.ReceivedPrivacyPermissionsUseCounters%2FAll.ReduceTransferSizeUpdatedIPCDisabled
+-All/AdsPageLoadMetricsObserverResourceBrowserTest.ReceivedPrivacyPermissionsUseCounters/ReduceTransferSizeUpdatedIPCDisabled
+# Flaky on Linux: https://ci.chromium.org/ui/test/chromium/ninja%3A%2F%2Fchrome%2Ftest%3Abrowser_tests%2FCookieSettingsTest.BasicCookies%2FAll.4
+-All/CookieSettingsTest.BasicCookies/4
+# Flaky on Linux: https://ci.chromium.org/ui/test/chromium/ninja%3A%2F%2Fchrome%2Ftest%3Abrowser_tests%2FCookieSettingsTest.BasicCookiesHttps%2FAll.5
+-All/CookieSettingsTest.BasicCookiesHttps/5
+# https://ci.chromium.org/ui/test/chromium/ninja%3A%2F%2Fchrome%2Ftest%3Abrowser_tests%2FOmniboxSearchAggregatorHTTPErrorTest.HTTPErrorResponse%2FAll.403
+-All/OmniboxSearchAggregatorHTTPErrorTest.HTTPErrorResponse/403
+# https://ci.chromium.org/ui/test/chromium/ninja%3A%2F%2Fchrome%2Ftest%3Abrowser_tests%2FOmniboxSearchAggregatorHTTPErrorTest.HTTPErrorResponse%2FAll.405
+-All/OmniboxSearchAggregatorHTTPErrorTest.HTTPErrorResponse/405
+# https://ci.chromium.org/ui/test/chromium/ninja%3A%2F%2Fchrome%2Ftest%3Abrowser_tests%2FOmniboxSearchAggregatorHTTPErrorTest.HTTPErrorResponse%2FAll.502
+-All/OmniboxSearchAggregatorHTTPErrorTest.HTTPErrorResponse/502
+# Flaky on Linux: https://ci.chromium.org/ui/test/chromium/ninja%3A%2F%2Fchrome%2Ftest%3Abrowser_tests%2FPdfDownloadTestSplitCacheEnabled.OpenDownloadInMostRecentBrowser%2FAll.PdfOopifEnabled_TriplePlusCrossSiteMainFrameNavigationBool
+-All/PdfDownloadTestSplitCacheEnabled.OpenDownloadInMostRecentBrowser/PdfOopifEnabled_TriplePlusCrossSiteMainFrameNavigationBool
+# Flaky on Windows, occasionally on Linux: https://ci.chromium.org/ui/test/chromium/ninja%3A%2F%2Fchrome%2Ftest%3Abrowser_tests%2FSearchEnginePreconnectorEnabledOnlyBrowserTest.AllowedSearch%2FAll.1
+-All/SearchEnginePreconnectorEnabledOnlyBrowserTest.AllowedSearch/1
+# Occasionally flaky on Linux: https://ci.chromium.org/ui/test/chromium/ninja%3A%2F%2Fchrome%2Ftest%3Abrowser_tests%2FSearchEnginePreconnectorWithPreconnect2FeatureBrowserTest.PreconnectSearchAfterOnFailure%2FAll.0
+-All/SearchEnginePreconnectorWithPreconnect2FeatureBrowserTest.PreconnectSearchAfterOnFailure/0
+# https://ci.chromium.org/ui/test/chromium/ninja%3A%2F%2Fchrome%2Ftest%3Abrowser_tests%2FAutocompleteTest.SubmitSimpleValue_OTR_DoesNotSave
+-AutocompleteTest.SubmitSimpleValue_OTR_DoesNotSave
+# https://ci.chromium.org/ui/test/chromium/ninja%3A%2F%2Fchrome%2Ftest%3Abrowser_tests%2FBookmarksTest.List
+-BookmarksTest.List
+# https://ci.chromium.org/ui/test/chromium/ninja%3A%2F%2Fchrome%2Ftest%3Abrowser_tests%2FBookmarksTest.List___bookmarks_list__resets_focused_index_if_out_of_bounds
+-BookmarksTest.List___bookmarks_list__resets_focused_index_if_out_of_bounds
+# https://ci.chromium.org/ui/test/chromium/ninja%3A%2F%2Fchrome%2Ftest%3Abrowser_tests%2FBrowserNavigatorTest.SaveAfterFocusTabSwitchTest
+-BrowserNavigatorTest.SaveAfterFocusTabSwitchTest
+# https://ci.chromium.org/ui/test/chromium/ninja%3A%2F%2Fchrome%2Ftest%3Abrowser_tests%2FClickToCallBrowserTest.ContextMenu_TelLink_Histograms
+-ClickToCallBrowserTest.ContextMenu_TelLink_Histograms
+# https://ci.chromium.org/ui/test/chromium/ninja%3A%2F%2Fchrome%2Ftest%3Abrowser_tests%2FCrComponentsSearchboxTest.RealboxTest
+-CrComponentsSearchboxTest.RealboxTest
+# https://ci.chromium.org/ui/test/chromium/ninja%3A%2F%2Fchrome%2Ftest%3Abrowser_tests%2FCrComponentsSearchboxTest.RealboxTest__NewTabPageRealboxTest_arrow_up_down_moves_selection___focus
+-CrComponentsSearchboxTest.RealboxTest__NewTabPageRealboxTest_arrow_up_down_moves_selection___focus
+# https://ci.chromium.org/ui/test/chromium/ninja%3A%2F%2Fchrome%2Ftest%3Abrowser_tests%2FCrElementsTest.CrToast
+-CrElementsTest.CrToast
+# https://ci.chromium.org/ui/test/chromium/ninja%3A%2F%2Fchrome%2Ftest%3Abrowser_tests%2FCrElementsTest.CrToast__cr_toast_stop_and_restart_auto_hide_depending_when_toast_focus_changes
+-CrElementsTest.CrToast__cr_toast_stop_and_restart_auto_hide_depending_when_toast_focus_changes
+# https://ci.chromium.org/ui/test/chromium/ninja%3A%2F%2Fchrome%2Ftest%3Abrowser_tests%2FDocumentPictureInPictureWindowControllerBrowserTest.CreateTwice
+-DocumentPictureInPictureWindowControllerBrowserTest.CreateTwice
+# Flaky on Linux: https://ci.chromium.org/ui/test/chromium/ninja%3A%2F%2Fchrome%2Ftest%3Abrowser_tests%2FDownloadToolbarUIControllerBrowserTest.DialogAutoCloses
+-DownloadToolbarUIControllerBrowserTest.DialogAutoCloses
+# https://ci.chromium.org/ui/test/chromium/ninja%3A%2F%2Fchrome%2Ftest%3Abrowser_tests%2FDownloadToolbarUIControllerBrowserTest.OpenPrimaryDialog
+-DownloadToolbarUIControllerBrowserTest.OpenPrimaryDialog
+# https://ci.chromium.org/ui/test/chromium/ninja%3A%2F%2Fchrome%2Ftest%3Abrowser_tests%2FFlagsUiBrowserTest.App
+-FlagsUiBrowserTest.App
+# https://ci.chromium.org/ui/test/chromium/ninja%3A%2F%2Fchrome%2Ftest%3Abrowser_tests%2FHistoryProductSpecificationsListTest.Load
+-HistoryProductSpecificationsListTest.Load
+# https://ci.chromium.org/ui/test/chromium/ninja%3A%2F%2Fchrome%2Ftest%3Abrowser_tests%2FHistoryProductSpecificationsListTest.Load__ProductSpecificationsListTest_focus_with_arrow_keys
+-HistoryProductSpecificationsListTest.Load__ProductSpecificationsListTest_focus_with_arrow_keys
+# https://ci.chromium.org/ui/test/chromium/ninja%3A%2F%2Fchrome%2Ftest%3Abrowser_tests%2FInlineLoginHelperBrowserTest.UntrustedSigninDialogCancel
+-InlineLoginHelperBrowserTest.UntrustedSigninDialogCancel
+# https://ci.chromium.org/ui/test/chromium/ninja%3A%2F%2Fchrome%2Ftest%3Abrowser_tests%2FLensOverlayControllerContextualFeaturesDisabledTest.PreselectionToastOmniboxFocusState
+-LensOverlayControllerContextualFeaturesDisabledTest.PreselectionToastOmniboxFocusState
+# https://ci.chromium.org/ui/test/chromium/ninja%3A%2F%2Fchrome%2Ftest%3Abrowser_tests%2FPolicyTest.DefaultCookiesSetting
+-PolicyTest.DefaultCookiesSetting
+# https://ci.chromium.org/ui/test/chromium/ninja%3A%2F%2Fchrome%2Ftest%3Abrowser_tests%2FPopupTrackerBrowserTest.PopupRedirectsTwice_RedirectCountTwo
+-PopupTrackerBrowserTest.PopupRedirectsTwice_RedirectCountTwo
+# https://ci.chromium.org/ui/test/chromium/ninja%3A%2F%2Fchrome%2Ftest%3Abrowser_tests%2FPreviewBrowserTest.ErrorOnRedirectionToNonHttpsUrl
+-PreviewBrowserTest.ErrorOnRedirectionToNonHttpsUrl
+# https://ci.chromium.org/ui/test/chromium/ninja%3A%2F%2Fchrome%2Ftest%3Abrowser_tests%2FPreviewBrowserTest.MojoCapabilityControl
+-PreviewBrowserTest.MojoCapabilityControl
+# https://ci.chromium.org/ui/test/chromium/ninja%3A%2F%2Fchrome%2Ftest%3Abrowser_tests%2FPreviewBrowserTest.TrivialSessionHistory
+-PreviewBrowserTest.TrivialSessionHistory
+# https://ci.chromium.org/ui/test/chromium/ninja%3A%2F%2Fchrome%2Ftest%3Abrowser_tests%2FPreviewBrowserTest.ErrorOnNonHttpsUrl
+-PreviewBrowserTest.ErrorOnNonHttpsUrl
+# https://ci.chromium.org/ui/test/chromium/ninja%3A%2F%2Fchrome%2Ftest%3Abrowser_tests%2FPreviewPageLoadMetricsObserverBrowserTest.LinkPreviewUsageUsedButNotPromoted
+-PreviewPageLoadMetricsObserverBrowserTest.LinkPreviewUsageUsedButNotPromoted
+# https://ci.chromium.org/ui/test/chromium/ninja%3A%2F%2Fchrome%2Ftest%3Abrowser_tests%2FPreviewPageLoadMetricsObserverBrowserTest.PreviewFinalStatus
+-PreviewPageLoadMetricsObserverBrowserTest.PreviewFinalStatus
+# https://ci.chromium.org/ui/test/chromium/ninja%3A%2F%2Fchrome%2Ftest%3Abrowser_tests%2FProductSpecificationsTest.ComparisonTableListItem
+-ProductSpecificationsTest.ComparisonTableListItem
+# https://ci.chromium.org/ui/test/chromium/ninja%3A%2F%2Fchrome%2Ftest%3Abrowser_tests%2FProductSpecificationsTest.ComparisonTableListItem__ComparisonTableListItemTest_context_menu_rename_option_displays_input__and_submitting_emits_event_with_UUID_and_name
+-ProductSpecificationsTest.ComparisonTableListItem__ComparisonTableListItemTest_context_menu_rename_option_displays_input__and_submitting_emits_event_with_UUID_and_name
+# https://ci.chromium.org/ui/test/chromium/ninja%3A%2F%2Fchrome%2Ftest%3Abrowser_tests%2FProductSpecificationsTest.Header
+-ProductSpecificationsTest.Header
+# Flaky on Linux: https://ci.chromium.org/ui/test/chromium/ninja%3A%2F%2Fchrome%2Ftest%3Abrowser_tests%2FProductSpecificationsTest.Header
+-ProfileMenuViewPixelTest.InvokeUi_default/SignedOut_MultipleProfiles_DarkTheme
+# https://ci.chromium.org/ui/test/chromium/ninja%3A%2F%2Fchrome%2Ftest%3Abrowser_tests%2FRecentActivityBubbleDialogViewActionBrowserTest.HandlesActionFocusTab
+-RecentActivityBubbleDialogViewActionBrowserTest.HandlesActionFocusTab
+# https://ci.chromium.org/ui/test/chromium/ninja%3A%2F%2Fchrome%2Ftest%3Abrowser_tests%2FRecentActivityBubbleDialogViewActionBrowserTest.HandlesActionOpenTabGroupEditDialog
+-RecentActivityBubbleDialogViewActionBrowserTest.HandlesActionOpenTabGroupEditDialog
+# Flaky on Linux: https://ci.chromium.org/ui/test/chromium/ninja%3A%2F%2Fchrome%2Ftest%3Abrowser_tests%2FSaveCardBubbleViewsFullFormBrowserTest.Local_ClickingNoThanksClosesBubble
+-SaveCardBubbleViewsFullFormBrowserTest.Local_ClickingNoThanksClosesBubble
+# Flaky on Linux: https://ci.chromium.org/ui/test/chromium/ninja%3A%2F%2Fchrome%2Ftest%3Abrowser_tests%2FSaveCardBubbleViewsFullFormBrowserTest.Local_ClickingSave_ClosesBubble
+-SaveCardBubbleViewsFullFormBrowserTest.Local_ClickingSave_ClosesBubble
+# https://ci.chromium.org/ui/test/chromium/ninja%3A%2F%2Fchrome%2Ftest%3Abrowser_tests%2FDocumentPictureInPictureWindowControllerBrowserTest.CreateTwice
+-SaveCardBubbleViewsFullFormBrowserTest.Local_ManageCardsDoneButtonClosesBubble
+# https://ci.chromium.org/ui/test/chromium/ninja%3A%2F%2Fchrome%2Ftest%3Abrowser_tests%2FSearchPrefetchServiceEnabledBrowserTest.HungRequestCanBeServed
+-SearchPrefetchServiceEnabledBrowserTest.HungRequestCanBeServed
+# https://ci.chromium.org/ui/test/chromium/ninja%3A%2F%2Fchrome%2Ftest%3Abrowser_tests%2FSettingsTest.PaymentsSectionCardDialogs
+-SettingsTest.PaymentsSectionCardDialogs
+# https://ci.chromium.org/ui/test/chromium/ninja%3A%2F%2Fchrome%2Ftest%3Abrowser_tests%2FSettingsTest.PaymentsSectionCardDialogs__PaymentsSectionCardDialogs_verifyOnlyValidCardNumbersAllowed_InvalidCasesWithNoError
+-SettingsTest.PaymentsSectionCardDialogs__PaymentsSectionCardDialogs_verifyOnlyValidCardNumbersAllowed_InvalidCasesWithNoError
+# https://ci.chromium.org/ui/test/chromium/ninja%3A%2F%2Fchrome%2Ftest%3Abrowser_tests%2FSettingsTest.PaymentsSectionIban
+-SettingsTest.PaymentsSectionIban
+# https://ci.chromium.org/ui/test/chromium/ninja%3A%2F%2Fchrome%2Ftest%3Abrowser_tests%2FSettingsTest.PaymentsSectionIban__PaymentsSectionIban_verifyIBANErrorMessage
+-SettingsTest.PaymentsSectionIban__PaymentsSectionIban_verifyIBANErrorMessage
+# Flaky on Linux: https://ci.chromium.org/ui/test/chromium/ninja%3A%2F%2Fchrome%2Ftest%3Abrowser_tests%2FSystemNetworkContextManagerWithFirstPartySetComponentBrowserTest.ReloadsFirstPartySetsAfterCrash
+-SystemNetworkContextManagerWithFirstPartySetComponentBrowserTest.ReloadsFirstPartySetsAfterCrash
+# https://ci.chromium.org/ui/test/chromium/ninja%3A%2F%2Fchrome%2Ftest%3Abrowser_tests%2FTabStripBrowsertest.TabGroupTabNavigationAccelerators
+-TabStripBrowsertest.TabGroupTabNavigationAccelerators
+# https://ci.chromium.org/ui/test/chromium/ninja%3A%2F%2Fchrome%2Ftest%3Abrowser_tests%2FTabRestoreTest.RestoreGroupWithUnloadHandlerRejected
+-TabRestoreTest.RestoreGroupWithUnloadHandlerRejected
+# https://ci.chromium.org/ui/test/chromium/ninja%3A%2F%2Fchrome%2Ftest%3Abrowser_tests%2FWebAuthenticationProxyApiTest.RemoteSessionStateChange
+-WebAuthenticationProxyApiTest.RemoteSessionStateChange
+# https://ci.chromium.org/ui/test/chromium/ninja%3A%2F%2Fchrome%2Ftest%3Abrowser_tests%2FWebBluetoothTest.NavigateWithChooserCrossOrigin
+-WebBluetoothTest.NavigateWithChooserCrossOrigin
+# https://ci.chromium.org/ui/test/chromium/ninja%3A%2F%2Fchrome%2Ftest%3Abrowser_tests%2FWebUIWebViewCoverageDisabledBrowserTest.AddContentScriptsWithNewWindowAPI
+-WebUIWebViewCoverageDisabledBrowserTest.AddContentScriptsWithNewWindowAPI
+# https://ci.chromium.org/ui/test/chromium/ninja%3A%2F%2Fchrome%2Ftest%3Abrowser_tests%2FWebViewUsbTest.Shim_TestCannotRequestUsb%2FMPArch
+-WebViewUsbTest.Shim_TestCannotRequestUsb/MPArch
+# https://ci.chromium.org/ui/test/chromium/ninja%3A%2F%2Fchrome%2Ftest%3Abrowser_tests%2FWebViewTest.Shim_TestTerminateAfterExit%2FInnerWebContents
+-WebViewTest.Shim_TestTerminateAfterExit/InnerWebContents
+
+# These tests are run on the main builders and don't need to also be run here.
 -AccessibilityLabelsMenuObserverTest.*
+-All/DumpAccessibility*
 -AutomationApiTest.*
 
-# These tests require that accessibility is disabled at startup, which is the
-# opposite of the situation when --force-renderer-accessibility is used.
+# These tests require that accessibility is disabled at startup, or that there
+# is some other AXMode during the test, which will not be the case when
+# --force-renderer-accessibility is used.
 -AccessibilityLabelsBrowserTest.NotEnabledWithoutScreenReader
 -All/PdfOcrControllerBrowserTest.NotEnabledWithoutScreenReader/0
 -AXMainNodeAnnotatorControllerBrowserTest.NotEnabledWithoutScreenReader
-
-# These tests still need to be triaged and either okayed to skip or fixed.
--AdsInterventionManagerTestWithEnforcement.AdsInterventionEnforced_PageActivated
--AdsInterventionManagerTestWithEnforcement.MultipleAdsInterventions_PageActivationClearedAfterFirst
--AdsPageLoadMetricsObserverBrowserTest.DisallowedAdFrames_NotMeasured
--AdsPageLoadMetricsObserverResourceBrowserTest.HeavyAdInterventionBlocklistFull_InterventionBlocked
--All/OtherFrameNavigationDownloadBrowserTest_AdFrame.Download/12
--All/OtherFrameNavigationDownloadBrowserTest_AdFrame.Download/4
--All/DumpAccessibility*
--All/PermissionPromptBubbleBaseViewQuietUiBrowserTest.DispositionEnabledInPrefsTest/0
--All/PermissionPromptBubbleBaseViewQuietUiBrowserTest.DispositionEnabledInPrefsTest/1
--All/PermissionPromptBubbleBaseViewQuietUiBrowserTest.DispositionPredictedVeryUnlikelyGrantTest/0
--All/PermissionPromptBubbleBaseViewQuietUiBrowserTest.DispositionPredictedVeryUnlikelyGrantTest/1
--All/QuietUIPromoBrowserTest.InvokeUi_QuietUIPromo/0
--All/SaveCardBubbleViewsFullFormBrowserTest.Local_ClickingIconShowsManageCards/0
--All/SaveCardBubbleViewsFullFormBrowserTest.Local_ClickingIconShowsManageCards/1
--All/SaveCardBubbleViewsFullFormBrowserTest.Local_ManageCardsDoneButtonClosesBubble/0
--All/SaveCardBubbleViewsFullFormBrowserTest.Local_ManageCardsDoneButtonClosesBubble/1
--All/SaveCardBubbleViewsFullFormBrowserTestSettings.Local_ManageCardsButtonRedirects/0
--All/SaveCardBubbleViewsFullFormBrowserTestSettings.Local_ManageCardsButtonRedirects/1
--All/ScreenAIServiceRouterTest.*
--All/SoftNavigationTest.INP_ClickWithPresentation/*
--All/StartupBrowserCreatorPickerTest.TestSetup/0
--All/SubresourceFilterPopupBrowserTestWithParam.BlockCreatingNewWindows/0
--All/SubresourceFilterPopupBrowserTestWithParam.BlockCreatingNewWindows/1
--All/SubresourceFilterPopupBrowserTestWithParam.BlockOpenURLFromTab/0
--All/SubresourceFilterPopupBrowserTestWithParam.BlockOpenURLFromTab/1
--All/SubresourceFilterPopupBrowserTestWithParam.BlockOpenURLFromTabInIframe/0
--All/SubresourceFilterPopupBrowserTestWithParam.BlockOpenURLFromTabInIframe/1
--All/SubresourceFilterPopupBrowserTestWithParam.TraditionalWindowOpen_NotBlocked/1
--All/SubresourceFilterWebSocketBrowserTest.BlockWebSocket/0
--All/SubresourceFilterWebSocketBrowserTest.BlockWebSocket/1
--All/TabSharingUIViewsBrowserTest.CloseTabInIncognitoBrowser/0
--All/TabSharingUIViewsBrowserTest.CloseTabInIncognitoBrowser/1
--AutofillAcrossIframesTest*
--BackgroundFetchBrowserTest.FetchRejectedWithoutPermission
--BackgroundPage/ExtensionPreferenceApiTest.*
--BackForwardCachePageLoadMetricsObserverBrowserTest.CumulativeLayoutShiftAfterBackForwardCacheRestore
--BackForwardCachePageLoadMetricsObserverBrowserTest.LayoutShiftNormalization_AfterBackForwardCacheRestore
--BlockedAppApiTest.OpenAppFromIframe
--Browser/SubresourceFilterDnsAliasFilteringThrottleBrowserTest.CheckDnsAliasesFromBrowser/ActivationEnabled
--BrowserNavigatorTest.Disposition_IncompatibleWindow_NoExisting
--BrowserNavigatorTest.SingletonIncognitoLeak
--BrowserNavigatorTest.SwitchToTabIncognitoLeak
--CapabilityDelegationBrowserTest.SameOriginPaymentRequest
--ChromeMainTest.SecondLaunchFromIncognitoWithNormalUrl
--ChromeNavigationBrowserTest.WindowOpenBlockedAfterClickNavigation
--ChromeSitePerProcessTest.PostMessageSenderAndReceiverRaceToCreatePopup
--ChromeSitePerProcessTest.TwoPostMessagesToDifferentSitesWithSameUserGesture
--ChromeSitePerProcessTest.UserActivationVisibilityInAncestorFrame
--ChromeSitePerProcessTestWithVerifiedUserActivation.UserActivationBrowserVerificationSameOriginSite
--ChromeURLDataManagerWebUITrustedTypesTest.NoTrustedTypesViolation/chrome___prefs_internals
--ChromeURLDataManagerWebUITrustedTypesTest.TrustedTypesEnabled/chrome___prefs_internals
--ClickToCallBrowserTest.ContextMenu_EscapedCharacters
--ClickToCallBrowserTest.ContextMenu_HighlightedText_Histograms
--ClickToCallBrowserTest.ContextMenu_HighlightedText_MultipleDevicesAvailable
--ClickToCallBrowserTest.ContextMenu_TelLink_Histograms
--ClickToCallBrowserTest.ContextMenu_TelLink_MultipleDevicesAvailable
--ClickToCallBrowserTest.ContextMenu_TelLink_SingleDeviceAvailable
--ClickToCallBrowserTest.ContextMenu_UKM
--ClickToCallBrowserTest.LeftClick_ChooseDevice
--ContentSettingBubbleDialogTest.*
--ContentSettingBubbleModelPopupTest.PopupsActionsCount
--ContentSettingImageModelBrowserTest.CreateBubbleModel
--CookieControlsBubbleViewBrowserTest.PageReload
--CookiePolicyBrowserTest.MultiTabNestedTest
--CookiePolicyBrowserTest.MultiTabTest
--CookiePolicyStorageBrowserTest.NestedFirstPartyIFrameStorage/0
--CookiePolicyStorageBrowserTest.NestedFirstPartyIFrameStorage/1
--CookiePolicyStorageBrowserTest.NestedThirdPartyIFrameStorage/0
--CookiePolicyStorageBrowserTest.NestedThirdPartyIFrameStorage/1
--CookiePolicyStorageBrowserTest.ThirdPartyIFrameStorage/0
--CookiePolicyStorageBrowserTest.ThirdPartyIFrameStorage/1
--CorbAndCorsExtensionBrowserTest.FromForegroundPage_IncognitoSplitMode
--DohHttpsProtocolUpgradeBrowserTest.HttpsProtocolUpgrade
--DownloadExtensionBubbleEnabledTest.SetUiOptionsOffTheRecord
--DownloadExtensionTest.DownloadExtensionTest_Download_Incognito
--DownloadExtensionTest.DownloadExtensionTest_OnDeterminingFilename_IncognitoSplit
--DownloadFramePolicyBrowserTest.SubframeNavigationDownloadBlockedByLoadPolicy
--DownloadTest.BrowserCloseAfterDownload
--DownloadTest.DownloadRequestLimiterDisallowsAnchorDownloadTag
--DownloadTest.DownloadResourceThrottleCancels
--DownloadTest.MultipleDownloadsFromIframeSrcdoc
--EventPage/CookiesApiTest.CookiesEventsSpanning/0
--EventPage/CookiesApiTest.CookiesEventsSpanning/1
--ExtensionApiTest.FontSettingsIncognito
--ExtensionContentSettingsApiTest.IncognitoIsolation
--ExtensionIconSourceTest.IconsLoadedIncognito
--ExtensionWebRequestApiWebTransportTest.Main
--ExtensionOverrideTest.OverrideNewTabIncognito
--ExtensionTabUtilBrowserTest.OpenSplitModeExtensionOptionsPageIncognito
--ExtensionTabsTest.DefaultToIncognitoWhenItIsForcedAndNoArgs
--FencedFrameDownloadTest.DownloadRequestLimiterIsUnaffectedByFencedFrame
--FramebustBlockBrowserTest.AllowRadioButtonSelected
--FramebustBlockBrowserTest.DisallowRadioButtonSelected
--FramebustBlockBrowserTest.FramebustBlocked_SubsequentNavigation_NoUI
--FramebustBlockBrowserTest.ModelAllowsRedirection
--FramebustBlockBrowserTest.SimpleFramebust_Blocked
--FramebustBlockFencedFrameTest.FramebustBlocked_FencedFrameNavigation
--FramebustBlockPrerenderTest.FramebustBlocked_PrerenderNavigation
--HeadlessModeTaggedPrintToPdfCommandBrowserTest.HeadlessTaggedPrintToPdf/1
--HistogramsInternalsUIBrowserTest.*
--IFrameTest.InEmptyFrame
--IdleBrowserTest.Start
--InteractiveBrowserTestBrowsertest.InstrumentTabsAsTestSteps
--InterestGroupPermissionsBrowserTest.ThirdPartyCookiesAllowedForSite
--InterestGroupPermissionsBrowserTest.ThirdPartyCookiesBlocked
--MediaEngagementContentsObserverPrerenderBrowserTest.DoNotSendEngagementLevelToRenderFrameInPrerendering
--LargeStickyAdViolationBrowserTest.LargeStickyAd_AdInterventionTriggered
--OfferNotificationIconViewBrowserTest.InvokeUi_show_offer_notification_icon_expanded
--OverlayPopupAdViolationBrowserTest.OverlayPopupAd_AdInterventionTriggered
--PasswordManagerUITest.App
--PdfOcrMenuObserverTest.PdfOcrItemNotShownWithoutScreenReader
--PermissionManagerBrowserTest.ServiceWorkerPermissionAfterRendererCrash
--PermissionManagerBrowserTest.ServiceWorkerPermissionQueryIncognitoClose
--PersistentBackground/ExtensionApiNewTabTest.Tabs/0
 -PersistentBackground/AutomationApiTestWithContextType.ImageLabels/0
 -PersistentBackground/AutomationApiTestWithContextType.TestRendererAccessibilityEnabled/0
--PopupTrackerBrowserTest.AllowlistedPopup_HasTracker
--PolicyTest.IncognitoEnabled
+-ServiceWorker/AutomationApiTestWithContextType.ImageLabels/0
+-ServiceWorker/AutomationApiTestWithContextType.TestRendererAccessibilityEnabled/0
+
+# The contents of chrome://prefs-interals for these tests is huge, with over
+# 46K FragmentItems being processed in AXBlockFlowData::ProcessLayoutBlock().
+# Processing lots of fragments, even when each one doesn't have a huge number
+# of items, also seems problematic. See crbug.com/417174864.
+-ChromeURLDataManagerWebUITrustedTypesTest.NoTrustedTypesViolation/chrome___prefs_internals
+-HistogramsInternalsUIBrowserTest.*
 -PrefsInternalsTest.TestPrefsAreServed
--PrerenderDownloadTest.DownloadRequestLimiterIsUnaffectedByPrerendering
--PrivacyBudgetBrowserTestWithTestRecorder.EveryNavigationRecordsDocumentCreatedMetrics
--PrivacyBudgetBrowserTestWithTestRecorder.SamplingAPIs
--PrivacyBudgetBrowserTestWithTestRecorder.SamplingScreenAPIs
--PrivacyBudgetReidScoreBrowserTestWithTestRecorder.ReidHashIsReported
--Renderer/SubresourceFilterDnsAliasResourceLoaderBrowserTest.CheckDnsAliasesFromRenderer/ActivationEnabled
--RuntimeGetContextsApiTest.RetrievingIncognitoContexts_SplitMode
--SSLUITest.MixedContentHistogramLoggedForBadCertificateSubresource
--SSLUITest.SimpleURLLoaderCertError
--SSLUITest.TestUnsafeContentsWithUserException
--SafeBrowsingTriggeredInterceptingBrowserTest.AbusiveMetadata
--SafeBrowsingTriggeredPopupBlockerBrowserTest.BlockCreatingNewWindows
--SafeBrowsingTriggeredPopupBlockerBrowserTest.BlockCreatingNewWindows_LogsToConsole
--SafeBrowsingTriggeredPopupBlockerBrowserTest.BlockOpenURLFromTab
--SafeBrowsingTriggeredPopupBlockerBrowserTest.BlockOpenURLFromTabInIframe
--SafeBrowsingTriggeredPopupBlockerBrowserTest.DrivenByEnterprisePolicy
--SafeBrowsingTriggeredPopupBlockerBrowserTest.MultipleNavigations
--SafeBrowsingTriggeredPopupBlockerBrowserTest.OpenNewWindowInSubFrame
--SafeBrowsingTriggeredPopupBlockerBrowserTest.ShowBlockedPopup
--SafeBrowsingTriggeredPopupBlockerFencedFrameBrowserTest.ShouldTriggerPopupBlocker
--SafeBrowsingTriggeredPopupBlockerPrerenderingBrowserTest.PopupBlockedAfterActivation
--SafetyTipPageInfoBubbleViewBrowserTest.NoBubbleOnErrorEvenAfterVisible
--SearchEngineChoiceJsBrowserTest.SearchEngineChoiceTest
--ServiceWorker/AutomationApiTestWithContextType.*
--ServiceWorker/CookiesApiTest.CookiesEventsSpanning/0
--ServiceWorker/CookiesApiTest.CookiesEventsSpanning/1
--ServiceWorker/ExtensionApiNewTabTest.Tabs/0
--ServiceWorker/ExtensionPreferenceApiTest.OnChangeSplit/0
--ServiceWorker/ExtensionPreferenceApiTest.OnChangeSplitWithoutIncognitoAccess/0
--ServiceWorker/ExtensionPreferenceApiTest.SessionOnlyIncognito/0
--SessionRestoreTest.IncognitotoNonIncognito
--SettingsTest.A11yPage
--SigninReauthViewControllerBrowserTest.OpenLinksInNewTab
--StorageAccessAPIStorageBrowserTest.MultiTabTest/*
--StorageAccessAPIWith3PCEnabledBrowserTest.AllowedByUserBypass
--SubresourceFilterBrowserTest.ActivationEnabledOnReload
--SubresourceFilterBrowserTest.ChildOfFrameWithAbortedLoadLoadsDisallowedResource_ResourceBlocked
--SubresourceFilterBrowserTest.CrossSiteSubFrameActivationWithAllowlist
--SubresourceFilterBrowserTest.CrossSiteSubFrameActivationWithoutAllowlist
--SubresourceFilterBrowserTest.DocumentActivationOutlivesSameDocumentNavigation
--SubresourceFilterBrowserTest.DynamicFrame
--SubresourceFilterBrowserTest.ExpectPerformanceHistogramsAreRecorded
--SubresourceFilterBrowserTest.FrameWithDocWriteAbortedLoad_ResourceStillDisallowed
--SubresourceFilterBrowserTest.FrameWithWindowStopAbortedLoad_ResourceStillDisallowed
--SubresourceFilterBrowserTest.HistoryNavigationActivation
--SubresourceFilterBrowserTest.MainFrameActivation
--SubresourceFilterBrowserTest.MainFrameActivationOnStartup
--SubresourceFilterBrowserTest.NewRulesetSameTab_ActivatesSuccessfully
--SubresourceFilterBrowserTest.PageLevelActivationOutlivesAbortedNavigation
--SubresourceFilterBrowserTest.PageLevelActivationOutlivesSameDocumentNavigation
--SubresourceFilterBrowserTest.PopupNavigatesBackToAboutBlank_FilterChecked
--SubresourceFilterBrowserTest.PopupsInheritActivation_ResourcesBlocked
--SubresourceFilterBrowserTest.PromptShownAgainOnNextNavigation
--SubresourceFilterBrowserTest.SameDocumentBeforeInitialNavigation
--SubresourceFilterBrowserTest.SubFrameActivation
--SubresourceFilterBrowserTest.SubframeDocumentLoadFiltering
--SubresourceFilterDevtoolsBrowserTest.ForceActivation_RequiresDevtools
--SubresourceFilterDevtoolsBrowserTest.ForceActivation_SubresourceLogging
--SubresourceFilterDevtoolsBrowserTestWithSitePerProcess.IsolatedSubframe_DoesNotSendAdBlockingMessages
--SubresourceFilterFencedFrameBrowserTest.CollapseBlockedFencedFrame
--SubresourceFilterFencedFrameBrowserTest.FencedFrameLoadFiltering
--SubresourceFilterFencedFrameBrowserTest.LoadFilteringNestedInFencedFrame
--SubresourceFilterFencedFrameBrowserTest.OutermostFrameActivation
--SubresourceFilterInterceptingBrowserTest.BetterAdsMetadata
--SubresourceFilterListInsertingBrowserTest.MainFrameActivation_SubresourceFilterList
--SubresourceFilterListInsertingBrowserTest.WarningSiteWithForceActivation_LogsWarning
--SubresourceFilterPopupBrowserTest.AllowCreatingNewWindows_NoLogToConsole
--SubresourceFilterPopupBrowserTest.BlockCreatingNewWindows_LogsToConsole
--SubresourceFilterPrerenderingBrowserTest.FilterWhilePrerendered
--SubresourceFilterPrerenderingBrowserTest.FilteringPrerenderBecomesPrimary
--SubresourceFilterSettingsBrowserTest.ContentSettingsAllowlistGlobal_DoNotActivate
--SubresourceFilterSettingsBrowserTest.ContentSettingsAllowlistViaReload_AllowlistIsByDomain
--SubresourceFilterSettingsBrowserTest.ContentSettingsAllowlistViaReload_DoNotActivate
--SubresourceFilterSettingsBrowserTest.ContentSettingsAllowlist_DoNotActivate
--SubresourceFilterSettingsBrowserTest.DoNotShowUIUntilThresholdReached
--SubresourceFilterSettingsBrowserTest.DrivenByEnterprisePolicy
--SubresourceFilterSpecialSubframeNavigationsBrowserTest.NavigateCrossProcessDataUrl_MaintainsActivation
--SubresourceFilterSpecialSubframeNavigationsBrowserTest.NavigationsWithNoIPC_HaveActivation
--SubresourceFilterWorkerFetchBrowserTest.WorkerFetch
--SubresourceFilterWorkerFetchBrowserTest.WorkletScriptFetch
--SubresourceFilterWorkerFetchBrowserTest.WorkletStaticImport
--SW_IncognitoEnabled/IncognitoExtensionApiTabTest.Tabs/*
--SystemNetworkContextManagerHttpNegotiateHeader.SetsPrefOnHttpNegotiateHeader
--TabCaptureApiTest.CaptureInSplitIncognitoMode
--TabResourceUsageTabHelperTest.MemoryUsageUpdatesAfterNavigation
--ThirdPartyPartitionedStorageAccessibilityCanBeDisabledTest.Basic/0
--ThirdPartyPartitionedStorageAccessibilityCanBeDisabledTest.Basic/1
--ThirdPartyPartitionedStorageAccessibilitySharedWorkerTest.Basic/0
--ThirdPartyPartitionedStorageAccessibilitySharedWorkerTest.Basic/1
--ThirdPartyPartitionedStorageAccessibilitySharedWorkerTest.UserSetting/0
--ThirdPartyPartitionedStorageAccessibilitySharedWorkerTest.UserSetting/1
--ThirdPartyPartitionedStorageAccessibilityTest.Basic/0
--ThirdPartyPartitionedStorageAccessibilityTest.Basic/1
--ThirdPartyPartitionedStorageAccessibilityTest.Basic/2
--ThirdPartyPartitionedStorageAccessibilityTest.Basic/3
--ThirdPartyPartitionedStorageAccessibilityTest.UserSetting/0
--ThirdPartyPartitionedStorageAccessibilityTest.UserSetting/1
--ThirdPartyPartitionedStorageAccessibilityTest.UserSetting/2
--ThirdPartyPartitionedStorageAccessibilityTest.UserSetting/3
--UnifiedAutoplayBrowserTest.OpenFromRendererNoGesture
--WebAppIntegration.SwitchIncognitoProfile
--WebAppIntegration.WAI_29StandaloneWindowed_79StandaloneStandaloneOriginal_24_12Standalone_7Standalone_112StandaloneNotShown_73_37Standalone_19
--WebAppIntegration.WAI_31Standalone_79StandaloneStandaloneOriginal_24_12Standalone_7Standalone_112StandaloneNotShown_73_37Standalone_19
--WebAppIntegration.WAI_47Standalone_79StandaloneStandaloneOriginal_24_12Standalone_7Standalone_112StandaloneNotShown_73_37Standalone_19
--WebAppIntegration.WAI_73_166_167
--WebAppIntegration.WAI_73_37NotPromotable_17
--WebAppIntegration.WAI_73_37Standalone_17
--WebRtcDesktopCaptureBrowserTest.RunP2PScreenshareWhileSharingTab
+
+# To triage: These tests are confirmed to be flaky or fail, but mainly on "heavy"
+# bots, e.g. debug, *san, etc; not just accessibility.
+-All/PdfDownloadTestSplitCacheEnabled.OpenDownloadInMostRecentBrowser/PdfOopifDisabled_TriplePlusCrossSiteMainFrameNavigationBool
+-ClickToCallBrowserTest.ContextMenu_NoDevicesAvailable
+-WebAppEngagementBrowserTest.CommandLineWindowByAppId
diff --git a/testing/buildbot/filters/ozone-linux.browser_tests_mutter.filter b/testing/buildbot/filters/ozone-linux.browser_tests_mutter.filter
index 9074ff4..8c525af 100644
--- a/testing/buildbot/filters/ozone-linux.browser_tests_mutter.filter
+++ b/testing/buildbot/filters/ozone-linux.browser_tests_mutter.filter
@@ -2,24 +2,43 @@
 # which doesn't work with per-window scaling.
 -All/GetDisplayMediaHiDpiBrowserTest.Capture/1
 
-# Very flaky/failing on mutter (less flaky/passing on weston)
--All/OmniboxSearchAggregatorHTTPErrorTest.HTTPErrorResponse/404
--BackForwardCachePageLoadMetricsObserverBrowserTest.LayoutShiftNormalization_AfterBackForwardCacheRestore
+# Very flaky/failing on mutter (more stable/passing on other builders)
+-AdsPageLoadMetricsObserverBrowserTest.PageAdDensityMultipleFrames
+-AdsPageLoadMetricsObserverBrowserTest.PageAdDensityRecordsPageMax
 -All/PDFExtensionJSInk2Test.Ink2/*
 -All/PDFExtensionJSInk2Test.Ink2BottomToolbar/*
 -All/PDFExtensionJSInk2Test.Ink2TextBottomToolbar/*
 -All/PDFExtensionScrollTest.WithArrowLeftRightScrollToPage/*
+-All/SearchEnginePreconnectorNoDelaysBrowserTest.PreconnectOnlyInForeground/*
+-All/SoftNavigationTest.LayoutShift/*
+-BackForwardCachePageLoadMetricsObserverBrowserTest.LayoutShiftNormalization_AfterBackForwardCacheRestore
+-BorderlessIsolatedWebAppBrowserTest.PopupResize_CanSubceedMinimumWindowSize_And_InnerAndOuterSizesAreCorrect
+-FindInPageControllerTest.FindMovesOnTabClose_Issue1343052
+-FindInPageControllerTest.FindMovesWhenObscuring
+-LayoutInstabilityTest.*
 -PageLoadMetricsBrowserTest.MainFrameIntersectionsMainFrame
--AdsPageLoadMetricsObserverBrowserTest.PageAdDensityMultipleFrames
--AdsPageLoadMetricsObserverBrowserTest.PageAdDensityRecordsPageMax
--ProfilePickerEnterpriseCreationFlowBrowserTest.CreateSignedInProfileWithSuggestedTwoFactorAuthSetup
--WebAppBrowserTest.SetBounds
+-PolicyTest.DefaultSearchProvider
 -PrintBrowserTest.NoResizeEvent
+-PrintBrowserTest.NoScrolling
+-ProfilePickerEnterpriseCreationFlowBrowserTest.CreateSignedInProfileWithSuggestedTwoFactorAuthSetup
+-ReadAnythingReadAloudPhraseHighlightingMochaTest.*
+-SessionRestoreTest.RestoredTabsHaveCorrectInitialSize
+-WebAppBrowserTest.PWASizeIsCorrectlyRestored
+-WebAppBrowserTest.SetBounds
+-WebAppBrowserTest.WindowOffsetsClampedToScreen
+-WebAppFrameToolbarBrowserTest_WindowControlsOverlay.DownloadIconVisibilityForAppDownload
 
-# Failing on both mutter and weston
+# Also flaky/failing weston and/or other builders
+-All/OmniboxSearchAggregatorHTTPErrorTest.HTTPErrorResponse/*
 -BackForwardCachePageLoadMetricsObserverBrowserTest.CumulativeLayoutShiftAfterBackForwardCacheRestore
 -BrowserViewTest.GetAccessibleTabModalDialogTree
+-EnclaveAuthenticatorWithTimeout.SecurityDomainCheckTimesOut
+-ExtensionWebRequestApiTest.ExtensionRequestRedirectToServer
+-OmniboxSearchAggregatorTest.*
+-PermissionElementBrowserTest.CombinedPermissionAndDeviceStatusesGranted
 -ProfileHelperTest.OpenNewWindowForProfile
+-SearchPrefetchServiceNavigationPrefetchBrowserTest.NavigationPrefetchIsServedForOmniboxOpenSelection
+-TabRestoreTest.RestoreGroupWithUnloadHandlerRejected
 
 # TODO(crbug.com/40688401) This test requires full Picture-in-Picture support.
 -FedCmAccountSelectionViewBrowserTest.DisabledWhenOccluded
diff --git a/testing/buildbot/filters/ozone-linux.interactive_ui_tests_mutter.filter b/testing/buildbot/filters/ozone-linux.interactive_ui_tests_mutter.filter
index 5b04ece7..fcf04fb 100644
--- a/testing/buildbot/filters/ozone-linux.interactive_ui_tests_mutter.filter
+++ b/testing/buildbot/filters/ozone-linux.interactive_ui_tests_mutter.filter
@@ -9,12 +9,14 @@
 -BookmarkBarViewTest7.DNDToDifferentMenu
 -BookmarkBarViewTest8.DNDBackToOriginatingMenu
 -BrowserFocusTest.BackgroundBrowserDontStealFocus
+-CaptionBubbleControllerViewsTest.BubblePositioningSmallNonBrowserContext
 -ChromeVisibilityObserverInteractiveTest.VisibilityTest
 -DesktopWidgetTestInteractive.DesktopNativeWidgetWithModalTransientChild
 -DesktopWindowTreeHostPlatformImplTest.CaptureEventForwarding
 -DesktopWindowTreeHostPlatformImplTest.Deactivate
 -DesktopWindowTreeHostPlatformImplTest.InputMethodFocus
 -ExtensionApiTest.WindowOpenFocus
+-GeolocationUsageObserverBrowsertest.GetCurrentPositionWhileWatchingPosition
 -MediaDialogViewBrowserTest.PictureInPicture
 -NotificationsTestWithFakeMediaStream.ShouldQueueDuringScreenPresent
 -PasswordBubbleInteractiveUiTest.AutoSigninNoFocus
diff --git a/testing/variations/fieldtrial_testing_config.json b/testing/variations/fieldtrial_testing_config.json
index f2ce0100..322fe82 100644
--- a/testing/variations/fieldtrial_testing_config.json
+++ b/testing/variations/fieldtrial_testing_config.json
@@ -13254,6 +13254,24 @@
             ]
         }
     ],
+    "LegacyKeyRepeatSynthesis": [
+        {
+            "platforms": [
+                "windows",
+                "mac",
+                "linux",
+                "chromeos"
+            ],
+            "experiments": [
+                {
+                    "name": "Disabled",
+                    "disable_features": [
+                        "LegacyKeyRepeatSynthesis"
+                    ]
+                }
+            ]
+        }
+    ],
     "LegacyTabStateDeprecation": [
         {
             "platforms": [
@@ -16549,7 +16567,7 @@
                         "hats_survey_ukm_id": "1027171324",
                         "ppm_survey_segment_name1": "Linux",
                         "ppm_survey_uniform_sample": "true",
-                        "probability": "0.88",
+                        "probability": "0.088",
                         "survey": "performance-ppm"
                     },
                     "enable_features": [
@@ -16573,7 +16591,7 @@
                         "ppm_survey_segment_name1": "Mac, up to 8 GB",
                         "ppm_survey_segment_name2": "Mac, over 8 GB",
                         "ppm_survey_uniform_sample": "true",
-                        "probability": "0.88",
+                        "probability": "0.088",
                         "survey": "performance-ppm"
                     },
                     "enable_features": [
@@ -16599,7 +16617,7 @@
                         "ppm_survey_segment_name2": "Windows, 4-8 GB",
                         "ppm_survey_segment_name3": "Windows, over 8 GB",
                         "ppm_survey_uniform_sample": "true",
-                        "probability": "0.88",
+                        "probability": "0.088",
                         "survey": "performance-ppm"
                     },
                     "enable_features": [
@@ -23196,7 +23214,7 @@
                 {
                     "name": "Enabled",
                     "params": {
-                        "has_background": "false"
+                        "tab_search_toolbar_button": "true"
                     },
                     "enable_features": [
                         "TabstripComboButton"
diff --git a/third_party/androidx/build.gradle b/third_party/androidx/build.gradle
index cb8e97f..0ef7975 100644
--- a/third_party/androidx/build.gradle
+++ b/third_party/androidx/build.gradle
@@ -304,7 +304,7 @@
     google()
     maven {
         // This URL is generated by the fetch_all_androidx.py script.
-        url 'https://androidx.dev/snapshots/builds/13479845/artifacts/repository'
+        url 'https://androidx.dev/snapshots/builds/13482523/artifacts/repository'
     }
     mavenCentral()
 }
diff --git a/third_party/angle b/third_party/angle
index 481000f..bf837e0 160000
--- a/third_party/angle
+++ b/third_party/angle
@@ -1 +1 @@
-Subproject commit 481000fdb0ae63238e041236545c35715fcfb90f
+Subproject commit bf837e01756fdd934c0f6adb17e5e379dde0f74a
diff --git a/third_party/blink/common/context_menu_data/context_menu_mojom_traits.cc b/third_party/blink/common/context_menu_data/context_menu_mojom_traits.cc
index e5e2e9e..a4d4cddf 100644
--- a/third_party/blink/common/context_menu_data/context_menu_mojom_traits.cc
+++ b/third_party/blink/common/context_menu_data/context_menu_mojom_traits.cc
@@ -66,7 +66,7 @@
   out->writing_direction_right_to_left = data.writing_direction_right_to_left();
   out->edit_flags = data.edit_flags();
   out->selection_start_offset = data.selection_start_offset();
-  out->opened_from_highlight = data.opened_from_highlight();
+  out->annotation_type = data.annotation_type();
   out->opened_from_interest_target = data.opened_from_interest_target();
   out->interest_target_node_id = data.interest_target_node_id();
   out->is_content_editable_for_autofill =
diff --git a/third_party/blink/common/context_menu_data/context_menu_params_builder.cc b/third_party/blink/common/context_menu_data/context_menu_params_builder.cc
index 98c6856a4..e510479 100644
--- a/third_party/blink/common/context_menu_data/context_menu_params_builder.cc
+++ b/third_party/blink/common/context_menu_data/context_menu_params_builder.cc
@@ -72,7 +72,7 @@
   params.frame_charset = data.frame_encoding;
   params.referrer_policy = data.referrer_policy;
   params.suggested_filename = base::UTF8ToUTF16(data.suggested_filename);
-  params.opened_from_highlight = data.opened_from_highlight;
+  params.annotation_type = data.annotation_type;
   params.opened_from_interest_target = data.opened_from_interest_target;
   params.interest_target_node_id = data.interest_target_node_id;
 
diff --git a/third_party/blink/common/context_menu_data/untrustworthy_context_menu_params.cc b/third_party/blink/common/context_menu_data/untrustworthy_context_menu_params.cc
index 6cf9396..da65a2e 100644
--- a/third_party/blink/common/context_menu_data/untrustworthy_context_menu_params.cc
+++ b/third_party/blink/common/context_menu_data/untrustworthy_context_menu_params.cc
@@ -76,7 +76,7 @@
   source_type = other.source_type;
   selection_rect = other.selection_rect;
   selection_start_offset = other.selection_start_offset;
-  opened_from_highlight = other.opened_from_highlight;
+  annotation_type = other.annotation_type;
   opened_from_interest_target = other.opened_from_interest_target;
   interest_target_node_id = other.interest_target_node_id;
   form_control_type = other.form_control_type;
diff --git a/third_party/blink/common/features.cc b/third_party/blink/common/features.cc
index 2eabd9f..b23a087 100644
--- a/third_party/blink/common/features.cc
+++ b/third_party/blink/common/features.cc
@@ -58,6 +58,10 @@
              "CrashReportingAPIMoreContextData",
              base::FEATURE_DISABLED_BY_DEFAULT);
 
+BASE_FEATURE(kOverrideCrashReportingEndpoint,
+             "OverrideCrashReportingEndpoint",
+             base::FEATURE_DISABLED_BY_DEFAULT);
+
 BASE_FEATURE(kLowerHighResolutionTimerThreshold,
              "LowerHighResolutionTimerThreshold",
              base::FEATURE_DISABLED_BY_DEFAULT);
diff --git a/third_party/blink/common/interest_group/auction_config.cc b/third_party/blink/common/interest_group/auction_config.cc
index 4993f3e00..395eee5e 100644
--- a/third_party/blink/common/interest_group/auction_config.cc
+++ b/third_party/blink/common/interest_group/auction_config.cc
@@ -129,6 +129,13 @@
   if (non_shared_params.deprecated_render_url_replacements.is_promise()) {
     ++total;
   }
+  for (const auto& buyer_tkv_signals :
+       non_shared_params.per_buyer_tkv_signals) {
+    if (buyer_tkv_signals.second.is_promise()) {
+      ++total;
+    }
+  }
+
   if (direct_from_seller_signals.is_promise()) {
     ++total;
   }
@@ -138,6 +145,7 @@
   if (expects_additional_bids) {
     ++total;
   }
+
   for (const blink::AuctionConfig& sub_auction :
        non_shared_params.component_auctions) {
     total += sub_auction.NumPromises();
diff --git a/third_party/blink/common/interest_group/auction_config_mojom_traits_test.cc b/third_party/blink/common/interest_group/auction_config_mojom_traits_test.cc
index a46f2af..eb39962 100644
--- a/third_party/blink/common/interest_group/auction_config_mojom_traits_test.cc
+++ b/third_party/blink/common/interest_group/auction_config_mojom_traits_test.cc
@@ -865,17 +865,18 @@
 TEST_F(AuctionConfigMojomTraitsTest, PerBuyerTKVSignals) {
   AuctionConfig auction_config = CreateBasicAuctionConfig();
   auction_config.non_shared_params.per_buyer_tkv_signals = {
-      {url::Origin::Create(GURL("https://example.com")), "foo"},
+      {url::Origin::Create(GURL("https://example.test")),
+       AuctionConfig::MaybePromiseJson::FromPromise()},
+      {url::Origin::Create(GURL("https://example2.test")),
+       AuctionConfig::MaybePromiseJson::FromValue(std::nullopt)},
+      {url::Origin::Create(GURL("https://example3.test")),
+       AuctionConfig::MaybePromiseJson::FromValue("[1, 2, 3]")},
   };
   EXPECT_TRUE(SerializeAndDeserialize(auction_config));
 
   auction_config.non_shared_params.per_buyer_tkv_signals = {
-      {url::Origin::Create(GURL("http://example.com")), "foo"},
-  };
-  EXPECT_FALSE(SerializeAndDeserialize(auction_config));
-
-  auction_config.non_shared_params.per_buyer_tkv_signals = {
-      {url::Origin::Create(GURL("data:,foo")), "foo"},
+      {url::Origin::Create(GURL("data:,foo")),
+       AuctionConfig::MaybePromiseJson::FromValue("[1, 2, 3]")},
   };
   EXPECT_FALSE(SerializeAndDeserialize(auction_config));
 }
diff --git a/third_party/blink/common/interest_group/auction_config_test_util.cc b/third_party/blink/common/interest_group/auction_config_test_util.cc
index 69058fc..5e94625 100644
--- a/third_party/blink/common/interest_group/auction_config_test_util.cc
+++ b/third_party/blink/common/interest_group/auction_config_test_util.cc
@@ -62,7 +62,8 @@
       blink::AuctionConfig::MaybePromisePerBuyerSignals::FromValue(
           std::move(per_buyer_signals));
 
-  non_shared_params.per_buyer_tkv_signals[buyer] = "[8]";
+  non_shared_params.per_buyer_tkv_signals[buyer] =
+      AuctionConfig::MaybePromiseJson::FromValue("[8]");
 
   AuctionConfig::BuyerTimeouts buyer_timeouts;
   buyer_timeouts.per_buyer_timeouts.emplace();
diff --git a/third_party/blink/common/interest_group/devtools_serialization_unittest.cc b/third_party/blink/common/interest_group/devtools_serialization_unittest.cc
index 2e258d6c..bcde868 100644
--- a/third_party/blink/common/interest_group/devtools_serialization_unittest.cc
+++ b/third_party/blink/common/interest_group/devtools_serialization_unittest.cc
@@ -179,7 +179,10 @@
       }
    },
    "perBuyerTKVSignals": {
-      "https://buyer.test": "[8]"
+      "https://buyer.test": {
+         "pending": false,
+         "value": "[8]"
+      }
    },
    "perBuyerTimeouts": {
       "pending": false,
diff --git a/third_party/blink/public/common/context_menu_data/context_menu_data.h b/third_party/blink/public/common/context_menu_data/context_menu_data.h
index d55b8230..5319149 100644
--- a/third_party/blink/public/common/context_menu_data/context_menu_data.h
+++ b/third_party/blink/public/common/context_menu_data/context_menu_data.h
@@ -38,6 +38,7 @@
 #include "third_party/blink/public/common/context_menu_data/menu_item_info.h"
 #include "third_party/blink/public/common/input/web_menu_source_type.h"
 #include "third_party/blink/public/common/navigation/impression.h"
+#include "third_party/blink/public/mojom/annotation/annotation.mojom-shared.h"
 #include "third_party/blink/public/mojom/context_menu/context_menu.mojom-shared.h"
 #include "third_party/blink/public/mojom/forms/form_control_type.mojom-shared.h"
 #include "ui/gfx/geometry/point.h"
@@ -160,9 +161,9 @@
 
   WebMenuSourceType source_type;
 
-  // True when the context contains text selected by a text fragment. See
-  // TextFragmentAnchor.
-  bool opened_from_highlight = false;
+  // Set when the context contains text selected by an annotation (see
+  // third_party/blink/renderer/core/annotation/README.md).
+  std::optional<mojom::AnnotationType> annotation_type;
 
   // True when the context menu was opened from an element with the
   // `interesttarget` attribute.
diff --git a/third_party/blink/public/common/context_menu_data/context_menu_mojom_traits.h b/third_party/blink/public/common/context_menu_data/context_menu_mojom_traits.h
index ae349ba..94dc66d2 100644
--- a/third_party/blink/public/common/context_menu_data/context_menu_mojom_traits.h
+++ b/third_party/blink/public/common/context_menu_data/context_menu_mojom_traits.h
@@ -177,9 +177,9 @@
     return r.selection_start_offset;
   }
 
-  static bool opened_from_highlight(
+  static std::optional<blink::mojom::AnnotationType> annotation_type(
       const blink::UntrustworthyContextMenuParams& r) {
-    return r.opened_from_highlight;
+    return r.annotation_type;
   }
 
   static bool opened_from_interest_target(
diff --git a/third_party/blink/public/common/context_menu_data/untrustworthy_context_menu_params.h b/third_party/blink/public/common/context_menu_data/untrustworthy_context_menu_params.h
index 927cfd8..f843e7c 100644
--- a/third_party/blink/public/common/context_menu_data/untrustworthy_context_menu_params.h
+++ b/third_party/blink/public/common/context_menu_data/untrustworthy_context_menu_params.h
@@ -15,6 +15,7 @@
 #include "build/build_config.h"
 #include "services/network/public/mojom/referrer_policy.mojom.h"
 #include "third_party/blink/public/common/navigation/impression.h"
+#include "third_party/blink/public/mojom/annotation/annotation.mojom-forward.h"
 #include "third_party/blink/public/mojom/context_menu/context_menu.mojom-forward.h"
 #include "third_party/blink/public/mojom/forms/form_control_type.mojom-shared.h"
 #include "ui/base/mojom/menu_source_type.mojom-forward.h"
@@ -137,9 +138,9 @@
   // Start position of the selection text.
   int selection_start_offset;
 
-  // The context menu was opened by right clicking on an existing
-  // highlight/fragment.
-  bool opened_from_highlight = false;
+  // If set to a value, the context menu was opened by right clicking on an
+  // existing annotation highlight with the corresponding type.
+  std::optional<mojom::AnnotationType> annotation_type;
 
   // True when the context menu was opened from an element with the
   // `interesttarget` attribute.
diff --git a/third_party/blink/public/common/features.h b/third_party/blink/public/common/features.h
index 9c8549e..73e3530 100644
--- a/third_party/blink/public/common/features.h
+++ b/third_party/blink/public/common/features.h
@@ -56,6 +56,11 @@
 // API. See https://crbug.com/400432195.
 BLINK_COMMON_EXPORT BASE_DECLARE_FEATURE(kCrashReportingAPIMoreContextData);
 
+// Enables crash reports to be sent to the `crash-endpoint` group as specified
+// in the `Reporting-Endpoints` response header.
+// See https://github.com/WICG/crash-reporting/issues/24 for more details.
+BLINK_COMMON_EXPORT BASE_DECLARE_FEATURE(kOverrideCrashReportingEndpoint);
+
 // Feature for allowing page into back/forward cache when datapipe has been
 // drained as bytes consumer for fetch requests.
 BLINK_COMMON_EXPORT BASE_DECLARE_FEATURE(
diff --git a/third_party/blink/public/common/interest_group/auction_config.h b/third_party/blink/public/common/interest_group/auction_config.h
index 4d14d52..96ef3f3d 100644
--- a/third_party/blink/public/common/interest_group/auction_config.h
+++ b/third_party/blink/public/common/interest_group/auction_config.h
@@ -290,9 +290,12 @@
     // Value is opaque JSON data, passed as object to particular buyers.
     MaybePromisePerBuyerSignals per_buyer_signals;
 
-    // Similar as per_buyer_signals, but is not a promise value and can only
-    // used for TKV server for trusted KVv2 bidding signals.
-    base::flat_map<url::Origin, std::string> per_buyer_tkv_signals;
+    // Similar to `per_buyer_signals`, but instead of being a single promise,
+    // allows a promise to be specified for each buyer. Sent to trusted
+    // servers for interest groups owned by the corresponding buyer, when
+    // using trusted key value servers that support the TEE-based version 2 of
+    // the protocol.
+    base::flat_map<url::Origin, MaybePromiseJson> per_buyer_tkv_signals;
 
     // Values restrict the runtime of generateBid() scripts.
     MaybePromiseBuyerTimeouts buyer_timeouts;
@@ -480,7 +483,7 @@
   // will be sent to V1 trusted seller signals server.
   std::optional<bool> send_creative_scanning_metadata;
 
-  static_assert(__LINE__ == 483, R"(
+  static_assert(__LINE__ == 486, R"(
 If modifying AuctionConfig fields, please make sure to also modify:
 
 * third_party/blink/public/mojom/interest_group/interest_group_types.mojom
diff --git a/third_party/blink/public/common/interest_group/auction_config_mojom_traits.h b/third_party/blink/public/common/interest_group/auction_config_mojom_traits.h
index f7ace7a..53682ff 100644
--- a/third_party/blink/public/common/interest_group/auction_config_mojom_traits.h
+++ b/third_party/blink/public/common/interest_group/auction_config_mojom_traits.h
@@ -313,8 +313,9 @@
     return params.per_buyer_signals;
   }
 
-  static const base::flat_map<url::Origin, std::string>& per_buyer_tkv_signals(
-      const blink::AuctionConfig::NonSharedParams& params) {
+  static const base::flat_map<url::Origin,
+                              blink::AuctionConfig::MaybePromiseJson>&
+  per_buyer_tkv_signals(const blink::AuctionConfig::NonSharedParams& params) {
     return params.per_buyer_tkv_signals;
   }
 
diff --git a/third_party/blink/public/mojom/annotation/annotation.mojom b/third_party/blink/public/mojom/annotation/annotation.mojom
index 00f54447..bfc45882 100644
--- a/third_party/blink/public/mojom/annotation/annotation.mojom
+++ b/third_party/blink/public/mojom/annotation/annotation.mojom
@@ -146,4 +146,9 @@
       SelectorCreationResult? result,
       LinkGenerationError error,
       LinkGenerationReadyStatus ready_status);
+
+  // Removes all annotations of a particular type from the container's document.
+  //
+  // `type`:  The particular type (use case) of annotation to remove
+  RemoveAgentsOfType(AnnotationType type);
 };
diff --git a/third_party/blink/public/mojom/context_menu/context_menu.mojom b/third_party/blink/public/mojom/context_menu/context_menu.mojom
index 6bfb4254..e746448 100644
--- a/third_party/blink/public/mojom/context_menu/context_menu.mojom
+++ b/third_party/blink/public/mojom/context_menu/context_menu.mojom
@@ -8,6 +8,7 @@
 import "services/network/public/mojom/referrer_policy.mojom";
 import "third_party/blink/public/mojom/conversions/conversions.mojom";
 import "third_party/blink/public/mojom/forms/form_control_type.mojom";
+import "third_party/blink/public/mojom/annotation/annotation.mojom";
 import "ui/base/mojom/menu_source_type.mojom";
 import "ui/gfx/geometry/mojom/geometry.mojom";
 import "url/mojom/url.mojom";
@@ -196,9 +197,11 @@
   // Start position of the selection text.
   int32 selection_start_offset;
 
-  // Whether the context contains text highlighted by a text fragment.
-  // See TextFragmentAnchor.
-  bool opened_from_highlight;
+  // Only set when the context contains an annotation, and specifies the type
+  // (use case) of that annotation.
+  // Note: There may be multiple annotations of different types in the context,
+  // in which case this set to the type of the "topmost" annotation.
+  AnnotationType? annotation_type;
 
   // True when the context menu was opened from an element with the
   // `interesttarget` attribute.
diff --git a/third_party/blink/public/mojom/interest_group/ad_auction_service.mojom b/third_party/blink/public/mojom/interest_group/ad_auction_service.mojom
index 087bfb5..8e1981a 100644
--- a/third_party/blink/public/mojom/interest_group/ad_auction_service.mojom
+++ b/third_party/blink/public/mojom/interest_group/ad_auction_service.mojom
@@ -54,6 +54,15 @@
       AuctionAdConfigAuctionId auction,
       map<url.mojom.Origin, string>? per_buyer_signals);
 
+  // Used to provide result of resolving a promise for a single buyer's entry in
+  // the `per_buyer_tkv_signals` field of an AuctionConfig. A nullopt `json_value`
+  // means not to send any signals, either due to the promise being rejected, or
+  // being passed data that is not serializable to JSON.
+  ResolvedBuyerTkvSignalsPromise(
+      AuctionAdConfigAuctionId auction,
+      url.mojom.Origin buyer,
+      string? json_value);
+
   // Used to provide result of resolving a promise specifying
   // `per_buyer_timeouts` or `per_buyer_cumulative_timeouts` field of an
   // AuctionConfig.
diff --git a/third_party/blink/public/mojom/interest_group/interest_group_types.mojom b/third_party/blink/public/mojom/interest_group/interest_group_types.mojom
index 9cb4759..d3261a1 100644
--- a/third_party/blink/public/mojom/interest_group/interest_group_types.mojom
+++ b/third_party/blink/public/mojom/interest_group/interest_group_types.mojom
@@ -529,9 +529,18 @@
   AuctionAdConfigMaybePromisePerBuyerSignals per_buyer_signals;
 
   // Per buyer signals of contextual data are sent with trusted KVv2 signals to
-  // TKV servers. The keys represent buyer HTTPS origins, while the values contain
-  // opaque JSON data.
-  map<url.mojom.Origin, string> per_buyer_tkv_signals;
+  // TKV servers. The keys represent buyer HTTPS origins, while the values are
+  // promises that are eventually resolved to opaque JSON data. These promises
+  // should be all resolved at time of call to the worklet, though the worklet
+  // should not need these values.
+  //
+  // An AuctionAdConfigMaybePromiseJson with a nullopt `value` means the promise
+  // was rejected or invalid data was provided, and no signals should be sent to
+  // the TKV server, though the auction should proceed.
+  //
+  // TODO(crbug.com/411554203): Move this out of AuctionAdConfigNonSharedParams,
+  // and into parent AuctionAdConfig instead, to avoid unnecessary copies.
+  map<url.mojom.Origin, AuctionAdConfigMaybePromiseJson> per_buyer_tkv_signals;
 
   // Timeouts for individual interest group worklet Javascript execution.
   AuctionAdConfigMaybePromiseBuyerTimeouts buyer_timeouts;
diff --git a/third_party/blink/public/mojom/use_counter/metrics/web_feature.mojom b/third_party/blink/public/mojom/use_counter/metrics/web_feature.mojom
index 3cc0c16..454e82d 100644
--- a/third_party/blink/public/mojom/use_counter/metrics/web_feature.mojom
+++ b/third_party/blink/public/mojom/use_counter/metrics/web_feature.mojom
@@ -4542,6 +4542,7 @@
   kCanvasTextDirectionSet = 5238,
   kCanvasTextDirectionSetInherit = 5239,
   kTopicsAPIImg = 5240,
+  // The items above roughly this point are available in the M133 branch.
   kMediaSessionEnterPictureInPicture = 5241,
   kOBSOLETE_V8AILanguageDetector_Detect_Method = 5242,
   kCharsetAutoDetection = 5243,
@@ -4603,6 +4604,7 @@
   kHTMLImageElementNaturalSizeDiffersForSvgImage = 5299,
   kWindowProxyIndexedGetter = 5300,
   kWindowProxyNamedGetter = 5301,
+  // The items above roughly this point are available in the M134 branch.
   kOBSOLETE_V8AILanguageModelFactory_Availability_Method = 5302,
   kOBSOLETE_V8AILanguageModelFactory_Params_Method = 5303,
   kOBSOLETE_V8AISummarizerFactory_Availability_Method = 5304,
@@ -4637,6 +4639,7 @@
   kButtonTypeAttrInvalidWithCommandOrCommandfor = 5333,
   kCSSVarFallbackCycle = 5334,
   kCSSAttrFallbackCycle = 5335,
+  // The items above roughly this point are available in the M135 branch.
   kCSSRainbowGradientPattern = 5336,
   kWebAppManifestStartUrl = 5337,
   kWebAppManifestDisplay = 5338,
@@ -4765,6 +4768,7 @@
   kLanguageDetector_ExpectedInputLanguages = 5461,
   kServiceWorkerPushEventListener = 5462,
   kServiceWorkerPushSubscriptionChangeEventListener = 5463,
+  // The items above roughly this point are available in the M136 branch.
   kMediaPlaybackWhileNotVisiblePermissionPolicy = 5464,
   kFirstLinePseudoElement = 5465,
   kFirstLetterPseudoElement = 5466,
@@ -4853,6 +4857,7 @@
   kSelectMultipleShowPopup = 5549,
   kSharedWorkerExtendedLifetimeFeatureEnabled = 5550,
   kSharedWorkerExtendedLifetimeIsTrue = 5551,
+  // The items above roughly this point are available in the M137 branch.
   kEditContextTextFormatUpdateAddListener = 5552,
   kEditContextTextFormatUpdateFireEvent = 5553,
   kEditContextTextFormatUpdateTextFormatThicknessOrStyleNotNone = 5554,
@@ -4880,6 +4885,7 @@
   kCredentialsGetImmediateMediationFailure = 5576,
   kClearSiteData = 5577,
   kScrollIntoViewContainerNearest = 5578,
+  kPopoverShown = 5579,
 
   // Add new features immediately above this line. Don't change assigned
   // numbers of any item, and don't reuse removed slots. Also don't add extra
diff --git a/third_party/blink/renderer/bindings/core/v8/module_record.cc b/third_party/blink/renderer/bindings/core/v8/module_record.cc
index 2181224..b645ef5 100644
--- a/third_party/blink/renderer/bindings/core/v8/module_record.cc
+++ b/third_party/blink/renderer/bindings/core/v8/module_record.cc
@@ -160,6 +160,7 @@
         v8_module_requests->Get(script_state->GetContext(), i)
             .As<v8::ModuleRequest>();
     v8::Local<v8::String> v8_specifier = v8_module_request->GetSpecifier();
+    v8::ModuleImportPhase import_phase = v8_module_request->GetPhase();
     TextPosition position = TextPosition::MinimumPosition();
     if (needs_text_position) {
       // The source position is only used by DevTools for module requests and
@@ -181,7 +182,7 @@
 
     requests.emplace_back(
         ToCoreString(script_state->GetIsolate(), v8_specifier), position,
-        import_attributes);
+        import_attributes, import_phase);
   }
 
   return requests;
diff --git a/third_party/blink/renderer/bindings/core/v8/module_request.h b/third_party/blink/renderer/bindings/core/v8/module_request.h
index cf0077e..6d6d156 100644
--- a/third_party/blink/renderer/bindings/core/v8/module_request.h
+++ b/third_party/blink/renderer/bindings/core/v8/module_request.h
@@ -8,6 +8,7 @@
 #include "third_party/blink/renderer/core/core_export.h"
 #include "third_party/blink/renderer/platform/wtf/text/text_position.h"
 #include "third_party/blink/renderer/platform/wtf/text/wtf_string.h"
+#include "v8/include/v8-callbacks.h"
 
 namespace blink {
 
@@ -25,18 +26,22 @@
 
 // An instance of a ModuleRequest record:
 // https://tc39.es/proposal-import-attributes/#sec-modulerequest-record
-// Represents a module script's request to import a module given a specifier and
-// list of import attributes.
+// Represents a module script's request to import a module given a specifier, an
+// import phase and a list of import attributes.
 struct CORE_EXPORT ModuleRequest {
   String specifier;
   TextPosition position;
   Vector<ImportAttribute> import_attributes;
+  v8::ModuleImportPhase import_phase;
   ModuleRequest(const String& specifier,
                 const TextPosition& position,
-                const Vector<ImportAttribute>& import_attributes)
+                const Vector<ImportAttribute>& import_attributes,
+                const v8::ModuleImportPhase& import_phase =
+                    v8::ModuleImportPhase::kEvaluation)
       : specifier(specifier),
         position(position),
-        import_attributes(import_attributes) {}
+        import_attributes(import_attributes),
+        import_phase(import_phase) {}
 
   String GetModuleTypeString() const;
 
diff --git a/third_party/blink/renderer/core/annotation/annotation_agent_container_impl.cc b/third_party/blink/renderer/core/annotation/annotation_agent_container_impl.cc
index e497614..4ca53c7 100644
--- a/third_party/blink/renderer/core/annotation/annotation_agent_container_impl.cc
+++ b/third_party/blink/renderer/core/annotation/annotation_agent_container_impl.cc
@@ -234,6 +234,23 @@
                     WrapWeakPersistent(this), std::move(callback)));
 }
 
+void AnnotationAgentContainerImpl::RemoveAgentsOfType(
+    mojom::blink::AnnotationType type) {
+  TRACE_EVENT("blink", "AnnotationAgentContainerImpl::RemoveAgentsOfType",
+              "type", ToString(type));
+  // Note: We need this temporary vector to avoid removal of elements in
+  // `agents_` while iterating through to it. `AnnotationAgentImpl::Remove`
+  // (called below) calls `AnnotationAgentContainerImpl::RemoveAgent`, which
+  // removes itself from `agents_`.
+  HeapVector<Member<AnnotationAgentImpl>> agents_to_remove;
+  std::ranges::copy_if(
+      agents_, std::back_inserter(agents_to_remove),
+      [type](AnnotationAgentImpl* agent) { return agent->GetType() == type; });
+  for (AnnotationAgentImpl* agent : agents_to_remove) {
+    agent->Remove();
+  }
+}
+
 // TODO(cheickcisse@): Move shared highlighting enums, also used in user note to
 // annotation.mojom.
 void AnnotationAgentContainerImpl::DidFinishSelectorGeneration(
diff --git a/third_party/blink/renderer/core/annotation/annotation_agent_container_impl.h b/third_party/blink/renderer/core/annotation/annotation_agent_container_impl.h
index ec27817b..9958d54 100644
--- a/third_party/blink/renderer/core/annotation/annotation_agent_container_impl.h
+++ b/third_party/blink/renderer/core/annotation/annotation_agent_container_impl.h
@@ -7,6 +7,7 @@
 
 #include "base/types/pass_key.h"
 #include "components/shared_highlighting/core/common/shared_highlighting_metrics.h"
+#include "third_party/blink/public/mojom/annotation/annotation.mojom-blink-forward.h"
 #include "third_party/blink/public/mojom/annotation/annotation.mojom-blink.h"
 #include "third_party/blink/renderer/core/core_export.h"
 #include "third_party/blink/renderer/core/dom/document.h"
@@ -112,6 +113,7 @@
   void CreateAgentFromSelection(
       mojom::blink::AnnotationType type,
       CreateAgentFromSelectionCallback callback) override;
+  void RemoveAgentsOfType(mojom::blink::AnnotationType type) override;
 
   void OpenedContextMenuOverSelection();
 
diff --git a/third_party/blink/renderer/core/annotation/annotation_agent_container_impl_test.cc b/third_party/blink/renderer/core/annotation/annotation_agent_container_impl_test.cc
index 5f6e691..290f88c 100644
--- a/third_party/blink/renderer/core/annotation/annotation_agent_container_impl_test.cc
+++ b/third_party/blink/renderer/core/annotation/annotation_agent_container_impl_test.cc
@@ -42,6 +42,11 @@
     return container.agents_.size();
   }
 
+  AnnotationAgentImpl* GetAgentAt(AnnotationAgentContainerImpl& container,
+                                  size_t index) {
+    return container.agents_[index];
+  }
+
   void SendRightClick(const gfx::Point& click_point) {
     auto event = frame_test_helpers::CreateMouseEvent(
         WebMouseEvent::Type::kMouseDown, WebMouseEvent::Button::kRight,
@@ -759,4 +764,53 @@
   EXPECT_FALSE(host.did_disconnect_);
 }
 
+TEST_F(AnnotationAgentContainerImplTest, RemoveAgentsOfType) {
+  SimRequest request("https://example.com/test.html", "text/html");
+  LoadURL("https://example.com/test.html");
+  request.Complete(R"HTML(
+    <!DOCTYPE html>
+    TEST PAGE
+  )HTML");
+  Compositor().BeginFrame();
+
+  mojo::Remote<mojom::blink::AnnotationAgentContainer> remote;
+  AnnotationAgentContainerImpl::BindReceiver(
+      GetDocument().GetFrame(), remote.BindNewPipeAndPassReceiver());
+  ASSERT_TRUE(remote.is_connected());
+  auto* container = AnnotationAgentContainerImpl::FromIfExists(GetDocument());
+  ASSERT_TRUE(container);
+
+  struct AnnotationAgentHost {
+    MockAnnotationAgentHost host;
+    mojom::AnnotationType type = mojom::AnnotationType::kSharedHighlight;
+  };
+  std::array<AnnotationAgentHost, 3> agent_hosts;
+  agent_hosts[0].type = mojom::AnnotationType::kGlic;
+  agent_hosts[2].type = mojom::AnnotationType::kGlic;
+
+  for (auto& agent_host : agent_hosts) {
+    auto remote_receiver_pair = agent_host.host.BindForCreateAgent();
+    container->CreateAgent(std::move(remote_receiver_pair.first),
+                           std::move(remote_receiver_pair.second),
+                           agent_host.type,
+                           mojom::blink::Selector::NewSerializedSelector(
+                               "MockAnnotationSelector"));
+  }
+
+  remote->RemoveAgentsOfType(mojom::AnnotationType::kGlic);
+  remote.FlushForTesting();
+
+  // Only the agents of type kGlic should be removed.
+  EXPECT_EQ(GetAgentCount(*container), 1u);
+  EXPECT_EQ(GetAgentAt(*container, 0)->GetType(),
+            mojom::AnnotationType::kSharedHighlight);
+
+  for (auto& agent_host : agent_hosts) {
+    agent_host.host.FlushForTesting();
+  }
+  EXPECT_TRUE(agent_hosts[0].host.did_disconnect_);
+  EXPECT_FALSE(agent_hosts[1].host.did_disconnect_);
+  EXPECT_TRUE(agent_hosts[2].host.did_disconnect_);
+}
+
 }  // namespace blink
diff --git a/third_party/blink/renderer/core/annotation/annotation_agent_impl.cc b/third_party/blink/renderer/core/annotation/annotation_agent_impl.cc
index e4541b1..c2c0d39 100644
--- a/third_party/blink/renderer/core/annotation/annotation_agent_impl.cc
+++ b/third_party/blink/renderer/core/annotation/annotation_agent_impl.cc
@@ -21,6 +21,7 @@
 #include "third_party/blink/renderer/core/editing/ephemeral_range.h"
 #include "third_party/blink/renderer/core/editing/markers/document_marker_controller.h"
 #include "third_party/blink/renderer/core/editing/markers/text_fragment_marker.h"
+#include "third_party/blink/renderer/core/editing/position_with_affinity.h"
 #include "third_party/blink/renderer/core/editing/range_in_flat_tree.h"
 #include "third_party/blink/renderer/core/editing/visible_units.h"
 #include "third_party/blink/renderer/core/execution_context/execution_context.h"
@@ -30,6 +31,7 @@
 #include "third_party/blink/renderer/core/html/html_details_element.h"
 #include "third_party/blink/renderer/core/layout/geometry/box_strut.h"
 #include "third_party/blink/renderer/core/layout/geometry/physical_rect.h"
+#include "third_party/blink/renderer/core/layout/hit_test_result.h"
 #include "third_party/blink/renderer/core/layout/layout_object.h"
 #include "third_party/blink/renderer/core/layout/layout_text.h"
 #include "third_party/blink/renderer/core/layout/layout_view.h"
@@ -189,6 +191,25 @@
   return length <= 1.f;
 }
 
+bool HasMarkerAroundPosition(const HitTestResult& result,
+                             DocumentMarker::MarkerType marker_type) {
+  // Tree should be clean before accessing the position.
+  // |HitTestResult::GetPosition| calls |PositionForPoint()| which requires
+  // |kPrePaintClean|.
+  DCHECK_GE(result.InnerNodeFrame()->GetDocument()->Lifecycle().GetState(),
+            DocumentLifecycle::kPrePaintClean);
+
+  DocumentMarkerController& marker_controller =
+      result.InnerNodeFrame()->GetDocument()->Markers();
+  PositionWithAffinity pos_with_affinity = result.GetPosition();
+  const Position marker_position = pos_with_affinity.GetPosition();
+
+  auto markers = marker_controller.MarkersAroundPosition(
+      ToPositionInFlatTree(marker_position),
+      DocumentMarker::MarkerTypes(marker_type));
+  return !markers.empty();
+}
+
 }  // namespace
 
 AnnotationAgentImpl::AnnotationAgentImpl(
@@ -408,6 +429,26 @@
                                              bounding_box, std::move(params));
 }
 
+std::optional<mojom::blink::AnnotationType>
+AnnotationAgentImpl::IsOverAnnotation(const HitTestResult& result) {
+  if (!result.InnerNode() || !result.InnerNodeFrame()) {
+    return std::nullopt;
+  }
+
+  if (HasMarkerAroundPosition(result, DocumentMarker::MarkerType::kGlic)) {
+    // Note: We could also have a marker of type kTextFragment around the
+    // position as well, but we treat kGlic as topmost.
+    return mojom::blink::AnnotationType::kGlic;
+  }
+
+  if (HasMarkerAroundPosition(result,
+                              DocumentMarker::MarkerType::kTextFragment)) {
+    return mojom::blink::AnnotationType::kSharedHighlight;
+  }
+
+  return std::nullopt;
+}
+
 void AnnotationAgentImpl::DidFinishFindRange(const RangeInFlatTree* range) {
   TRACE_EVENT("blink", "AnnotationAgentImpl::DidFinishFindRange",
               "bound_to_host", agent_host_.is_bound());
diff --git a/third_party/blink/renderer/core/annotation/annotation_agent_impl.h b/third_party/blink/renderer/core/annotation/annotation_agent_impl.h
index 651efd9b..c1135fb 100644
--- a/third_party/blink/renderer/core/annotation/annotation_agent_impl.h
+++ b/third_party/blink/renderer/core/annotation/annotation_agent_impl.h
@@ -24,6 +24,7 @@
 class AnnotationAgentImplTest;
 class AnnotationSelector;
 class Document;
+class HitTestResult;
 class RangeInFlatTree;
 
 // This class represents an instantiation of an annotation in a Document. It is
@@ -121,6 +122,14 @@
 
   mojom::blink::AnnotationType GetType() const { return type_; }
 
+  // Determine if `result` represents a click on an existing annotation, and
+  // returns the type of the annotation if so (or std::nullopt if not).
+  // Note: It is possible for the click to be above multiple annotations, in
+  // which case we only return the type of what we consider to be the "topmost"
+  // (see implementation).
+  static std::optional<mojom::blink::AnnotationType> IsOverAnnotation(
+      const HitTestResult& result);
+
  private:
   friend AnnotationAgentImplTest;
 
diff --git a/third_party/blink/renderer/core/css/resolver/style_adjuster.cc b/third_party/blink/renderer/core/css/resolver/style_adjuster.cc
index bbb80ad..7eb0239 100644
--- a/third_party/blink/renderer/core/css/resolver/style_adjuster.cc
+++ b/third_party/blink/renderer/core/css/resolver/style_adjuster.cc
@@ -696,7 +696,7 @@
 // g-issues.chromium.org/issues/349835587
 // https://github.com/WICG/canvas-place-element
 static bool IsCanvasPlaceOrDrawElement(const Element* element) {
-  if (RuntimeEnabledFeatures::CanvasElementDrawElementEnabled() && element &&
+  if (RuntimeEnabledFeatures::CanvasDrawElementEnabled() && element &&
       element->IsInCanvasSubtree()) {
     // Placed elements are always immediate children of the canvas.
     if (const auto* canvas =
@@ -709,7 +709,7 @@
 }
 
 static bool IsCanvasWithPlaceOrDrawElements(const Element* element) {
-  if (!RuntimeEnabledFeatures::CanvasElementDrawElementEnabled() || !element) {
+  if (!RuntimeEnabledFeatures::CanvasDrawElementEnabled() || !element) {
     return false;
   }
 
diff --git a/third_party/blink/renderer/core/display_lock/display_lock_context_test.cc b/third_party/blink/renderer/core/display_lock/display_lock_context_test.cc
index 4991ee6..3e9f2ec2 100644
--- a/third_party/blink/renderer/core/display_lock/display_lock_context_test.cc
+++ b/third_party/blink/renderer/core/display_lock/display_lock_context_test.cc
@@ -36,6 +36,7 @@
 #include "third_party/blink/renderer/core/paint/paint_layer.h"
 #include "third_party/blink/renderer/core/style/computed_style.h"
 #include "third_party/blink/renderer/core/testing/core_unit_test_helper.h"
+#include "third_party/blink/renderer/platform/testing/runtime_enabled_features_test_helpers.h"
 #include "third_party/blink/renderer/platform/testing/task_environment.h"
 #include "third_party/blink/renderer/platform/testing/unit_test_helpers.h"
 
@@ -2224,6 +2225,7 @@
 }
 TEST_F(DisplayLockContextRenderingTest,
        SelectionOnAnonymousColumnSpannerDoesNotCrash) {
+  ScopedFlowThreadLessForTest scope(false);
   SetHtmlInnerHTML(R"HTML(
     <style>
       #columns {
diff --git a/third_party/blink/renderer/core/dom/document.cc b/third_party/blink/renderer/core/dom/document.cc
index f1e23756..1644b59 100644
--- a/third_party/blink/renderer/core/dom/document.cc
+++ b/third_party/blink/renderer/core/dom/document.cc
@@ -7491,7 +7491,8 @@
     render_blocking_resource_manager_->ClearPendingParsingElements();
     if (features::kThrottleFrameRateOnInitialization.Get() && GetFrame() &&
         GetFrame()->IsLocalRoot() && GetFrame()->GetPage() &&
-        GetFrame()->IsAttached()) {
+        GetFrame()->IsAttached() &&
+        GetExecutionContext()->CrossOriginIsolatedCapability()) {
       // The frame rate will be implicitly throttled during initialization
       // if the feature is enabled so unthrottle here.
       GetFrame()->GetPage()->GetChromeClient().SetShouldThrottleFrameRate(
@@ -9560,7 +9561,8 @@
 }
 
 void Document::UpdateRenderFrameRate() {
-  if (!GetFrame() || !GetFrame()->GetPage() || !GetFrame()->IsAttached()) {
+  if (!GetFrame() || !GetFrame()->GetPage() || !GetFrame()->IsAttached() ||
+      !GetExecutionContext()->CrossOriginIsolatedCapability()) {
     return;
   }
   GetFrame()->GetPage()->GetChromeClient().SetShouldThrottleFrameRate(
diff --git a/third_party/blink/renderer/core/editing/selection_controller.cc b/third_party/blink/renderer/core/editing/selection_controller.cc
index 740f2249..a855ee6 100644
--- a/third_party/blink/renderer/core/editing/selection_controller.cc
+++ b/third_party/blink/renderer/core/editing/selection_controller.cc
@@ -32,6 +32,7 @@
 #include "base/auto_reset.h"
 #include "third_party/blink/public/common/input/web_menu_source_type.h"
 #include "third_party/blink/public/platform/web_input_event_result.h"
+#include "third_party/blink/renderer/core/annotation/annotation_agent_impl.h"
 #include "third_party/blink/renderer/core/dom/document.h"
 #include "third_party/blink/renderer/core/dom/events/event.h"
 #include "third_party/blink/renderer/core/editing/bidi_adjustment.h"
@@ -43,12 +44,12 @@
 #include "third_party/blink/renderer/core/editing/frame_selection.h"
 #include "third_party/blink/renderer/core/editing/iterators/text_iterator.h"
 #include "third_party/blink/renderer/core/editing/markers/document_marker_controller.h"
+#include "third_party/blink/renderer/core/editing/position_iterator.h"
 #include "third_party/blink/renderer/core/editing/selection_template.h"
 #include "third_party/blink/renderer/core/editing/set_selection_options.h"
 #include "third_party/blink/renderer/core/editing/spellcheck/spell_checker.h"
 #include "third_party/blink/renderer/core/editing/suggestion/text_suggestion_controller.h"
 #include "third_party/blink/renderer/core/editing/visible_position.h"
-#include "third_party/blink/renderer/core/fragment_directive/text_fragment_handler.h"
 #include "third_party/blink/renderer/core/frame/local_dom_window.h"
 #include "third_party/blink/renderer/core/frame/local_frame.h"
 #include "third_party/blink/renderer/core/frame/local_frame_client.h"
@@ -1353,8 +1354,9 @@
 
   // Opening a context menu from an existing text fragment/highlight should not
   // select additional text.
-  if (TextFragmentHandler::IsOverTextFragment(hit_test_result))
+  if (AnnotationAgentImpl::IsOverAnnotation(hit_test_result)) {
     return;
+  }
 
   // Opening the context menu, triggered by long press or keyboard, should not
   // change the selected text.
diff --git a/third_party/blink/renderer/core/exported/web_media_player_impl_unittest.cc b/third_party/blink/renderer/core/exported/web_media_player_impl_unittest.cc
index 7dc3616..879b4c7 100644
--- a/third_party/blink/renderer/core/exported/web_media_player_impl_unittest.cc
+++ b/third_party/blink/renderer/core/exported/web_media_player_impl_unittest.cc
@@ -2796,6 +2796,19 @@
   EXPECT_TRUE(IsSuspended());
 }
 
+TEST_F(WebMediaPlayerImplTest, DominantPlayersAreNotCleanedUp) {
+  InitializeWebMediaPlayerImpl();
+  wmpi_->BecameDominantVisibleContent(true);
+  EXPECT_FALSE(delegate_.ExpireForTesting());
+}
+
+TEST_F(WebMediaPlayerImplTest, FullscreenPlayersAreNotCleanedUp) {
+  InitializeWebMediaPlayerImpl();
+  wmpi_->SetIsEffectivelyFullscreen(
+      WebFullscreenVideoStatus::kFullscreenAndPictureInPictureEnabled);
+  EXPECT_FALSE(delegate_.ExpireForTesting());
+}
+
 class WebMediaPlayerImplBackgroundBehaviorTest
     : public WebMediaPlayerImplTest,
       public WebAudioSourceProviderClient,
diff --git a/third_party/blink/renderer/core/fragment_directive/text_fragment_handler.cc b/third_party/blink/renderer/core/fragment_directive/text_fragment_handler.cc
index 2e98dd6..8bb337c 100644
--- a/third_party/blink/renderer/core/fragment_directive/text_fragment_handler.cc
+++ b/third_party/blink/renderer/core/fragment_directive/text_fragment_handler.cc
@@ -15,7 +15,6 @@
 #include "third_party/blink/renderer/core/dom/document.h"
 #include "third_party/blink/renderer/core/editing/markers/document_marker.h"
 #include "third_party/blink/renderer/core/editing/markers/document_marker_controller.h"
-#include "third_party/blink/renderer/core/editing/position_with_affinity.h"
 #include "third_party/blink/renderer/core/editing/range_in_flat_tree.h"
 #include "third_party/blink/renderer/core/editing/selection_editor.h"
 #include "third_party/blink/renderer/core/editing/visible_units.h"
@@ -24,7 +23,6 @@
 #include "third_party/blink/renderer/core/fragment_directive/text_fragment_selector_generator.h"
 #include "third_party/blink/renderer/core/frame/local_dom_window.h"
 #include "third_party/blink/renderer/core/frame/local_frame.h"
-#include "third_party/blink/renderer/core/layout/hit_test_result.h"
 #include "third_party/blink/renderer/core/loader/document_loader.h"
 
 namespace blink {
@@ -108,28 +106,6 @@
   GetFrame()->View()->ClearFragmentAnchor();
 }
 
-// static
-bool TextFragmentHandler::IsOverTextFragment(const HitTestResult& result) {
-  if (!result.InnerNode() || !result.InnerNodeFrame()) {
-    return false;
-  }
-
-  // Tree should be clean before accessing the position.
-  // |HitTestResult::GetPosition| calls |PositionForPoint()| which requires
-  // |kPrePaintClean|.
-  DCHECK_GE(result.InnerNodeFrame()->GetDocument()->Lifecycle().GetState(),
-            DocumentLifecycle::kPrePaintClean);
-
-  DocumentMarkerController& marker_controller =
-      result.InnerNodeFrame()->GetDocument()->Markers();
-  PositionWithAffinity pos_with_affinity = result.GetPosition();
-  const Position marker_position = pos_with_affinity.GetPosition();
-  auto markers = marker_controller.MarkersAroundPosition(
-      ToPositionInFlatTree(marker_position),
-      DocumentMarker::MarkerTypes::TextFragment());
-  return !markers.empty();
-}
-
 void TextFragmentHandler::ExtractTextFragmentsMatches(
     ExtractTextFragmentsMatchesCallback callback) {
   Vector<String> text_fragment_matches;
diff --git a/third_party/blink/renderer/core/fragment_directive/text_fragment_handler.h b/third_party/blink/renderer/core/fragment_directive/text_fragment_handler.h
index 42aeb0f..4e66798 100644
--- a/third_party/blink/renderer/core/fragment_directive/text_fragment_handler.h
+++ b/third_party/blink/renderer/core/fragment_directive/text_fragment_handler.h
@@ -34,9 +34,6 @@
   TextFragmentHandler(const TextFragmentHandler&) = delete;
   TextFragmentHandler& operator=(const TextFragmentHandler&) = delete;
 
-  // Determine if |result| represents a click on an existing highlight.
-  static bool IsOverTextFragment(const HitTestResult& result);
-
   // Called to notify the frame's TextFragmentHandler on context menu open over
   // a selection. Will trigger preemptive generation if needed.
   static void OpenedContextMenuOverSelection(LocalFrame* frame);
diff --git a/third_party/blink/renderer/core/html/canvas/html_canvas_element.idl b/third_party/blink/renderer/core/html/canvas/html_canvas_element.idl
index 06d3214..489b1c80 100644
--- a/third_party/blink/renderer/core/html/canvas/html_canvas_element.idl
+++ b/third_party/blink/renderer/core/html/canvas/html_canvas_element.idl
@@ -36,7 +36,7 @@
     [RaisesException=Setter, CEReactions] attribute unsigned long width;
     [RaisesException=Setter, CEReactions] attribute unsigned long height;
 
-    [RuntimeEnabled=CanvasElementDrawElement] attribute boolean layoutSubtree;
+    [RuntimeEnabled=CanvasDrawElement] attribute boolean layoutSubtree;
 
     [HighEntropy, MeasureAs=CanvasToDataURL, RaisesException] DOMString toDataURL(optional DOMString type = "image/png", optional any quality);
 
diff --git a/third_party/blink/renderer/core/html/html_element.cc b/third_party/blink/renderer/core/html/html_element.cc
index a78d967..3d0f6731 100644
--- a/third_party/blink/renderer/core/html/html_element.cc
+++ b/third_party/blink/renderer/core/html/html_element.cc
@@ -1599,6 +1599,10 @@
     GetPopoverData()->setCloseWatcher(close_watcher);
   }
 
+  if (!IsInUserAgentShadowRoot()) {
+    // Don't count things like customizable-`<select>`'s use of a popover.
+    UseCounter::Count(GetDocument(), WebFeature::kPopoverShown);
+  }
   MarkPopoverInvokersDirty(*this);
   GetPopoverData()->setPreviouslyFocusedElement(nullptr);
   Element* originally_focused_element = original_document.FocusedElement();
diff --git a/third_party/blink/renderer/core/input/gesture_manager.cc b/third_party/blink/renderer/core/input/gesture_manager.cc
index c0d8fc2..dcbd200 100644
--- a/third_party/blink/renderer/core/input/gesture_manager.cc
+++ b/third_party/blink/renderer/core/input/gesture_manager.cc
@@ -8,12 +8,12 @@
 #include "third_party/blink/public/common/input/web_pointer_event.h"
 #include "third_party/blink/public/mojom/frame/user_activation_notification_type.mojom-blink.h"
 #include "third_party/blink/public/public_buildflags.h"
+#include "third_party/blink/renderer/core/annotation/annotation_agent_impl.h"
 #include "third_party/blink/renderer/core/dom/document.h"
 #include "third_party/blink/renderer/core/editing/selection_controller.h"
 #include "third_party/blink/renderer/core/event_type_names.h"
 #include "third_party/blink/renderer/core/events/gesture_event.h"
 #include "third_party/blink/renderer/core/events/pointer_event_factory.h"
-#include "third_party/blink/renderer/core/fragment_directive/text_fragment_handler.h"
 #include "third_party/blink/renderer/core/frame/local_dom_window.h"
 #include "third_party/blink/renderer/core/frame/local_frame.h"
 #include "third_party/blink/renderer/core/frame/local_frame_view.h"
@@ -379,7 +379,8 @@
         ->UpdateAllLifecyclePhasesExceptPaint(DocumentUpdateReason::kHitTest);
     current_hit_test = event_handling_util::HitTestResultInFrame(
         frame_, HitTestLocation(adjusted_point), hit_type);
-    if (TextFragmentHandler::IsOverTextFragment(current_hit_test) &&
+    if (AnnotationAgentImpl::IsOverAnnotation(current_hit_test) ==
+            mojom::blink::AnnotationType::kSharedHighlight &&
         event_result == WebInputEventResult::kNotHandled) {
       return SendContextMenuEventForGesture(targeted_event);
     }
diff --git a/third_party/blink/renderer/core/layout/block_layout_algorithm.cc b/third_party/blink/renderer/core/layout/block_layout_algorithm.cc
index 1b4d640..04baa63c 100644
--- a/third_party/blink/renderer/core/layout/block_layout_algorithm.cc
+++ b/third_party/blink/renderer/core/layout/block_layout_algorithm.cc
@@ -42,6 +42,7 @@
 #include "third_party/blink/renderer/core/mathml_names.h"
 #include "third_party/blink/renderer/core/style/computed_style.h"
 #include "third_party/blink/renderer/platform/heap/collection_support/clear_collection_scope.h"
+#include "third_party/blink/renderer/platform/runtime_enabled_features.h"
 
 namespace blink {
 namespace {
@@ -66,9 +67,17 @@
     return block_flow->HasLineIfEmpty() &&
            InlineNode(block_flow).IsBlockLevel();
   }
-  if (const auto* const flow_thread = block_flow->MultiColumnFlowThread()) {
-    DCHECK(!flow_thread->ChildrenInline());
-    for (const auto* child = flow_thread->FirstChild(); child;
+  const LayoutBlockFlow* fragmentation_context_root = nullptr;
+  if (RuntimeEnabledFeatures::FlowThreadLessEnabled()) {
+    if (block_flow->IsMulticolContainer()) {
+      fragmentation_context_root = block_flow;
+    }
+  } else {
+    fragmentation_context_root = block_flow->MultiColumnFlowThread();
+  }
+  if (fragmentation_context_root) {
+    DCHECK(!fragmentation_context_root->ChildrenInline());
+    for (const auto* child = fragmentation_context_root->FirstChild(); child;
          child = child->NextSibling()) {
       if (child->IsInline()) {
         // Note: |LayoutOutsideListMarker| is out-of-flow for the tree
diff --git a/third_party/blink/renderer/core/layout/block_layout_algorithm.h b/third_party/blink/renderer/core/layout/block_layout_algorithm.h
index 86c1313..5a16873d 100644
--- a/third_party/blink/renderer/core/layout/block_layout_algorithm.h
+++ b/third_party/blink/renderer/core/layout/block_layout_algorithm.h
@@ -521,7 +521,7 @@
     }
     // Ensure we're really a column box. We can't use |BoxType| to call this
     // from the constructor.
-    DCHECK(node_.GetLayoutBox()->SlowFirstChild()->IsLayoutFlowThread());
+    DCHECK(node_.GetLayoutBox()->IsMulticolContainer());
     return false;
   }
 
diff --git a/third_party/blink/renderer/core/layout/block_node.cc b/third_party/blink/renderer/core/layout/block_node.cc
index ecf9274..093d020 100644
--- a/third_party/blink/renderer/core/layout/block_node.cc
+++ b/third_party/blink/renderer/core/layout/block_node.cc
@@ -111,10 +111,6 @@
   return block_flow->MultiColumnFlowThread();
 }
 
-inline LayoutMultiColumnFlowThread* GetFlowThread(const LayoutBox& box) {
-  return GetFlowThread(DynamicTo<LayoutBlockFlow>(box));
-}
-
 // The entire purpose of this function is to avoid allocating space on the stack
 // for all layout algorithms for each node we lay out. Therefore it must not be
 // inline.
@@ -196,12 +192,7 @@
     CreateAlgorithmAndRun<FieldsetLayoutAlgorithm>(params, callback);
   } else if (box.IsFrameSet()) {
     CreateAlgorithmAndRun<FrameSetLayoutAlgorithm>(params, callback);
-  }
-  // If there's a legacy layout box, we can only do block fragmentation if
-  // we would have done block fragmentation with the legacy engine.
-  // Otherwise writing data back into the legacy tree will fail. Look for
-  // the flow thread.
-  else if (GetFlowThread(box) && params.node.Style().SpecifiesColumns()) {
+  } else if (box.IsMulticolContainer()) {
     CreateAlgorithmAndRun<ColumnLayoutAlgorithm>(params, callback);
   } else if (!box.Parent() && params.node.IsPaginatedRoot()) [[unlikely]] {
     CreateAlgorithmAndRun<PaginatedRootLayoutAlgorithm>(params, callback);
@@ -1433,6 +1424,9 @@
 }
 
 void BlockNode::MakeRoomForExtraColumns(LayoutUnit block_size) const {
+  if (RuntimeEnabledFeatures::FlowThreadLessEnabled()) {
+    return;
+  }
   auto* block_flow = DynamicTo<LayoutBlockFlow>(GetLayoutBox());
   DCHECK(block_flow && block_flow->MultiColumnFlowThread());
   MultiColumnFragmentainerGroup& last_group =
@@ -1860,6 +1854,9 @@
 }
 
 void BlockNode::StoreColumnCount(int count) {
+  if (RuntimeEnabledFeatures::FlowThreadLessEnabled()) {
+    return;
+  }
   LayoutMultiColumnFlowThread* flow_thread =
       To<LayoutBlockFlow>(box_.Get())->MultiColumnFlowThread();
   flow_thread->SetColumnCountFromNG(count);
diff --git a/third_party/blink/renderer/core/layout/forms/layout_text_control_inner_editor.cc b/third_party/blink/renderer/core/layout/forms/layout_text_control_inner_editor.cc
index 03a002d1..338bd05 100644
--- a/third_party/blink/renderer/core/layout/forms/layout_text_control_inner_editor.cc
+++ b/third_party/blink/renderer/core/layout/forms/layout_text_control_inner_editor.cc
@@ -62,6 +62,24 @@
 
   DCHECK(FirstChild());
   auto* before_parent = To<LayoutBlockFlow>(before_child->Parent());
+
+  if (!before_parent->IsAnonymous()) {
+    // `before_child` is the "holder" for TestRendering.
+    DCHECK_EQ(before_parent, this);
+    if (auto* previous = before_child->PreviousSibling()) {
+      DCHECK(previous->IsAnonymousBlockFlow());
+      auto* previous_last = previous->SlowLastChild();
+      if (!previous_last || !previous_last->IsBR()) {
+        previous->AddChild(new_child);
+        return;
+      }
+    }
+    auto* anonymous = LayoutBlockFlow::CreateAnonymous(&GetDocument(), Style());
+    LayoutBlockFlow::AddChild(anonymous, before_child);
+    anonymous->AddChild(new_child);
+    return;
+  }
+
   if (!new_child->IsBR()) {
     before_parent->AddChild(new_child, before_child);
     return;
diff --git a/third_party/blink/renderer/core/layout/forms/layout_text_control_inner_editor_test.cc b/third_party/blink/renderer/core/layout/forms/layout_text_control_inner_editor_test.cc
index 7cc7edb..8b993544 100644
--- a/third_party/blink/renderer/core/layout/forms/layout_text_control_inner_editor_test.cc
+++ b/third_party/blink/renderer/core/layout/forms/layout_text_control_inner_editor_test.cc
@@ -3,6 +3,7 @@
 // found in the LICENSE file.
 
 #include "third_party/blink/renderer/core/testing/core_unit_test_helper.h"
+#include "third_party/blink/renderer/platform/bindings/exception_state.h"
 
 namespace blink {
 
@@ -118,4 +119,19 @@
   EXPECT_FALSE(child->NextSibling());
 }
 
+// crbug.com/416795534
+TEST_F(LayoutTextControlInnerEditorTest, AddChildBeforeTestRenderingHolder) {
+  if (!RuntimeEnabledFeatures::TextareaMultipleIfcsEnabled()) {
+    return;
+  }
+  SetBodyInnerHTML("<textarea id=ta>A\n</textarea>");
+  GetElementById("ta")->Focus();
+  GetDocument().execCommand(
+      "inserthtml", false,
+      "<style> :first-letter { max-width: initial; }</style>",
+      ASSERT_NO_EXCEPTION);
+  UpdateAllLifecyclePhasesForTest();
+  // Pass if no crashes.
+}
+
 }  // namespace blink
diff --git a/third_party/blink/renderer/core/layout/fragment_builder.cc b/third_party/blink/renderer/core/layout/fragment_builder.cc
index 62681368..cbba6f8 100644
--- a/third_party/blink/renderer/core/layout/fragment_builder.cc
+++ b/third_party/blink/renderer/core/layout/fragment_builder.cc
@@ -552,7 +552,7 @@
 void FragmentBuilder::AddMulticolWithPendingOOFs(
     const BlockNode& multicol,
     MulticolWithPendingOofs<LogicalOffset>* multicol_info) {
-  DCHECK(To<LayoutBlockFlow>(multicol.GetLayoutBox())->MultiColumnFlowThread());
+  DCHECK(multicol.GetLayoutBox()->IsMulticolContainer());
   auto it = multicols_with_pending_oofs_.find(multicol.GetLayoutBox());
   if (it != multicols_with_pending_oofs_.end())
     return;
diff --git a/third_party/blink/renderer/core/layout/fragmentation_test.cc b/third_party/blink/renderer/core/layout/fragmentation_test.cc
index b539fdc5..2455f03 100644
--- a/third_party/blink/renderer/core/layout/fragmentation_test.cc
+++ b/third_party/blink/renderer/core/layout/fragmentation_test.cc
@@ -264,10 +264,6 @@
   )HTML");
   const auto* container =
       To<LayoutBlockFlow>(GetLayoutObjectByElementId("container"));
-  const auto* flow_thread = To<LayoutBlockFlow>(container->FirstChild());
-  DCHECK(flow_thread->IsLayoutFlowThread());
-  // |flow_thread| is in the stitched coordinate system.
-  // Legacy had (0, 0, 150, 30), but NG doesn't compute for |LayoutFlowThread|.
   EXPECT_EQ(container->VisualOverflowRect(), PhysicalRect(0, 0, 260, 15));
 }
 
diff --git a/third_party/blink/renderer/core/layout/layout_block.cc b/third_party/blink/renderer/core/layout/layout_block.cc
index 5f8744d..2bc5adb 100644
--- a/third_party/blink/renderer/core/layout/layout_block.cc
+++ b/third_party/blink/renderer/core/layout/layout_block.cc
@@ -299,9 +299,11 @@
   // and grids).
   child->MoveAllChildrenTo(this, child->NextSibling());
 
-  // Remove all the information in the flow thread associated with the leftover
-  // anonymous block.
-  child->RemoveFromLayoutFlowThread();
+  if (!RuntimeEnabledFeatures::FlowThreadLessEnabled()) {
+    // Remove all the information in the flow thread associated with the
+    // leftover anonymous block.
+    child->RemoveFromLayoutFlowThread();
+  }
 
   // Now remove the leftover anonymous block from the tree, and destroy it.
   // We'll rip it out manually from the tree before destroying it, because we
diff --git a/third_party/blink/renderer/core/layout/layout_block_flow.cc b/third_party/blink/renderer/core/layout/layout_block_flow.cc
index 3a8e663..49fdb948 100644
--- a/third_party/blink/renderer/core/layout/layout_block_flow.cc
+++ b/third_party/blink/renderer/core/layout/layout_block_flow.cc
@@ -70,6 +70,7 @@
 #include "third_party/blink/renderer/core/paint/paint_layer_scrollable_area.h"
 #include "third_party/blink/renderer/platform/heap/collection_support/clear_collection_scope.h"
 #include "third_party/blink/renderer/platform/instrumentation/use_counter.h"
+#include "third_party/blink/renderer/platform/runtime_enabled_features.h"
 #include "third_party/blink/renderer/platform/wtf/allocator/allocator.h"
 #include "third_party/blink/renderer/platform/wtf/size_assertions.h"
 
@@ -81,9 +82,14 @@
 // returned, and there are inline children, an anonymous block wrapper needs to
 // be created.
 bool AllowsInlineChildren(const LayoutBlockFlow& block) {
+  bool is_multicol;
+  if (RuntimeEnabledFeatures::FlowThreadLessEnabled()) {
+    is_multicol = block.IsMulticolContainer();
+  } else {
+    is_multicol = IsA<LayoutMultiColumnFlowThread>(block);
+  }
   const auto* inner_editor = DynamicTo<LayoutTextControlInnerEditor>(block);
-  return !IsA<LayoutMultiColumnFlowThread>(block) &&
-         !block.IsScrollMarkerGroup() &&
+  return !is_multicol && !block.IsScrollMarkerGroup() &&
          !(inner_editor && inner_editor->IsMultiline());
 }
 
@@ -709,12 +715,13 @@
   return true;
 }
 
-void LayoutBlockFlow::CreateOrDestroyMultiColumnFlowThreadIfNeeded(
-    const ComputedStyle* old_style) {
+// TODO(crbug.com/371802475): Remove the parameter.
+void LayoutBlockFlow::UpdateForMulticol(const ComputedStyle* old_style) {
   NOT_DESTROYED();
   bool specifies_columns = StyleRef().SpecifiesColumns();
 
   if (MultiColumnFlowThread()) {
+    DCHECK(!RuntimeEnabledFeatures::FlowThreadLessEnabled());
     DCHECK(old_style);
     if (specifies_columns != old_style->SpecifiesColumns()) {
       // If we're no longer to be multicol/paged, destroy the flow thread. Also
@@ -722,57 +729,91 @@
       // affects the column set structure (multicol containers may have
       // spanners, paged containers may not).
       MultiColumnFlowThread()->EvacuateAndDestroy();
+      SetIsMulticolContainer(false);
       DCHECK(!MultiColumnFlowThread());
     }
     return;
   }
 
-  if (!specifies_columns)
+  auto ShouldBeMulticol = [this]() -> bool {
+    if (!StyleRef().SpecifiesColumns() || !AllowsColumns()) {
+      return false;
+    }
+
+    // Multicol is applied to the anonymous content box child of a fieldset, not
+    // the fieldset itself, and the fieldset code will make sure that any
+    // relevant multicol properties are copied to said child.
+    if (IsFieldset()) {
+      return false;
+    }
+
+    // Form controls are replaced content (also when implemented as a regular
+    // block), and are therefore not supposed to support multicol.
+    const auto* element = DynamicTo<Element>(GetNode());
+    if (element && element->IsFormControlElement()) {
+      return false;
+    }
+
+    return true;
+  };
+
+  bool should_be_multicol = ShouldBeMulticol();
+  if (should_be_multicol == IsMulticolContainer()) {
     return;
+  }
+
+  SetIsMulticolContainer(should_be_multicol);
 
   if (IsListItem()) {
     UseCounter::Count(GetDocument(), WebFeature::kMultiColAndListItem);
   }
 
-  if (!AllowsColumns())
-    return;
+  if (!RuntimeEnabledFeatures::FlowThreadLessEnabled()) {
+    if (!should_be_multicol) {
+      return;
+    }
 
-  // Fieldsets look for a legend special child (layoutSpecialExcludedChild()).
-  // We currently only support one special child per layout object, and the
-  // flow thread would make for a second one.
-  // For LayoutNG, the multi-column display type will be applied to the
-  // anonymous content box. Thus, the flow thread should be added to the
-  // anonymous content box instead of the fieldset itself.
-  if (IsFieldset()) {
+    auto* flow_thread =
+        LayoutMultiColumnFlowThread::CreateAnonymous(GetDocument(), StyleRef());
+    AddChild(flow_thread);
+    if (IsLayoutNGObject()) {
+      // For simplicity of layout algorithm, we assume flow thread having block
+      // level children only.
+      // For example, we can handle them in same way:
+      //   <div style="columns:3">abc<br>def<br>ghi<br></div>
+      //   <div style="columns:3"><div>abc<br>def<br>ghi<br></div></div>
+      flow_thread->SetChildrenInline(false);
+    }
+
+    // Check that addChild() put the flow thread as a direct child, and didn't
+    // do fancy things.
+    DCHECK_EQ(flow_thread->Parent(), this);
+
+    flow_thread->Populate();
+
+    DCHECK(!multi_column_flow_thread_);
+    multi_column_flow_thread_ = flow_thread;
     return;
   }
 
-  // Form controls are replaced content (also when implemented as a regular
-  // block), and are therefore not supposed to support multicol.
-  const auto* element = DynamicTo<Element>(GetNode());
-  if (element && element->IsFormControlElement())
-    return;
-
-  auto* flow_thread =
-      LayoutMultiColumnFlowThread::CreateAnonymous(GetDocument(), StyleRef());
-  AddChild(flow_thread);
-  if (IsLayoutNGObject()) {
-    // For simplicity of layout algorithm, we assume flow thread having block
-    // level children only.
-    // For example, we can handle them in same way:
-    //   <div style="columns:3">abc<br>def<br>ghi<br></div>
-    //   <div style="columns:3"><div>abc<br>def<br>ghi<br></div></div>
-    flow_thread->SetChildrenInline(false);
+  // Descendants are inside multicol if this is now a multicol container, or if
+  // this ex-multicol container is inside an outer multicol container.
+  bool is_inside_multicol = should_be_multicol || IsInsideMulticol();
+  for (LayoutObject* child = FirstChild(); child;
+       child = child->NextSibling()) {
+    child->SetIsInsideMulticolIncludingDescendants(is_inside_multicol);
   }
 
-  // Check that addChild() put the flow thread as a direct child, and didn't do
-  // fancy things.
-  DCHECK_EQ(flow_thread->Parent(), this);
-
-  flow_thread->Populate();
-
-  DCHECK(!multi_column_flow_thread_);
-  multi_column_flow_thread_ = flow_thread;
+  if (should_be_multicol) {
+    // Inline children need to be wrapped inside an anonymous block. This
+    // anonymous block will participate in the fragmentation context established
+    // by `this`, whereas `this` (the multicol container itself) won't.
+    MakeChildrenNonInline();
+  } else {
+    // No longer a multicol, so no need to force anonymous blocks around all
+    // inline children.
+    MakeChildrenInlineIfPossible();
+  }
 }
 
 void LayoutBlockFlow::SetShouldDoFullPaintInvalidationForFirstLine() {
diff --git a/third_party/blink/renderer/core/layout/layout_block_flow.h b/third_party/blink/renderer/core/layout/layout_block_flow.h
index 83cd5bf..704b37f 100644
--- a/third_party/blink/renderer/core/layout/layout_block_flow.h
+++ b/third_party/blink/renderer/core/layout/layout_block_flow.h
@@ -97,13 +97,9 @@
 
   // Return true if this block establishes a fragmentation context root (e.g. a
   // multicol container).
-  //
-  // Implementation detail: At some point in the future there should be no flow
-  // threads. Callers that only want to know if this is a fragmentation context
-  // root (and don't depend on flow threads) should call this method.
   bool IsFragmentationContextRoot() const override {
     NOT_DESTROYED();
-    return MultiColumnFlowThread();
+    return IsMulticolContainer();
   }
 
   bool IsInitialLetterBox() const override;
@@ -173,8 +169,7 @@
   void DirtyLinesFromChangedChild(LayoutObject* child) final;
 
  private:
-  void CreateOrDestroyMultiColumnFlowThreadIfNeeded(
-      const ComputedStyle* old_style);
+  void UpdateForMulticol(const ComputedStyle* old_style);
 
   // Merge children of |sibling_that_may_be_deleted| into this object if
   // possible, and delete |sibling_that_may_be_deleted|. Returns true if we
diff --git a/third_party/blink/renderer/core/layout/layout_block_flow_hot.cc b/third_party/blink/renderer/core/layout/layout_block_flow_hot.cc
index d2199bc..d1a2dd5 100644
--- a/third_party/blink/renderer/core/layout/layout_block_flow_hot.cc
+++ b/third_party/blink/renderer/core/layout/layout_block_flow_hot.cc
@@ -30,7 +30,7 @@
     return true;
   }
 
-  if (RuntimeEnabledFeatures::CanvasElementDrawElementEnabled() &&
+  if (RuntimeEnabledFeatures::CanvasDrawElementEnabled() &&
       Parent()->IsCanvas()) {
     return true;
   }
@@ -65,8 +65,9 @@
   NOT_DESTROYED();
   LayoutBlock::StyleDidChange(diff, old_style);
 
-  if (diff.NeedsFullLayout() || !old_style)
-    CreateOrDestroyMultiColumnFlowThreadIfNeeded(old_style);
+  if (diff.NeedsFullLayout() || !old_style) {
+    UpdateForMulticol(old_style);
+  }
   if (old_style) {
     if (LayoutMultiColumnFlowThread* flow_thread = MultiColumnFlowThread()) {
       if (!StyleRef().ColumnRuleEquivalent(*old_style)) {
diff --git a/third_party/blink/renderer/core/layout/layout_box.cc b/third_party/blink/renderer/core/layout/layout_box.cc
index 49fa8f6..074ddc3f 100644
--- a/third_party/blink/renderer/core/layout/layout_box.cc
+++ b/third_party/blink/renderer/core/layout/layout_box.cc
@@ -2994,6 +2994,57 @@
   rare_data_->spanner_placeholder_ = nullptr;
 }
 
+bool LayoutBox::IsValidColumnSpanner() const {
+  NOT_DESTROYED();
+  // Note that this function may be called in many circumstances, such as before
+  // it is inserted into the tree, and even as part of calculating the
+  // containing block. Be careful.
+  DCHECK_EQ(StyleRef().GetColumnSpan(), EColumnSpan::kAll);
+  if (!RuntimeEnabledFeatures::FlowThreadLessEnabled()) {
+    return SpannerPlaceholder();
+  }
+
+  if (!Parent() || !IsInsideMulticol()) {
+    return false;
+  }
+
+  // The spec says that column-span only applies to in-flow block-level
+  // elements.
+  if (IsInline() || IsFloatingOrOutOfFlowPositioned()) {
+    return false;
+  }
+
+  // This looks like a spanner, but if we're inside something unbreakable or
+  // something that establishes a new formatting context, it's not to be treated
+  // as one.
+  for (const LayoutBox* ancestor = Parent()->EnclosingBox(); ancestor;
+       ancestor = ancestor->ContainingBlock()) {
+    if (ancestor->IsMulticolContainer()) {
+      return true;
+    }
+    const auto* ancestor_block_flow = DynamicTo<LayoutBlockFlow>(ancestor);
+    if (!ancestor_block_flow) {
+      // Needs to be in a block-flow container, and not e.g. a table.
+      return false;
+    }
+
+    // Make sure that there's nothing about this ancestor that prevents `this`
+    // from becoming a column spanner. We require the ancestor to participate in
+    // the block formatting context established by the multicol container
+    // (i.e. that there are no formatting contexts in-between). Transforms are
+    // also forbidden, since they insist on being in the containing block chain
+    // for everything inside, which will easily conflict with a spanners's need
+    // to have the multicol container as its direct containing block.
+    if (ancestor_block_flow->IsMonolithic() ||
+        ancestor_block_flow->CreatesNewFormattingContext() ||
+        ancestor_block_flow->CanContainFixedPositionObjects()) {
+      return false;
+    }
+    DCHECK(!ancestor->IsColumnSpanAll());
+  }
+  return false;
+}
+
 void LayoutBox::InflateVisualRectForFilterUnderContainer(
     TransformState& transform_state,
     const LayoutObject& container,
diff --git a/third_party/blink/renderer/core/layout/layout_box.h b/third_party/blink/renderer/core/layout/layout_box.h
index ece8679..73f354c 100644
--- a/third_party/blink/renderer/core/layout/layout_box.h
+++ b/third_party/blink/renderer/core/layout/layout_box.h
@@ -744,6 +744,8 @@
     return rare_data_ ? rare_data_->spanner_placeholder_.Get() : nullptr;
   }
 
+  bool IsValidColumnSpanner() const final;
+
   bool MapToVisualRectInAncestorSpaceInternal(
       const LayoutBoxModelObject* ancestor,
       TransformState&,
diff --git a/third_party/blink/renderer/core/layout/layout_box_model_object.cc b/third_party/blink/renderer/core/layout/layout_box_model_object.cc
index c067ca7..0a3a10c 100644
--- a/third_party/blink/renderer/core/layout/layout_box_model_object.cc
+++ b/third_party/blink/renderer/core/layout/layout_box_model_object.cc
@@ -136,7 +136,7 @@
     ObjectPaintInvalidator(*this).SlowSetPaintingLayerNeedsRepaint();
   }
 
-  if (Style()) {
+  if (!RuntimeEnabledFeatures::FlowThreadLessEnabled() && Style()) {
     LayoutFlowThread* flow_thread = FlowThreadContainingBlock();
     if (flow_thread && flow_thread != this) {
       flow_thread->FlowThreadDescendantStyleWillChange(this, diff, new_style);
@@ -235,9 +235,12 @@
   }
 
   if (old_style && Parent()) {
-    if (LayoutFlowThread* flow_thread = FlowThreadContainingBlock()) {
-      if (flow_thread != this) {
-        flow_thread->FlowThreadDescendantStyleDidChange(this, diff, *old_style);
+    if (!RuntimeEnabledFeatures::FlowThreadLessEnabled()) {
+      if (LayoutFlowThread* flow_thread = FlowThreadContainingBlock()) {
+        if (flow_thread != this) {
+          flow_thread->FlowThreadDescendantStyleDidChange(this, diff,
+                                                          *old_style);
+        }
       }
     }
 
diff --git a/third_party/blink/renderer/core/layout/layout_flow_thread.cc b/third_party/blink/renderer/core/layout/layout_flow_thread.cc
index 5a1aa602..958b334 100644
--- a/third_party/blink/renderer/core/layout/layout_flow_thread.cc
+++ b/third_party/blink/renderer/core/layout/layout_flow_thread.cc
@@ -37,7 +37,9 @@
 namespace blink {
 
 LayoutFlowThread::LayoutFlowThread()
-    : LayoutBlockFlow(nullptr), column_sets_invalidated_(false) {}
+    : LayoutBlockFlow(nullptr), column_sets_invalidated_(false) {
+  DCHECK(!RuntimeEnabledFeatures::FlowThreadLessEnabled());
+}
 
 void LayoutFlowThread::Trace(Visitor* visitor) const {
   visitor->Trace(multi_column_set_list_);
diff --git a/third_party/blink/renderer/core/layout/layout_html_canvas.h b/third_party/blink/renderer/core/layout/layout_html_canvas.h
index 3cc28ae..62683616 100644
--- a/third_party/blink/renderer/core/layout/layout_html_canvas.h
+++ b/third_party/blink/renderer/core/layout/layout_html_canvas.h
@@ -96,7 +96,7 @@
   }
   bool CanHaveChildren() const final {
     NOT_DESTROYED();
-    return RuntimeEnabledFeatures::CanvasElementDrawElementEnabled();
+    return RuntimeEnabledFeatures::CanvasDrawElementEnabled();
   }
   bool IsChildAllowed(LayoutObject*, const ComputedStyle&) const final;
 
diff --git a/third_party/blink/renderer/core/layout/layout_multi_column_flow_thread.cc b/third_party/blink/renderer/core/layout/layout_multi_column_flow_thread.cc
index d1b5de5..5b70064 100644
--- a/third_party/blink/renderer/core/layout/layout_multi_column_flow_thread.cc
+++ b/third_party/blink/renderer/core/layout/layout_multi_column_flow_thread.cc
@@ -49,6 +49,7 @@
     : last_set_worked_on_(nullptr),
       column_count_(1),
       is_being_evacuated_(false) {
+  DCHECK(!RuntimeEnabledFeatures::FlowThreadLessEnabled());
   SetIsInsideMulticol(true);
 }
 
diff --git a/third_party/blink/renderer/core/layout/layout_multi_column_flow_thread_test.cc b/third_party/blink/renderer/core/layout/layout_multi_column_flow_thread_test.cc
index f3e83b7..65cdc982 100644
--- a/third_party/blink/renderer/core/layout/layout_multi_column_flow_thread_test.cc
+++ b/third_party/blink/renderer/core/layout/layout_multi_column_flow_thread_test.cc
@@ -11,13 +11,18 @@
 #include "third_party/blink/renderer/core/layout/layout_multi_column_set.h"
 #include "third_party/blink/renderer/core/layout/layout_multi_column_spanner_placeholder.h"
 #include "third_party/blink/renderer/core/testing/core_unit_test_helper.h"
+#include "third_party/blink/renderer/platform/testing/runtime_enabled_features_test_helpers.h"
 #include "third_party/blink/renderer/platform/wtf/text/string_builder.h"
 
 namespace blink {
 
 namespace {
 
-class MultiColumnRenderingTest : public RenderingTest {
+class MultiColumnRenderingTest : public RenderingTest,
+                                 ScopedFlowThreadLessForTest {
+ public:
+  MultiColumnRenderingTest() : ScopedFlowThreadLessForTest(false) {}
+
  protected:
   LayoutMultiColumnFlowThread* FindFlowThread(const char* id) const;
 
diff --git a/third_party/blink/renderer/core/layout/layout_multi_column_set.cc b/third_party/blink/renderer/core/layout/layout_multi_column_set.cc
index 5d502c94..b6b810c 100644
--- a/third_party/blink/renderer/core/layout/layout_multi_column_set.cc
+++ b/third_party/blink/renderer/core/layout/layout_multi_column_set.cc
@@ -149,7 +149,9 @@
 LayoutMultiColumnSet::LayoutMultiColumnSet(LayoutFlowThread* flow_thread)
     : LayoutBlockFlow(nullptr),
       fragmentainer_groups_(*this),
-      flow_thread_(flow_thread) {}
+      flow_thread_(flow_thread) {
+  DCHECK(!RuntimeEnabledFeatures::FlowThreadLessEnabled());
+}
 
 LayoutMultiColumnSet* LayoutMultiColumnSet::CreateAnonymous(
     LayoutFlowThread& flow_thread,
diff --git a/third_party/blink/renderer/core/layout/layout_multi_column_spanner_placeholder.cc b/third_party/blink/renderer/core/layout/layout_multi_column_spanner_placeholder.cc
index 7230d74..ac5cd1a5 100644
--- a/third_party/blink/renderer/core/layout/layout_multi_column_spanner_placeholder.cc
+++ b/third_party/blink/renderer/core/layout/layout_multi_column_spanner_placeholder.cc
@@ -37,7 +37,9 @@
 LayoutMultiColumnSpannerPlaceholder::LayoutMultiColumnSpannerPlaceholder(
     LayoutBox* layout_object_in_flow_thread)
     : LayoutBox(nullptr),
-      layout_object_in_flow_thread_(layout_object_in_flow_thread) {}
+      layout_object_in_flow_thread_(layout_object_in_flow_thread) {
+  DCHECK(!RuntimeEnabledFeatures::FlowThreadLessEnabled());
+}
 
 void LayoutMultiColumnSpannerPlaceholder::Trace(Visitor* visitor) const {
   visitor->Trace(layout_object_in_flow_thread_);
diff --git a/third_party/blink/renderer/core/layout/layout_object.cc b/third_party/blink/renderer/core/layout/layout_object.cc
index e56570d..8ad6ba7 100644
--- a/third_party/blink/renderer/core/layout/layout_object.cc
+++ b/third_party/blink/renderer/core/layout/layout_object.cc
@@ -152,17 +152,40 @@
 
 namespace {
 
+LayoutObject* FindColumnSpannerContainer(
+    const LayoutObject* spanner,
+    LayoutObject::AncestorSkipInfo* skip_info) {
+  DCHECK(RuntimeEnabledFeatures::FlowThreadLessEnabled());
+  DCHECK(spanner->IsColumnSpanAll());
+  for (LayoutObject* walker = spanner->Parent(); walker;
+       walker = walker->ContainingBlock(skip_info)) {
+    if (walker->IsMulticolContainer()) {
+      return walker;
+    }
+    if (skip_info) {
+      skip_info->Update(*walker);
+    }
+  }
+  // It's possible to end up here, because this function is also called after
+  // the object has been taken out of the tree, during destruction.
+  return nullptr;
+}
+
 template <typename Predicate>
 LayoutObject* FindAncestorByPredicate(const LayoutObject* descendant,
                                       LayoutObject::AncestorSkipInfo* skip_info,
                                       Predicate predicate) {
-  for (auto* object = descendant->Parent(); object; object = object->Parent()) {
+  for (auto* object = descendant->Parent(); object;) {
     if (predicate(object))
       return object;
     if (skip_info)
       skip_info->Update(*object);
 
     if (object->IsColumnSpanAll()) [[unlikely]] {
+      if (RuntimeEnabledFeatures::FlowThreadLessEnabled()) {
+        object = FindColumnSpannerContainer(object, skip_info);
+        continue;
+      }
       // The containing block chain goes directly from the column spanner to the
       // multi-column container.
       const auto* multicol_container =
@@ -175,6 +198,7 @@
         }
       }
     }
+    object = object->Parent();
   }
   return nullptr;
 }
@@ -1189,10 +1213,13 @@
 PaintLayer* LayoutObject::PaintingLayer(int max_depth) const {
   NOT_DESTROYED();
   auto FindContainer = [](const LayoutObject& object) -> const LayoutObject* {
-    // Column spanners paint through their multicolumn containers which can
-    // be accessed through the associated out-of-flow placeholder's parent.
-    if (object.IsColumnSpanAll())
-      return object.SpannerPlaceholder();
+    if (object.IsColumnSpanAll()) {
+      // Column spanners paint through their multicolumn containers.
+      if (!RuntimeEnabledFeatures::FlowThreadLessEnabled()) {
+        return object.SpannerPlaceholder();
+      }
+      return object.ContainerForColumnSpanner();
+    }
     // Physical fragments and fragment items for ruby-text boxes are not
     // managed by inline parents, and stored in a separated line of the IFC.
     if (object.IsInlineRubyText()) {
@@ -1311,6 +1338,7 @@
 
 LayoutFlowThread* LayoutObject::LocateFlowThreadContainingBlock() const {
   NOT_DESTROYED();
+  DCHECK(!RuntimeEnabledFeatures::FlowThreadLessEnabled());
   DCHECK(IsInsideMulticol());
   return LayoutFlowThread::LocateFlowThreadContainingBlockOf(
       *this, LayoutFlowThread::kAnyAncestor);
@@ -1757,6 +1785,13 @@
   });
 }
 
+LayoutObject* LayoutObject::ContainerForColumnSpanner(
+    AncestorSkipInfo* skip_info) const {
+  NOT_DESTROYED();
+  DCHECK(RuntimeEnabledFeatures::FlowThreadLessEnabled());
+  return FindColumnSpannerContainer(this, skip_info);
+}
+
 LayoutBlock* LayoutObject::ContainingBlockForAbsolutePosition(
     AncestorSkipInfo* skip_info) const {
   NOT_DESTROYED();
@@ -2158,7 +2193,7 @@
   if (IsFloating()) {
     attributes.push_back("floating");
   }
-  if (SpannerPlaceholder()) {
+  if (IsColumnSpanAll()) {
     attributes.push_back("column spanner");
   }
   if (IsLayoutBlock() && IsInline()) {
@@ -3893,8 +3928,11 @@
   if (Parent()->ChildrenInline())
     Parent()->DirtyLinesFromChangedChild(this);
 
-  if (LayoutFlowThread* flow_thread = FlowThreadContainingBlock())
-    flow_thread->FlowThreadDescendantWasInserted(this);
+  if (!RuntimeEnabledFeatures::FlowThreadLessEnabled()) {
+    if (LayoutFlowThread* flow_thread = FlowThreadContainingBlock()) {
+      flow_thread->FlowThreadDescendantWasInserted(this);
+    }
+  }
 
   if (const Element* element = DynamicTo<Element>(GetNode());
       element && element->HasImplicitlyAnchoredElement()) {
@@ -3958,7 +3996,9 @@
   if (IsOutOfFlowPositioned() && Parent()->ChildrenInline())
     Parent()->DirtyLinesFromChangedChild(this);
 
-  RemoveFromLayoutFlowThread();
+  if (!RuntimeEnabledFeatures::FlowThreadLessEnabled()) {
+    RemoveFromLayoutFlowThread();
+  }
 
   if (bitfields_.IsScrollAnchorObject()) {
     // Clear the bit first so that anchor.clear() doesn't recurse into
@@ -4003,6 +4043,7 @@
 
 void LayoutObject::RemoveFromLayoutFlowThread() {
   NOT_DESTROYED();
+  DCHECK(!RuntimeEnabledFeatures::FlowThreadLessEnabled());
   if (!IsInsideMulticol()) {
     return;
   }
@@ -4028,6 +4069,7 @@
 void LayoutObject::RemoveFromLayoutFlowThreadRecursive(
     LayoutFlowThread* layout_flow_thread) {
   NOT_DESTROYED();
+  DCHECK(!RuntimeEnabledFeatures::FlowThreadLessEnabled());
   if (const LayoutObjectChildList* children = VirtualChildren()) {
     for (LayoutObject* child = children->FirstChild(); child;
          child = child->NextSibling()) {
diff --git a/third_party/blink/renderer/core/layout/layout_object.h b/third_party/blink/renderer/core/layout/layout_object.h
index cd0e0a7..ce13038 100644
--- a/third_party/blink/renderer/core/layout/layout_object.h
+++ b/third_party/blink/renderer/core/layout/layout_object.h
@@ -467,6 +467,7 @@
   // one. This function follows the containing block chain.
   LayoutFlowThread* FlowThreadContainingBlock() const {
     NOT_DESTROYED();
+    DCHECK(!RuntimeEnabledFeatures::FlowThreadLessEnabled());
     if (!IsInsideMulticol()) {
       return nullptr;
     }
@@ -757,12 +758,21 @@
     NOT_DESTROYED();
     parent_ = parent;
 
-    // Only update if our flow thread state is different from our new parent and
-    // if we're not a LayoutFlowThread.
-    // A LayoutFlowThread is always considered to be inside itself, so it never
-    // has to change its state in response to parent changes.
-    bool inside_multicol = parent && parent->IsInsideMulticol();
-    if (inside_multicol != IsInsideMulticol() && !IsLayoutFlowThread()) {
+    if (!RuntimeEnabledFeatures::FlowThreadLessEnabled()) {
+      // Only update if our flow thread state is different from our new parent
+      // and if we're not a LayoutFlowThread.  A LayoutFlowThread is always
+      // considered to be inside itself, so it never has to change its state in
+      // response to parent changes.
+      bool inside_multicol = parent && parent->IsInsideMulticol();
+      if (inside_multicol != IsInsideMulticol() && !IsLayoutFlowThread()) {
+        SetIsInsideMulticolIncludingDescendants(inside_multicol);
+      }
+      return;
+    }
+
+    bool inside_multicol =
+        parent && (parent->IsInsideMulticol() || parent->IsMulticolContainer());
+    if (inside_multicol != IsInsideMulticol()) {
       SetIsInsideMulticolIncludingDescendants(inside_multicol);
     }
   }
@@ -1743,10 +1753,22 @@
     NOT_DESTROYED();
     return nullptr;
   }
+
+  // Return true if this box is to be treated as a column spanner. This function
+  // assumes that `column-span` is `all`, but there are additional requirements
+  // for it to actually become a spanner. For one, it needs to be a block-level
+  // box that's inside a multicol container, and it also needs to be in the
+  // block formatting context established by the columns.
+  virtual bool IsValidColumnSpanner() const {
+    NOT_DESTROYED();
+    return false;
+  }
+
   bool IsColumnSpanAll() const {
     NOT_DESTROYED();
-    return StyleRef().GetColumnSpan() == EColumnSpan::kAll &&
-           SpannerPlaceholder();
+    // May be called before style is set.
+    return Style() && Style()->GetColumnSpan() == EColumnSpan::kAll &&
+           IsValidColumnSpanner();
   }
 
   // We include LayoutButton in this check, because buttons are
@@ -1836,6 +1858,8 @@
   LayoutObject* ContainerForAbsolutePosition(AncestorSkipInfo* = nullptr) const;
   // Finds the container as if this object is fixed-position.
   LayoutObject* ContainerForFixedPosition(AncestorSkipInfo* = nullptr) const;
+  // Finds the container as if this object is a column spanner.
+  LayoutObject* ContainerForColumnSpanner(AncestorSkipInfo* = nullptr) const;
 
   bool CanContainOutOfFlowPositionedElement(EPosition position) const {
     NOT_DESTROYED();
@@ -3392,6 +3416,15 @@
     bitfields_.SetHasSVGTextDescendants(b);
   }
 
+  bool IsMulticolContainer() const {
+    NOT_DESTROYED();
+    return bitfields_.IsMulticolContainer();
+  }
+  void SetIsMulticolContainer(bool b) {
+    NOT_DESTROYED();
+    bitfields_.SetIsMulticolContainer(b);
+  }
+
   // Returns true if this layout object is created for an element which will be
   // changing behaviour for overflow: visible.
   // See
@@ -3793,7 +3826,8 @@
           has_broken_spine_(false),
           has_valid_cached_geometry_(false),
           may_be_non_contiguous_ifc_(false),
-          has_svg_text_descendants_(false) {}
+          has_svg_text_descendants_(false),
+          is_multicol_container_(false) {}
 
     // Typically indicates that this object has had its style changed, and
     // requires a "full" layout.
@@ -4133,6 +4167,9 @@
     // For LayoutBlock - true if this block has *any* SVG text descendants.
     // Used for invalidation on transform changes.
     ADD_BOOLEAN_BITFIELD(has_svg_text_descendants_, HasSVGTextDescendants);
+
+    // True if this is a LayoutBlockFlow that establishes a multicol container.
+    ADD_BOOLEAN_BITFIELD(is_multicol_container_, IsMulticolContainer);
   };
 
 #undef ADD_BOOLEAN_BITFIELD
diff --git a/third_party/blink/renderer/core/layout/layout_object_hot.cc b/third_party/blink/renderer/core/layout/layout_object_hot.cc
index 35c7a52..e6cd137 100644
--- a/third_party/blink/renderer/core/layout/layout_object_hot.cc
+++ b/third_party/blink/renderer/core/layout/layout_object_hot.cc
@@ -46,6 +46,9 @@
   }
 
   if (IsColumnSpanAll()) {
+    if (RuntimeEnabledFeatures::FlowThreadLessEnabled()) {
+      return ContainerForColumnSpanner(skip_info);
+    }
     LayoutObject* multicol_container = SpannerPlaceholder()->Container();
     if (skip_info) {
       // We jumped directly from the spanner to the multicol container. Need to
@@ -224,7 +227,11 @@
   }
   LayoutObject* object;
   if (IsColumnSpanAll()) {
-    object = SpannerPlaceholder()->ContainingBlock();
+    if (RuntimeEnabledFeatures::FlowThreadLessEnabled()) {
+      object = ContainerForColumnSpanner(skip_info);
+    } else {
+      object = SpannerPlaceholder()->ContainingBlock();
+    }
   } else {
     object = Parent();
     if (!object && IsLayoutCustomScrollbarPart()) {
diff --git a/third_party/blink/renderer/core/layout/multi_column_fragmentainer_group.cc b/third_party/blink/renderer/core/layout/multi_column_fragmentainer_group.cc
index f8b6bcc..acccc21 100644
--- a/third_party/blink/renderer/core/layout/multi_column_fragmentainer_group.cc
+++ b/third_party/blink/renderer/core/layout/multi_column_fragmentainer_group.cc
@@ -7,6 +7,7 @@
 #include "third_party/blink/renderer/core/layout/geometry/logical_rect.h"
 #include "third_party/blink/renderer/core/layout/geometry/writing_mode_converter.h"
 #include "third_party/blink/renderer/core/layout/layout_multi_column_set.h"
+#include "third_party/blink/renderer/platform/runtime_enabled_features.h"
 
 namespace blink {
 
@@ -21,7 +22,9 @@
 
 MultiColumnFragmentainerGroup::MultiColumnFragmentainerGroup(
     const LayoutMultiColumnSet& column_set)
-    : column_set_(&column_set) {}
+    : column_set_(&column_set) {
+  DCHECK(!RuntimeEnabledFeatures::FlowThreadLessEnabled());
+}
 
 LogicalOffset MultiColumnFragmentainerGroup::OffsetFromColumnSet() const {
   return LogicalOffset(LayoutUnit(), LogicalTop());
diff --git a/third_party/blink/renderer/core/layout/multi_column_fragmentainer_group_test.cc b/third_party/blink/renderer/core/layout/multi_column_fragmentainer_group_test.cc
index cb499fb..27c14a6 100644
--- a/third_party/blink/renderer/core/layout/multi_column_fragmentainer_group_test.cc
+++ b/third_party/blink/renderer/core/layout/multi_column_fragmentainer_group_test.cc
@@ -9,15 +9,19 @@
 #include "third_party/blink/renderer/core/layout/layout_multi_column_flow_thread.h"
 #include "third_party/blink/renderer/core/layout/layout_multi_column_set.h"
 #include "third_party/blink/renderer/core/testing/core_unit_test_helper.h"
+#include "third_party/blink/renderer/platform/testing/runtime_enabled_features_test_helpers.h"
 
 namespace blink {
 
 namespace {
 
-class MultiColumnFragmentainerGroupTest : public RenderingTest {
+class MultiColumnFragmentainerGroupTest : public RenderingTest,
+                                          public ScopedFlowThreadLessForTest {
  public:
   MultiColumnFragmentainerGroupTest()
-      : flow_thread_(nullptr), column_set_(nullptr) {}
+      : ScopedFlowThreadLessForTest(false),
+        flow_thread_(nullptr),
+        column_set_(nullptr) {}
 
  protected:
   void SetUp() override;
diff --git a/third_party/blink/renderer/core/layout/replaced_layout_algorithm.cc b/third_party/blink/renderer/core/layout/replaced_layout_algorithm.cc
index 981c1da..04d1e1d1 100644
--- a/third_party/blink/renderer/core/layout/replaced_layout_algorithm.cc
+++ b/third_party/blink/renderer/core/layout/replaced_layout_algorithm.cc
@@ -27,8 +27,7 @@
     LayoutMediaChildren();
   }
 
-  if (Node().IsCanvas() &&
-      RuntimeEnabledFeatures::CanvasElementDrawElementEnabled()) {
+  if (Node().IsCanvas() && RuntimeEnabledFeatures::CanvasDrawElementEnabled()) {
     LayoutCanvasChildren();
   }
 
diff --git a/third_party/blink/renderer/core/loader/modulescript/document_module_script_fetcher.cc b/third_party/blink/renderer/core/loader/modulescript/document_module_script_fetcher.cc
index 665254f..eb18f62 100644
--- a/third_party/blink/renderer/core/loader/modulescript/document_module_script_fetcher.cc
+++ b/third_party/blink/renderer/core/loader/modulescript/document_module_script_fetcher.cc
@@ -31,12 +31,14 @@
     ModuleType expected_module_type,
     ResourceFetcher* fetch_client_settings_object_fetcher,
     ModuleGraphLevel level,
-    ModuleScriptFetcher::Client* client) {
+    ModuleScriptFetcher::Client* client,
+    ModuleImportPhase import_phase) {
   DCHECK_EQ(fetch_params.GetScriptType(), mojom::blink::ScriptType::kModule);
   DCHECK(fetch_client_settings_object_fetcher);
   DCHECK(!client_);
   client_ = client;
   expected_module_type_ = expected_module_type;
+  import_phase_ = import_phase;
   // Streaming can currently only be triggered from the main thread. This
   // currently happens only for dynamic imports in worker modules.
   ScriptResource::StreamingAllowed streaming_allowed =
@@ -105,7 +107,7 @@
       /*source_url=*/url, /*base_url=*/url,
       ScriptSourceLocationType::kExternalFile, resolved_module_type.value(),
       script_resource->SourceText(), script_resource->CacheHandler(),
-      response_referrer_policy, streamer, not_streamed_reason));
+      response_referrer_policy, streamer, not_streamed_reason, import_phase_));
 }
 
 void DocumentModuleScriptFetcher::Trace(Visitor* visitor) const {
diff --git a/third_party/blink/renderer/core/loader/modulescript/document_module_script_fetcher.h b/third_party/blink/renderer/core/loader/modulescript/document_module_script_fetcher.h
index 8728457..a27f75b 100644
--- a/third_party/blink/renderer/core/loader/modulescript/document_module_script_fetcher.h
+++ b/third_party/blink/renderer/core/loader/modulescript/document_module_script_fetcher.h
@@ -30,7 +30,8 @@
              ModuleType,
              ResourceFetcher*,
              ModuleGraphLevel,
-             Client*) override;
+             Client*,
+             ModuleImportPhase) override;
 
   // Implements ResourceClient
   void NotifyFinished(Resource*) override;
@@ -41,6 +42,7 @@
  private:
   Member<Client> client_;
   ModuleType expected_module_type_;
+  ModuleImportPhase import_phase_;
 };
 
 }  // namespace blink
diff --git a/third_party/blink/renderer/core/loader/modulescript/installed_service_worker_module_script_fetcher.cc b/third_party/blink/renderer/core/loader/modulescript/installed_service_worker_module_script_fetcher.cc
index 48b5e9f..838e2d8 100644
--- a/third_party/blink/renderer/core/loader/modulescript/installed_service_worker_module_script_fetcher.cc
+++ b/third_party/blink/renderer/core/loader/modulescript/installed_service_worker_module_script_fetcher.cc
@@ -30,7 +30,8 @@
     ModuleType expected_module_type,
     ResourceFetcher*,
     ModuleGraphLevel level,
-    ModuleScriptFetcher::Client* client) {
+    ModuleScriptFetcher::Client* client,
+    ModuleImportPhase import_phase) {
   DCHECK_EQ(fetch_params.GetScriptType(), mojom::blink::ScriptType::kModule);
   DCHECK(global_scope_->IsContextThread());
   auto* installed_scripts_manager = global_scope_->GetInstalledScriptsManager();
diff --git a/third_party/blink/renderer/core/loader/modulescript/installed_service_worker_module_script_fetcher.h b/third_party/blink/renderer/core/loader/modulescript/installed_service_worker_module_script_fetcher.h
index e7dba50c..1834b077 100644
--- a/third_party/blink/renderer/core/loader/modulescript/installed_service_worker_module_script_fetcher.h
+++ b/third_party/blink/renderer/core/loader/modulescript/installed_service_worker_module_script_fetcher.h
@@ -27,7 +27,8 @@
              ModuleType,
              ResourceFetcher*,
              ModuleGraphLevel,
-             ModuleScriptFetcher::Client*) override;
+             ModuleScriptFetcher::Client*,
+             ModuleImportPhase) override;
 
   void Trace(Visitor*) const override;
 
diff --git a/third_party/blink/renderer/core/loader/modulescript/module_script_creation_params.h b/third_party/blink/renderer/core/loader/modulescript/module_script_creation_params.h
index 951c6e85..0c8a640 100644
--- a/third_party/blink/renderer/core/loader/modulescript/module_script_creation_params.h
+++ b/third_party/blink/renderer/core/loader/modulescript/module_script_creation_params.h
@@ -17,9 +17,12 @@
 #include "third_party/blink/renderer/platform/weborigin/kurl.h"
 #include "third_party/blink/renderer/platform/wtf/cross_thread_copier.h"
 #include "third_party/blink/renderer/platform/wtf/text/wtf_string.h"
+#include "v8/include/v8-callbacks.h"
 
 namespace blink {
 
+typedef v8::ModuleImportPhase ModuleImportPhase;
+
 // Spec module types. Return value of
 // "https://html.spec.whatwg.org/#module-type-from-module-request".
 enum class ModuleType { kInvalid, kJavaScriptOrWasm, kJSON, kCSS };
@@ -47,7 +50,8 @@
       network::mojom::ReferrerPolicy response_referrer_policy,
       ScriptStreamer* script_streamer = nullptr,
       ScriptStreamer::NotStreamingReason not_streaming_reason =
-          ScriptStreamer::NotStreamingReason::kStreamingDisabled)
+          ScriptStreamer::NotStreamingReason::kStreamingDisabled,
+      ModuleImportPhase import_phase = ModuleImportPhase::kEvaluation)
       : source_url_(source_url),
         base_url_(base_url),
         source_location_type_(source_location_type),
@@ -58,7 +62,8 @@
         cache_handler_(cache_handler),
         response_referrer_policy_(response_referrer_policy),
         script_streamer_(script_streamer),
-        not_streaming_reason_(not_streaming_reason) {
+        not_streaming_reason_(not_streaming_reason),
+        import_phase_(import_phase) {
     DCHECK(source_location_type == ScriptSourceLocationType::kExternalFile ||
            source_location_type == ScriptSourceLocationType::kInline);
     // https://html.spec.whatwg.org/multipage/webappapis.html#concept-script-base-url
@@ -78,10 +83,11 @@
                                       : GetSourceText().ToString();
     return ModuleScriptCreationParams(
         SourceURL(), BaseURL(), source_location_type_, GetModuleType(),
-        isolated_source_text, response_referrer_policy_);
+        isolated_source_text, response_referrer_policy_, import_phase_);
   }
 
   ResolvedModuleType GetModuleType() const { return module_type_; }
+  ModuleImportPhase GetModuleImportPhase() const { return import_phase_; }
 
   const KURL& SourceURL() const { return source_url_; }
   const KURL& BaseURL() const { return base_url_; }
@@ -130,7 +136,8 @@
       ScriptSourceLocationType source_location_type,
       const ResolvedModuleType& module_type,
       const String& isolated_source_text,
-      network::mojom::ReferrerPolicy response_referrer_policy)
+      network::mojom::ReferrerPolicy response_referrer_policy,
+      ModuleImportPhase import_phase)
       : source_url_(source_url),
         base_url_(base_url),
         source_location_type_(source_location_type),
@@ -145,7 +152,8 @@
         // passed across threads.
         script_streamer_(nullptr),
         not_streaming_reason_(
-            ScriptStreamer::NotStreamingReason::kStreamingDisabled) {}
+            ScriptStreamer::NotStreamingReason::kStreamingDisabled),
+        import_phase_(import_phase) {}
 
   const KURL source_url_;
   const KURL base_url_;
@@ -170,6 +178,8 @@
   // |script_streamer_| is cleared when crossing thread boundaries.
   Persistent<ScriptStreamer> script_streamer_;
   const ScriptStreamer::NotStreamingReason not_streaming_reason_;
+
+  const ModuleImportPhase import_phase_;
 };
 
 }  // namespace blink
diff --git a/third_party/blink/renderer/core/loader/modulescript/module_script_fetch_request.h b/third_party/blink/renderer/core/loader/modulescript/module_script_fetch_request.h
index 3d77885..15537991 100644
--- a/third_party/blink/renderer/core/loader/modulescript/module_script_fetch_request.h
+++ b/third_party/blink/renderer/core/loader/modulescript/module_script_fetch_request.h
@@ -30,26 +30,32 @@
                            network::mojom::RequestDestination destination,
                            const ScriptFetchOptions& options,
                            const String& referrer_string,
-                           const TextPosition& referrer_position)
+                           const TextPosition& referrer_position,
+                           const v8::ModuleImportPhase import_phase)
       : url_(url),
         expected_module_type_(module_type),
         context_type_(context_type),
         destination_(destination),
         options_(options),
         referrer_string_(referrer_string),
-        referrer_position_(referrer_position) {}
+        referrer_position_(referrer_position),
+        import_phase_(import_phase) {}
 
-  static ModuleScriptFetchRequest CreateForTest(const KURL& url,
-                                                ModuleType module_type) {
+  static ModuleScriptFetchRequest CreateForTest(
+      const KURL& url,
+      ModuleType module_type,
+      v8::ModuleImportPhase import_phase = v8::ModuleImportPhase::kEvaluation) {
     return ModuleScriptFetchRequest(
         url, module_type, mojom::blink::RequestContextType::SCRIPT,
         network::mojom::RequestDestination::kScript, ScriptFetchOptions(),
-        Referrer::ClientReferrerString(), TextPosition::MinimumPosition());
+        Referrer::ClientReferrerString(), TextPosition::MinimumPosition(),
+        import_phase);
   }
   ~ModuleScriptFetchRequest() = default;
 
   const KURL& Url() const { return url_; }
   ModuleType GetExpectedModuleType() const { return expected_module_type_; }
+  v8::ModuleImportPhase GetModuleImportPhase() const { return import_phase_; }
   mojom::blink::RequestContextType ContextType() const { return context_type_; }
   network::mojom::RequestDestination Destination() const {
     return destination_;
@@ -66,6 +72,7 @@
   const ScriptFetchOptions options_;
   const String referrer_string_;
   const TextPosition referrer_position_;
+  const v8::ModuleImportPhase import_phase_;
 };
 
 }  // namespace blink
diff --git a/third_party/blink/renderer/core/loader/modulescript/module_script_fetcher.h b/third_party/blink/renderer/core/loader/modulescript/module_script_fetcher.h
index 8d975b3c..01871d1 100644
--- a/third_party/blink/renderer/core/loader/modulescript/module_script_fetcher.h
+++ b/third_party/blink/renderer/core/loader/modulescript/module_script_fetcher.h
@@ -48,7 +48,8 @@
                      ModuleType,
                      ResourceFetcher*,
                      ModuleGraphLevel,
-                     Client*) = 0;
+                     Client*,
+                     ModuleImportPhase) = 0;
 
   void Trace(Visitor*) const override;
 
diff --git a/third_party/blink/renderer/core/loader/modulescript/module_script_loader.cc b/third_party/blink/renderer/core/loader/modulescript/module_script_loader.cc
index 459559db..1a18975 100644
--- a/third_party/blink/renderer/core/loader/modulescript/module_script_loader.cc
+++ b/third_party/blink/renderer/core/loader/modulescript/module_script_loader.cc
@@ -57,7 +57,8 @@
 }
 #endif
 
-void ModuleScriptLoader::AdvanceState(ModuleScriptLoader::State new_state) {
+void ModuleScriptLoader::AdvanceState(ModuleScriptLoader::State new_state,
+                                      ModuleImportPhase load_type) {
   switch (state_) {
     case State::kInitial:
       DCHECK_EQ(new_state, State::kFetching);
@@ -78,7 +79,7 @@
 
   if (state_ == State::kFinished) {
     registry_->ReleaseFinishedLoader(this);
-    client_->NotifyNewSingleModuleFinished(module_script_);
+    client_->NotifyNewSingleModuleFinished(module_script_, load_type);
   }
 }
 
@@ -274,7 +275,8 @@
   module_fetcher_ =
       modulator_->CreateModuleScriptFetcher(custom_fetch_type, PassKey());
   module_fetcher_->Fetch(fetch_params, module_request.GetExpectedModuleType(),
-                         fetch_client_settings_object_fetcher, level, this);
+                         fetch_client_settings_object_fetcher, level, this,
+                         module_request.GetModuleImportPhase());
 }
 
 // <specdef href="https://html.spec.whatwg.org/C/#fetch-a-single-module-script">
@@ -353,7 +355,7 @@
       break;
   }
 
-  AdvanceState(State::kFinished);
+  AdvanceState(State::kFinished, params.GetModuleImportPhase());
 }
 
 void ModuleScriptLoader::Trace(Visitor* visitor) const {
diff --git a/third_party/blink/renderer/core/loader/modulescript/module_script_loader.h b/third_party/blink/renderer/core/loader/modulescript/module_script_loader.h
index 69839be..22d2acf 100644
--- a/third_party/blink/renderer/core/loader/modulescript/module_script_loader.h
+++ b/third_party/blink/renderer/core/loader/modulescript/module_script_loader.h
@@ -77,7 +77,9 @@
                      ModuleGraphLevel,
                      ModuleScriptCustomFetchType);
 
-  void AdvanceState(State new_state);
+  void AdvanceState(
+      State new_state,
+      ModuleImportPhase import_phase = ModuleImportPhase::kEvaluation);
 
   using PassKey = base::PassKey<ModuleScriptLoader>;
   // PassKey should be private and cannot be accessed from outside, but allow
diff --git a/third_party/blink/renderer/core/loader/modulescript/module_script_loader_client.h b/third_party/blink/renderer/core/loader/modulescript/module_script_loader_client.h
index 900347aa..2cba67f 100644
--- a/third_party/blink/renderer/core/loader/modulescript/module_script_loader_client.h
+++ b/third_party/blink/renderer/core/loader/modulescript/module_script_loader_client.h
@@ -7,6 +7,10 @@
 
 #include "third_party/blink/renderer/platform/heap/garbage_collected.h"
 
+namespace v8 {
+enum class ModuleImportPhase;
+}
+
 namespace blink {
 
 class ModuleScript;
@@ -23,7 +27,8 @@
   friend class ModuleScriptLoader;
   friend class ModuleMapTestModulator;
 
-  virtual void NotifyNewSingleModuleFinished(ModuleScript*) = 0;
+  virtual void NotifyNewSingleModuleFinished(ModuleScript*,
+                                             v8::ModuleImportPhase) = 0;
 };
 
 }  // namespace blink
diff --git a/third_party/blink/renderer/core/loader/modulescript/module_script_loader_test.cc b/third_party/blink/renderer/core/loader/modulescript/module_script_loader_test.cc
index 61e84ab4..2c0b288 100644
--- a/third_party/blink/renderer/core/loader/modulescript/module_script_loader_test.cc
+++ b/third_party/blink/renderer/core/loader/modulescript/module_script_loader_test.cc
@@ -60,17 +60,21 @@
     visitor->Trace(module_script_);
   }
 
-  void NotifyNewSingleModuleFinished(ModuleScript* module_script) override {
+  void NotifyNewSingleModuleFinished(ModuleScript* module_script,
+                                     ModuleImportPhase import_phase) override {
     was_notify_finished_ = true;
     module_script_ = module_script;
+    import_phase_ = import_phase;
   }
 
   bool WasNotifyFinished() const { return was_notify_finished_; }
   ModuleScript* GetModuleScript() { return module_script_.Get(); }
+  ModuleImportPhase GetModuleImportPhase() const { return import_phase_; }
 
  private:
   bool was_notify_finished_ = false;
   Member<ModuleScript> module_script_;
+  ModuleImportPhase import_phase_;
 };
 
 class ModuleScriptLoaderTestModulator final : public DummyModulator {
@@ -132,11 +136,13 @@
                             TestModuleScriptLoaderClient*);
   void TestFetchInvalidURL(ModuleScriptCustomFetchType,
                            TestModuleScriptLoaderClient*);
-  void TestFetchURL(ModuleScriptCustomFetchType,
-                    TestModuleScriptLoaderClient*,
-                    const char*,
-                    const char*,
-                    const char*);
+  void TestFetchURL(
+      ModuleScriptCustomFetchType,
+      TestModuleScriptLoaderClient*,
+      const char*,
+      const char*,
+      const char*,
+      ModuleImportPhase import_phase = ModuleImportPhase::kEvaluation);
   void TestFetchDataURLJSONModule(ModuleScriptCustomFetchType custom_fetch_type,
                                   TestModuleScriptLoaderClient* client);
   void TestFetchDataURLInvalidJSONModule(
@@ -507,18 +513,19 @@
     TestModuleScriptLoaderClient* client,
     const char* url_string,
     const char* module_name,
-    const char* mime_type) {
+    const char* mime_type,
+    ModuleImportPhase import_phase) {
   KURL url(url_string);
   url_test_helpers::RegisterMockedURLLoad(
       url, test::CoreTestDataPath(module_name), WebString(mime_type),
       platform_->GetURLLoaderMockFactory());
 
   auto* registry = MakeGarbageCollected<ModuleScriptLoaderRegistry>();
-  ModuleScriptLoader::Fetch(ModuleScriptFetchRequest::CreateForTest(
-                                url, ModuleType::kJavaScriptOrWasm),
-                            fetcher_, ModuleGraphLevel::kTopLevelModuleFetch,
-                            GetModulator(), custom_fetch_type, registry,
-                            client);
+  ModuleScriptLoader::Fetch(
+      ModuleScriptFetchRequest::CreateForTest(
+          url, ModuleType::kJavaScriptOrWasm, import_phase),
+      fetcher_, ModuleGraphLevel::kTopLevelModuleFetch, GetModulator(),
+      custom_fetch_type, registry, client);
 }
 
 TEST_F(ModuleScriptLoaderTest, FetchURL) {
@@ -590,7 +597,8 @@
       MakeGarbageCollected<TestModuleScriptLoaderClient>();
   TestFetchURL(ModuleScriptCustomFetchType::kNone, client,
                "https://example.test/exported-names.wasm",
-               "exported-names.wasm", "text/javascript");
+               "exported-names.wasm", "text/javascript",
+               ModuleImportPhase::kSource);
 
   EXPECT_FALSE(client->WasNotifyFinished())
       << "ModuleScriptLoader unexpectedly finished synchronously.";
@@ -601,6 +609,7 @@
   ModuleScript* module_script = client->GetModuleScript();
   EXPECT_TRUE(module_script);
   EXPECT_TRUE(module_script->HasParseError());
+  EXPECT_EQ(client->GetModuleImportPhase(), ModuleImportPhase::kSource);
 }
 
 TEST_F(ModuleScriptLoaderTest, FetchWasmURLInvalidModule) {
@@ -613,7 +622,8 @@
       MakeGarbageCollected<TestModuleScriptLoaderClient>();
   TestFetchURL(ModuleScriptCustomFetchType::kNone, client,
                "https://example.test/invalid-module.wasm",
-               "invalid-module.wasm", "application/wasm");
+               "invalid-module.wasm", "application/wasm",
+               ModuleImportPhase::kSource);
 
   EXPECT_FALSE(client->WasNotifyFinished())
       << "ModuleScriptLoader unexpectedly finished synchronously.";
@@ -624,6 +634,7 @@
   ModuleScript* module_script = client->GetModuleScript();
   EXPECT_TRUE(module_script);
   EXPECT_TRUE(module_script->HasParseError());
+  EXPECT_EQ(client->GetModuleImportPhase(), ModuleImportPhase::kSource);
 }
 
 }  // namespace blink
diff --git a/third_party/blink/renderer/core/loader/modulescript/module_tree_linker.cc b/third_party/blink/renderer/core/loader/modulescript/module_tree_linker.cc
index 57de595a..8fc0d75 100644
--- a/third_party/blink/renderer/core/loader/modulescript/module_tree_linker.cc
+++ b/third_party/blink/renderer/core/loader/modulescript/module_tree_linker.cc
@@ -41,12 +41,17 @@
 struct ModuleScriptFetchTarget {
   ModuleScriptFetchTarget(KURL url,
                           ModuleType module_type,
-                          TextPosition position)
-      : url(url), module_type(module_type), position(position) {}
+                          TextPosition position,
+                          ModuleImportPhase import_phase)
+      : url(url),
+        module_type(module_type),
+        position(position),
+        import_phase(import_phase) {}
 
   KURL url;
   ModuleType module_type;
   TextPosition position;
+  ModuleImportPhase import_phase;
 };
 
 }  // namespace
@@ -155,6 +160,7 @@
                                  ModuleType module_type,
                                  const ScriptFetchOptions& options,
                                  base::PassKey<ModuleTreeLinkerRegistry>,
+                                 ModuleImportPhase import_phase,
                                  String referrer) {
 #if DCHECK_IS_ON()
   original_url_ = original_url;
@@ -225,9 +231,9 @@
   // well, so we pass through `referrer` which is usually the client string
   // (`Referrer::ClientReferrerString()`), but isn't for the dynamic import
   // case.
-  ModuleScriptFetchRequest request(url, module_type, context_type_,
-                                   destination_, options, referrer,
-                                   TextPosition::MinimumPosition());
+  ModuleScriptFetchRequest request(
+      url, module_type, context_type_, destination_, options, referrer,
+      TextPosition::MinimumPosition(), import_phase);
   ++num_incomplete_fetches_;
 
   // <spec label="fetch-a-module-script-tree" step="2">Fetch a single module
@@ -282,7 +288,9 @@
 // Returning from #fetch-a-single-module-script, calling from
 // #fetch-a-module-script-tree, #fetch-an-import()-module-script-graph, and
 // #fetch-a-module-worker-script-tree, and IMSGF.
-void ModuleTreeLinker::NotifyModuleLoadFinished(ModuleScript* module_script) {
+void ModuleTreeLinker::NotifyModuleLoadFinished(
+    ModuleScript* module_script,
+    ModuleImportPhase import_phase) {
   CHECK_GT(num_incomplete_fetches_, 0u);
   --num_incomplete_fetches_;
 
@@ -296,10 +304,16 @@
   }
 #endif
 
+  const bool is_source_phase = import_phase == ModuleImportPhase::kSource;
   if (state_ == State::kFetchingSelf) {
     // non-IMSGF cases: |module_script| is the top-level module, and will be
     // instantiated and returned later.
     result_ = module_script;
+    if (is_source_phase) {
+      // This also handles the error path where `module_script` is nullptr.
+      AdvanceState(State::kFinished);
+      return;
+    }
     AdvanceState(State::kFetchingDependencies);
   }
 
@@ -341,6 +355,18 @@
   //
   // <spec label="IMSGF" step="5">Fetch the descendants of result given fetch
   // client settings object, destination, and visited set.</spec>
+  if (is_source_phase) {
+    // Source phase imports don't load their descendants.
+    // TODO(https://crbug.com/42204365): Update with the real spec link once
+    // the PR is merged.
+    // See FinishLoadingIportedModule in
+    // https://arai-a.github.io/ecma262-compare/?pr=3492
+    if (AbortBeforeFinalizingIfNecessary(module_script)) {
+      return;
+    }
+    FinalizeFetchDescendantsForOneModuleScript();
+    return;
+  }
   FetchDescendants(module_script);
 }
 
@@ -351,31 +377,7 @@
 void ModuleTreeLinker::FetchDescendants(const ModuleScript* module_script) {
   DCHECK(module_script);
 
-  // [nospec] Abort the steps if the browsing context is discarded.
-  if (!modulator_->HasValidContext()) {
-    result_ = nullptr;
-    AdvanceState(State::kFinished);
-    return;
-  }
-  ScriptState* script_state = modulator_->GetScriptState();
-  v8::HandleScope scope(script_state->GetIsolate());
-
-  // <spec step="2">Let record be module script's record.</spec>
-  v8::Local<v8::Module> record = module_script->V8Module();
-
-  // <spec step="1">If module script's record is null, then asynchronously
-  // complete this algorithm with module script and abort these steps.</spec>
-  if (record.IsEmpty()) {
-    found_parse_error_ = true;
-    // We don't early-exit here and wait until all module scripts to be
-    // loaded, because we might be not sure which error to be reported.
-    //
-    // It is possible to determine whether the error to be reported can be
-    // determined without waiting for loading module scripts, and thus to
-    // early-exit here if possible. However, the complexity of such early-exit
-    // implementation might be high, and optimizing error cases with the
-    // implementation cost might be not worth doing.
-    FinalizeFetchDescendantsForOneModuleScript();
+  if (AbortBeforeFinalizingIfNecessary(module_script)) {
     return;
   }
 
@@ -391,7 +393,7 @@
   // <spec step="5">For each ModuleRequest Record requested of
   // record.[[RequestedModules]],</spec>
   Vector<ModuleRequest> record_requested_modules =
-      ModuleRecord::ModuleRequests(script_state, record);
+      module_script->GetModuleRecordRequests();
 
   for (const auto& requested : record_requested_modules) {
     // <spec step="5.1">Let url be the result of resolving a module specifier
@@ -410,7 +412,8 @@
     // then:</spec>
     if (!visited_set_.Contains(std::make_pair(url, module_type))) {
       // <spec step="5.4.1">Append (url, module type) to moduleRequests.</spec>
-      module_requests.emplace_back(url, module_type, requested.position);
+      module_requests.emplace_back(url, module_type, requested.position,
+                                   requested.import_phase);
 
       // <spec step="5.4.2">Append (url, module type) to visited set.</spec>
       visited_set_.insert(std::make_pair(url, module_type));
@@ -460,7 +463,7 @@
     ModuleScriptFetchRequest request(
         module_request.url, module_request.module_type, context_type_,
         destination_, options, module_script->BaseUrl().GetString(),
-        module_request.position);
+        module_request.position, module_request.import_phase);
 
     // <spec label="IMSGF" step="1">Assert: visited set contains url.</spec>
     DCHECK(visited_set_.Contains(
@@ -497,6 +500,33 @@
     Instantiate();
 }
 
+bool ModuleTreeLinker::AbortBeforeFinalizingIfNecessary(
+    const ModuleScript* module_script) {
+  // [nospec] Abort the steps if the browsing context is discarded.
+  if (!modulator_->HasValidContext()) {
+    result_ = nullptr;
+    AdvanceState(State::kFinished);
+    return true;
+  }
+
+  // <spec step="1">If module script's record is null, then asynchronously
+  // complete this algorithm with module script and abort these steps.</spec>
+  if (module_script->HasEmptyRecord()) {
+    found_parse_error_ = true;
+    // We don't early-exit here and wait until all module scripts to be
+    // loaded, because we might be not sure which error to be reported.
+    //
+    // It is possible to determine whether the error to be reported can be
+    // determined without waiting for loading module scripts, and thus to
+    // early-exit here if possible. However, the complexity of such early-exit
+    // implementation might be high, and optimizing error cases with the
+    // implementation cost might be not worth doing.
+    FinalizeFetchDescendantsForOneModuleScript();
+    return true;
+  }
+  return false;
+}
+
 // <specdef
 // href="https://html.spec.whatwg.org/C/#fetch-the-descendants-of-and-link-a-module-script">
 void ModuleTreeLinker::Instantiate() {
@@ -525,17 +555,17 @@
     DCHECK(FindFirstParseError(result_, &discovered_set).IsEmpty());
 #endif
 
+    ScriptState* script_state = modulator_->GetScriptState();
+    ScriptState::Scope scope(script_state);
     // <spec step="5.1">Let record be result's record.</spec>
     v8::Local<v8::Module> record = result_->V8Module();
 
     // <spec step="5.2">Perform record.Instantiate(). ...</spec>
     AdvanceState(State::kInstantiating);
 
-    ScriptState* script_state = modulator_->GetScriptState();
     UseCounter::Count(ExecutionContext::From(script_state),
                       WebFeature::kInstantiateModuleScript);
 
-    ScriptState::Scope scope(script_state);
     ScriptValue instantiation_error =
         ModuleRecord::Instantiate(script_state, record, result_->SourceUrl());
 
@@ -586,14 +616,14 @@
 
   // <spec step="4">If moduleScript's record is null, then return moduleScript's
   // parse error.</spec>
-  v8::Local<v8::Module> record = module_script->V8Module();
-  if (record.IsEmpty())
+  if (module_script->HasEmptyRecord()) {
     return module_script->CreateParseError();
+  }
 
   // <spec step="5.1">Let childSpecifiers be the value of moduleScript's
   // record's [[RequestedModules]] internal slot.</spec>
   Vector<ModuleRequest> child_specifiers =
-      ModuleRecord::ModuleRequests(modulator_->GetScriptState(), record);
+      module_script->GetModuleRecordRequests();
 
   for (const auto& module_request : child_specifiers) {
     // <spec step="5.2">Let childURLs be the list obtained by calling resolve a
diff --git a/third_party/blink/renderer/core/loader/modulescript/module_tree_linker.h b/third_party/blink/renderer/core/loader/modulescript/module_tree_linker.h
index 9aa9f0c..2380a25 100644
--- a/third_party/blink/renderer/core/loader/modulescript/module_tree_linker.h
+++ b/third_party/blink/renderer/core/loader/modulescript/module_tree_linker.h
@@ -53,6 +53,7 @@
                  ModuleType,
                  const ScriptFetchOptions&,
                  base::PassKey<ModuleTreeLinkerRegistry>,
+                 v8::ModuleImportPhase,
                  String referrer);
   void FetchRootInline(ModuleScript*, base::PassKey<ModuleTreeLinkerRegistry>);
 
@@ -73,12 +74,14 @@
 #endif
   void AdvanceState(State);
 
-  void NotifyModuleLoadFinished(ModuleScript*) override;
+  void NotifyModuleLoadFinished(ModuleScript*, v8::ModuleImportPhase) override;
   void FetchDescendants(const ModuleScript*);
 
   // Completion of [FD].
   void FinalizeFetchDescendantsForOneModuleScript();
 
+  bool AbortBeforeFinalizingIfNecessary(const ModuleScript* module_script);
+
   // [FDaI] Steps 4--8.
   void Instantiate();
 
diff --git a/third_party/blink/renderer/core/loader/modulescript/module_tree_linker_registry.cc b/third_party/blink/renderer/core/loader/modulescript/module_tree_linker_registry.cc
index 638974d..f773f04 100644
--- a/third_party/blink/renderer/core/loader/modulescript/module_tree_linker_registry.cc
+++ b/third_party/blink/renderer/core/loader/modulescript/module_tree_linker_registry.cc
@@ -20,6 +20,7 @@
     Modulator* modulator,
     ModuleScriptCustomFetchType custom_fetch_type,
     ModuleTreeClient* client,
+    ModuleImportPhase import_phase,
     String referrer) {
   ModuleTreeLinker* linker = MakeGarbageCollected<ModuleTreeLinker>(
       fetch_client_settings_object_fetcher, context_type, destination,
@@ -27,7 +28,8 @@
       base::PassKey<ModuleTreeLinkerRegistry>());
   AddLinker(linker);
   linker->FetchRoot(url, module_type, options,
-                    base::PassKey<ModuleTreeLinkerRegistry>(), referrer);
+                    base::PassKey<ModuleTreeLinkerRegistry>(), import_phase,
+                    referrer);
   DCHECK(linker->IsFetching());
 }
 
diff --git a/third_party/blink/renderer/core/loader/modulescript/module_tree_linker_registry.h b/third_party/blink/renderer/core/loader/modulescript/module_tree_linker_registry.h
index 01d0cfae..83c61580 100644
--- a/third_party/blink/renderer/core/loader/modulescript/module_tree_linker_registry.h
+++ b/third_party/blink/renderer/core/loader/modulescript/module_tree_linker_registry.h
@@ -37,6 +37,7 @@
              Modulator*,
              ModuleScriptCustomFetchType,
              ModuleTreeClient*,
+             ModuleImportPhase import_phase,
              String referrer);
 
   // https://html.spec.whatwg.org/C/#fetch-an-inline-module-script-graph
diff --git a/third_party/blink/renderer/core/loader/modulescript/module_tree_linker_test.cc b/third_party/blink/renderer/core/loader/modulescript/module_tree_linker_test.cc
index 6fe93f1..6d567c6b 100644
--- a/third_party/blink/renderer/core/loader/modulescript/module_tree_linker_test.cc
+++ b/third_party/blink/renderer/core/loader/modulescript/module_tree_linker_test.cc
@@ -100,7 +100,8 @@
       sim_module.GetURL(), ModuleType::kJavaScriptOrWasm,
       GetDocument().Fetcher(), mojom::blink::RequestContextType::SCRIPT,
       network::mojom::RequestDestination::kScript, ScriptFetchOptions(),
-      ModuleScriptCustomFetchType::kNone, client);
+      ModuleScriptCustomFetchType::kNone, client,
+      ModuleImportPhase::kEvaluation);
 
   EXPECT_FALSE(client->WasNotifyFinished())
       << "ModuleTreeLinker should always finish asynchronously.";
@@ -124,7 +125,8 @@
       sim_module.GetURL(), ModuleType::kJavaScriptOrWasm,
       GetDocument().Fetcher(), mojom::blink::RequestContextType::SCRIPT,
       network::mojom::RequestDestination::kScript, ScriptFetchOptions(),
-      ModuleScriptCustomFetchType::kNone, client);
+      ModuleScriptCustomFetchType::kNone, client,
+      ModuleImportPhase::kEvaluation);
 
   EXPECT_FALSE(client->WasNotifyFinished())
       << "ModuleTreeLinker should always finish asynchronously.";
@@ -151,7 +153,8 @@
       sim_module.GetURL(), ModuleType::kJavaScriptOrWasm,
       GetDocument().Fetcher(), mojom::blink::RequestContextType::SCRIPT,
       network::mojom::RequestDestination::kScript, ScriptFetchOptions(),
-      ModuleScriptCustomFetchType::kNone, client);
+      ModuleScriptCustomFetchType::kNone, client,
+      ModuleImportPhase::kEvaluation);
 
   EXPECT_FALSE(client->WasNotifyFinished())
       << "ModuleTreeLinker should always finish asynchronously.";
@@ -179,7 +182,8 @@
       sim_module.GetURL(), ModuleType::kJavaScriptOrWasm,
       GetDocument().Fetcher(), mojom::blink::RequestContextType::SCRIPT,
       network::mojom::RequestDestination::kScript, ScriptFetchOptions(),
-      ModuleScriptCustomFetchType::kNone, client);
+      ModuleScriptCustomFetchType::kNone, client,
+      ModuleImportPhase::kEvaluation);
 
   EXPECT_FALSE(client->WasNotifyFinished())
       << "ModuleTreeLinker should always finish asynchronously.";
@@ -219,7 +223,8 @@
       sim_module.GetURL(), ModuleType::kJavaScriptOrWasm,
       GetDocument().Fetcher(), mojom::blink::RequestContextType::SCRIPT,
       network::mojom::RequestDestination::kScript, ScriptFetchOptions(),
-      ModuleScriptCustomFetchType::kNone, client);
+      ModuleScriptCustomFetchType::kNone, client,
+      ModuleImportPhase::kEvaluation);
 
   EXPECT_FALSE(client->WasNotifyFinished())
       << "ModuleTreeLinker should always finish asynchronously.";
@@ -269,7 +274,8 @@
       sim_module.GetURL(), ModuleType::kJavaScriptOrWasm,
       GetDocument().Fetcher(), mojom::blink::RequestContextType::SCRIPT,
       network::mojom::RequestDestination::kScript, ScriptFetchOptions(),
-      ModuleScriptCustomFetchType::kNone, client);
+      ModuleScriptCustomFetchType::kNone, client,
+      ModuleImportPhase::kEvaluation);
 
   EXPECT_FALSE(client->WasNotifyFinished())
       << "ModuleTreeLinker should always finish asynchronously.";
diff --git a/third_party/blink/renderer/core/loader/modulescript/worker_module_script_fetcher.cc b/third_party/blink/renderer/core/loader/modulescript/worker_module_script_fetcher.cc
index 7f1f8c2..025388f 100644
--- a/third_party/blink/renderer/core/loader/modulescript/worker_module_script_fetcher.cc
+++ b/third_party/blink/renderer/core/loader/modulescript/worker_module_script_fetcher.cc
@@ -39,7 +39,8 @@
     ModuleType expected_module_type,
     ResourceFetcher* fetch_client_settings_object_fetcher,
     ModuleGraphLevel level,
-    ModuleScriptFetcher::Client* client) {
+    ModuleScriptFetcher::Client* client,
+    ModuleImportPhase import_phase) {
   DCHECK_EQ(fetch_params.GetScriptType(), mojom::blink::ScriptType::kModule);
   DCHECK(global_scope_->IsContextThread());
   DCHECK(!fetch_client_settings_object_fetcher_);
@@ -47,6 +48,7 @@
   client_ = client;
   level_ = level;
   expected_module_type_ = expected_module_type;
+  import_phase_ = import_phase;
 
   // Use WorkerMainScriptLoader to load the main script when
   // dedicated workers and shared workers.
@@ -199,7 +201,8 @@
   client_->NotifyFetchFinishedSuccess(ModuleScriptCreationParams(
       /*source_url=*/response_url, /*base_url=*/response_url,
       ScriptSourceLocationType::kExternalFile, module_type, source_text,
-      cache_handler, response_referrer_policy));
+      cache_handler, response_referrer_policy, nullptr,
+      ScriptStreamer::NotStreamingReason::kStreamingDisabled, import_phase_));
 }
 
 void WorkerModuleScriptFetcher::DidReceiveDataWorkerMainScript(
diff --git a/third_party/blink/renderer/core/loader/modulescript/worker_module_script_fetcher.h b/third_party/blink/renderer/core/loader/modulescript/worker_module_script_fetcher.h
index 41b8db6f..c93e6b0 100644
--- a/third_party/blink/renderer/core/loader/modulescript/worker_module_script_fetcher.h
+++ b/third_party/blink/renderer/core/loader/modulescript/worker_module_script_fetcher.h
@@ -33,7 +33,8 @@
              ModuleType,
              ResourceFetcher*,
              ModuleGraphLevel,
-             ModuleScriptFetcher::Client*) override;
+             ModuleScriptFetcher::Client*,
+             ModuleImportPhase import_phase) override;
 
   // Implements WorkerMainScriptLoaderClient, and these will be called for
   // dedicated workers and shared workers.
@@ -67,6 +68,7 @@
   Member<Client> client_;
   ModuleGraphLevel level_;
   ModuleType expected_module_type_;
+  ModuleImportPhase import_phase_;
 };
 
 }  // namespace blink
diff --git a/third_party/blink/renderer/core/loader/modulescript/worklet_module_script_fetcher.cc b/third_party/blink/renderer/core/loader/modulescript/worklet_module_script_fetcher.cc
index 30628e84..8da1148 100644
--- a/third_party/blink/renderer/core/loader/modulescript/worklet_module_script_fetcher.cc
+++ b/third_party/blink/renderer/core/loader/modulescript/worklet_module_script_fetcher.cc
@@ -21,7 +21,8 @@
     ModuleType expected_module_type,
     ResourceFetcher* fetch_client_settings_object_fetcher,
     ModuleGraphLevel level,
-    ModuleScriptFetcher::Client* client) {
+    ModuleScriptFetcher::Client* client,
+    ModuleImportPhase import_phase) {
   DCHECK_EQ(fetch_params.GetScriptType(), mojom::blink::ScriptType::kModule);
   if (global_scope_->GetModuleResponsesMap()->GetEntry(
           fetch_params.Url(), expected_module_type, client,
diff --git a/third_party/blink/renderer/core/loader/modulescript/worklet_module_script_fetcher.h b/third_party/blink/renderer/core/loader/modulescript/worklet_module_script_fetcher.h
index b8b6c62e4..797ce2a 100644
--- a/third_party/blink/renderer/core/loader/modulescript/worklet_module_script_fetcher.h
+++ b/third_party/blink/renderer/core/loader/modulescript/worklet_module_script_fetcher.h
@@ -35,7 +35,8 @@
              ModuleType,
              ResourceFetcher*,
              ModuleGraphLevel,
-             ModuleScriptFetcher::Client*) override;
+             ModuleScriptFetcher::Client*,
+             ModuleImportPhase) override;
 
   void Trace(Visitor* visitor) const override;
 
diff --git a/third_party/blink/renderer/core/loader/pending_link_preload.cc b/third_party/blink/renderer/core/loader/pending_link_preload.cc
index 33673eec..61c9317d 100644
--- a/third_party/blink/renderer/core/loader/pending_link_preload.cc
+++ b/third_party/blink/renderer/core/loader/pending_link_preload.cc
@@ -76,7 +76,8 @@
 }
 
 // https://html.spec.whatwg.org/C/#link-type-modulepreload
-void PendingLinkPreload::NotifyModuleLoadFinished(ModuleScript* module) {
+void PendingLinkPreload::NotifyModuleLoadFinished(ModuleScript* module,
+                                                  v8::ModuleImportPhase) {
   if (loader_)
     loader_->NotifyModuleLoadFinished(module);
   document_->RemovePendingLinkHeaderPreloadIfNeeded(*this);
diff --git a/third_party/blink/renderer/core/loader/pending_link_preload.h b/third_party/blink/renderer/core/loader/pending_link_preload.h
index d633136..df2835ff 100644
--- a/third_party/blink/renderer/core/loader/pending_link_preload.h
+++ b/third_party/blink/renderer/core/loader/pending_link_preload.h
@@ -38,7 +38,7 @@
   class FinishObserver;
 
   // SingleModuleClient implementation
-  void NotifyModuleLoadFinished(ModuleScript*) override;
+  void NotifyModuleLoadFinished(ModuleScript*, v8::ModuleImportPhase) override;
 
   void NotifyFinished();
 
diff --git a/third_party/blink/renderer/core/loader/preload_helper.cc b/third_party/blink/renderer/core/loader/preload_helper.cc
index 1463b0b0..eccf165 100644
--- a/third_party/blink/renderer/core/loader/preload_helper.cc
+++ b/third_party/blink/renderer/core/loader/preload_helper.cc
@@ -601,7 +601,8 @@
       modulator->TaskRunner()->PostTask(
           FROM_HERE,
           WTF::BindOnce(&SingleModuleClient::NotifyModuleLoadFinished,
-                        WrapPersistent(client), nullptr));
+                        WrapPersistent(client), nullptr,
+                        ModuleImportPhase::kEvaluation));
     }
     return;
   }
@@ -675,7 +676,8 @@
                          params.referrer_policy,
                          mojom::blink::FetchPriorityHint::kAuto,
                          RenderBlockingBehavior::kNonBlocking),
-      Referrer::NoReferrer(), TextPosition::MinimumPosition());
+      Referrer::NoReferrer(), TextPosition::MinimumPosition(),
+      ModuleImportPhase::kEvaluation);
 
   // Step 13. "Fetch a modulepreload module script graph given url, destination,
   // settings object, and options. Wait until the algorithm asynchronously
diff --git a/third_party/blink/renderer/core/page/context_menu_controller.cc b/third_party/blink/renderer/core/page/context_menu_controller.cc
index 1741c1f..4d749ab 100644
--- a/third_party/blink/renderer/core/page/context_menu_controller.cc
+++ b/third_party/blink/renderer/core/page/context_menu_controller.cc
@@ -45,6 +45,7 @@
 #include "third_party/blink/public/web/web_plugin.h"
 #include "third_party/blink/public/web/web_text_check_client.h"
 #include "third_party/blink/renderer/core/annotation/annotation_agent_container_impl.h"
+#include "third_party/blink/renderer/core/annotation/annotation_agent_impl.h"
 #include "third_party/blink/renderer/core/dom/document.h"
 #include "third_party/blink/renderer/core/dom/element_traversal.h"
 #include "third_party/blink/renderer/core/dom/events/event_target.h"
@@ -424,7 +425,7 @@
          !data.link_url.is_empty() ||
          data.media_type == mojom::blink::ContextMenuDataMediaType::kImage ||
          data.media_type == mojom::blink::ContextMenuDataMediaType::kVideo ||
-         data.is_editable || data.opened_from_highlight ||
+         data.is_editable || data.annotation_type ||
          !data.selected_text.empty();
 }
 
@@ -716,8 +717,9 @@
   if (result.InnerNodeFrame()) {
     result.InnerNodeFrame()->View()->UpdateAllLifecyclePhasesExceptPaint(
         DocumentUpdateReason::kHitTest);
-    if (TextFragmentHandler::IsOverTextFragment(result)) {
-      data.opened_from_highlight = true;
+    if (std::optional<mojom::blink::AnnotationType> annotation =
+            AnnotationAgentImpl::IsOverAnnotation(result)) {
+      data.annotation_type = annotation;
     }
   }
 
diff --git a/third_party/blink/renderer/core/page/context_menu_controller_test.cc b/third_party/blink/renderer/core/page/context_menu_controller_test.cc
index 5fbca76..fec69c3 100644
--- a/third_party/blink/renderer/core/page/context_menu_controller_test.cc
+++ b/third_party/blink/renderer/core/page/context_menu_controller_test.cc
@@ -1866,7 +1866,7 @@
       1);
 }
 
-TEST_F(ContextMenuControllerTest, OpenedFromHighlight) {
+TEST_F(ContextMenuControllerTest, AnnotationType) {
   WebURL url = url_test_helpers::ToKURL("http://www.test.com/");
   frame_test_helpers::LoadHTMLString(LocalMainFrame(),
                                      R"(<html><head><style>body
@@ -1875,6 +1875,8 @@
       <p id="two">This is a test page two</p>
       <p id="three">This is a test page three</p>
       <p id="four">This is a test page four</p>
+      <p id="five">This is a test page five</p>
+      <p id="six">This is a test page six</p>
       </html>
       )",
                                      url);
@@ -1883,35 +1885,58 @@
   ASSERT_TRUE(IsA<HTMLDocument>(document));
 
   Element* first_element = document->getElementById(AtomicString("one"));
-  Element* middle_element = document->getElementById(AtomicString("one"));
+  Element* second_element = document->getElementById(AtomicString("one"));
   Element* third_element = document->getElementById(AtomicString("three"));
-  Element* last_element = document->getElementById(AtomicString("four"));
+  Element* fourth_element = document->getElementById(AtomicString("four"));
+  Element* fifth_element = document->getElementById(AtomicString("five"));
+  Element* last_element = document->getElementById(AtomicString("six"));
 
   // Install a text fragment marker from the beginning of <p> one to near the
-  // end of <p> three.
+  // end of <p> four.
   EphemeralRange dom_range =
       EphemeralRange(Position(first_element->firstChild(), 0),
-                     Position(third_element->firstChild(), 22));
+                     Position(fourth_element->firstChild(), 21));
   document->Markers().AddTextFragmentMarker(dom_range);
+
+  // Install a glic marker from the beginning of <p> four to near the end of
+  // of <p> five.
+  dom_range = EphemeralRange(Position(fourth_element->firstChild(), 0),
+                             Position(fifth_element->firstChild(), 21));
+  document->Markers().AddGlicMarker(dom_range);
+
   document->UpdateStyleAndLayout(DocumentUpdateReason::kTest);
 
   // Opening the context menu from the last <p> should not set
-  // |opened_from_highlight|.
+  // `annotation_type`.
   EXPECT_TRUE(ShowContextMenuForElement(last_element, kMenuSourceMouse));
   ContextMenuData context_menu_data = GetWebFrameClient().GetContextMenuData();
-  EXPECT_FALSE(context_menu_data.opened_from_highlight);
+  EXPECT_EQ(context_menu_data.annotation_type, std::nullopt);
 
-  // Opening the context menu from the second <p> should set
-  // |opened_from_highlight|.
-  EXPECT_TRUE(ShowContextMenuForElement(middle_element, kMenuSourceMouse));
+  // Opening the context menu from the second <p> should set `annotation_type`.
+  EXPECT_TRUE(ShowContextMenuForElement(second_element, kMenuSourceMouse));
   context_menu_data = GetWebFrameClient().GetContextMenuData();
-  EXPECT_TRUE(context_menu_data.opened_from_highlight);
+  EXPECT_EQ(context_menu_data.annotation_type,
+            mojom::AnnotationType::kSharedHighlight);
 
   // Opening the context menu from the middle of the third <p> should set
-  // |opened_from_highlight|.
+  // `annotation_type`.
   EXPECT_TRUE(ShowContextMenuForElement(third_element, kMenuSourceMouse));
   context_menu_data = GetWebFrameClient().GetContextMenuData();
-  EXPECT_TRUE(context_menu_data.opened_from_highlight);
+  EXPECT_EQ(context_menu_data.annotation_type,
+            mojom::AnnotationType::kSharedHighlight);
+
+  // Opening the context menu from fifth <p> should set `annotation_type` to
+  // kGlic.
+  EXPECT_TRUE(ShowContextMenuForElement(fifth_element, kMenuSourceMouse));
+  context_menu_data = GetWebFrameClient().GetContextMenuData();
+  EXPECT_EQ(context_menu_data.annotation_type, mojom::AnnotationType::kGlic);
+
+  // Opening the context menu from fourth <p> should set `annotation_type` to
+  // kGlic (even though there's also an overlapping annotation of type
+  // kSharedHighlight).
+  EXPECT_TRUE(ShowContextMenuForElement(fourth_element, kMenuSourceMouse));
+  context_menu_data = GetWebFrameClient().GetContextMenuData();
+  EXPECT_EQ(context_menu_data.annotation_type, mojom::AnnotationType::kGlic);
 }
 
 TEST_F(ContextMenuControllerTest, SelectAllEnabledForEditContext) {
diff --git a/third_party/blink/renderer/core/paint/paint_property_tree_builder_test.cc b/third_party/blink/renderer/core/paint/paint_property_tree_builder_test.cc
index 1a09494..547dd418 100644
--- a/third_party/blink/renderer/core/paint/paint_property_tree_builder_test.cc
+++ b/third_party/blink/renderer/core/paint/paint_property_tree_builder_test.cc
@@ -4621,10 +4621,7 @@
     </div>
   )HTML");
 
-  LayoutObject* thread =
-      GetLayoutObjectByElementId("multicol")->SlowFirstChild();
   LayoutObject* container = GetLayoutObjectByElementId("container");
-  EXPECT_TRUE(thread->IsLayoutFlowThread());
   ASSERT_EQ(2u, NumFragments(container));
   EXPECT_EQ(PhysicalOffset(100, 0), FragmentAt(container, 0).PaintOffset());
   EXPECT_EQ(PhysicalOffset(200, 100), FragmentAt(container, 1).PaintOffset());
diff --git a/third_party/blink/renderer/core/paint/pre_paint_tree_walk.cc b/third_party/blink/renderer/core/paint/pre_paint_tree_walk.cc
index 735787b..e4c5701 100644
--- a/third_party/blink/renderer/core/paint/pre_paint_tree_walk.cc
+++ b/third_party/blink/renderer/core/paint/pre_paint_tree_walk.cc
@@ -1264,15 +1264,15 @@
         // We can traverse PhysicalFragments in LayoutMedia though it's not
         // a LayoutNGObject.
         if (!box->IsMedia()) {
-          // Leave LayoutNGBoxFragment-accompanied child LayoutObject
-          // traversal, since this object doesn't support that (or has no
-          // fragments (happens for table columns)). We need to switch back to
-          // legacy LayoutObject traversal for its children. We're then also
-          // assuming that we're either not block-fragmenting, or that this is
-          // monolithic content. We may re-enter
-          // LayoutNGBoxFragment-accompanied traversal if we get to a
-          // descendant that supports that.
-          DCHECK(!box->FlowThreadContainingBlock() || box->IsMonolithic());
+          // Leave PhysicalBoxFragment-accompanied child LayoutObject traversal,
+          // since this object doesn't support that (or has no fragments
+          // (happens for table columns)). We need to switch back to plain
+          // LayoutObject traversal for its children. We're then also assuming
+          // that we're either not block-fragmenting, or that this is monolithic
+          // content. We may re-enter PhysicalBoxFragment-accompanied traversal
+          // if we get to a descendant that supports that.
+          DCHECK(!box->ContainingFragmentationContextRoot() ||
+                 box->IsMonolithic());
 
           traversable_fragment = nullptr;
         }
diff --git a/third_party/blink/renderer/core/script/dynamic_module_resolver.cc b/third_party/blink/renderer/core/script/dynamic_module_resolver.cc
index 6e3f58f..ddb8106 100644
--- a/third_party/blink/renderer/core/script/dynamic_module_resolver.cc
+++ b/third_party/blink/renderer/core/script/dynamic_module_resolver.cc
@@ -305,6 +305,7 @@
                         mojom::blink::RequestContextType::SCRIPT,
                         network::mojom::RequestDestination::kScript, options,
                         ModuleScriptCustomFetchType::kNone, tree_client,
+                        module_request.import_phase,
                         referrer_info.BaseURL().GetString());
 
   // Steps 6-9 are implemented at
diff --git a/third_party/blink/renderer/core/script/dynamic_module_resolver_test.cc b/third_party/blink/renderer/core/script/dynamic_module_resolver_test.cc
index 4763fcb..8d4430d 100644
--- a/third_party/blink/renderer/core/script/dynamic_module_resolver_test.cc
+++ b/third_party/blink/renderer/core/script/dynamic_module_resolver_test.cc
@@ -96,6 +96,7 @@
                  const ScriptFetchOptions&,
                  ModuleScriptCustomFetchType custom_fetch_type,
                  ModuleTreeClient* client,
+                 ModuleImportPhase import_phase,
                  String) final {
     EXPECT_EQ(expected_fetch_tree_url_, url);
     EXPECT_EQ(expected_fetch_tree_module_type_, module_type);
diff --git a/third_party/blink/renderer/core/script/modulator.h b/third_party/blink/renderer/core/script/modulator.h
index ee32516..ade3584 100644
--- a/third_party/blink/renderer/core/script/modulator.h
+++ b/third_party/blink/renderer/core/script/modulator.h
@@ -28,6 +28,12 @@
 #include "third_party/blink/renderer/platform/wtf/text/text_position.h"
 #include "third_party/blink/renderer/platform/wtf/text/wtf_string.h"
 
+namespace v8 {
+
+enum class ModuleImportPhase;
+
+}
+
 namespace blink {
 
 class ModuleScript;
@@ -55,7 +61,8 @@
     return "SingleModuleClient";
   }
 
-  virtual void NotifyModuleLoadFinished(ModuleScript*) = 0;
+  virtual void NotifyModuleLoadFinished(ModuleScript*,
+                                        v8::ModuleImportPhase) = 0;
 };
 
 // A ModuleTreeClient is notified when a module script and its whole descendent
@@ -138,6 +145,7 @@
       const ScriptFetchOptions&,
       ModuleScriptCustomFetchType,
       ModuleTreeClient*,
+      v8::ModuleImportPhase,
       String referrer = Referrer::ClientReferrerString()) = 0;
 
   // Asynchronously retrieve a module script from the module map, or fetch it
diff --git a/third_party/blink/renderer/core/script/modulator_impl_base.cc b/third_party/blink/renderer/core/script/modulator_impl_base.cc
index e4cf068..3f4eaaf7 100644
--- a/third_party/blink/renderer/core/script/modulator_impl_base.cc
+++ b/third_party/blink/renderer/core/script/modulator_impl_base.cc
@@ -73,10 +73,12 @@
     const ScriptFetchOptions& options,
     ModuleScriptCustomFetchType custom_fetch_type,
     ModuleTreeClient* client,
+    ModuleImportPhase import_phase,
     String referrer) {
   tree_linker_registry_->Fetch(
       url, module_type, fetch_client_settings_object_fetcher, context_type,
-      destination, options, this, custom_fetch_type, client, referrer);
+      destination, options, this, custom_fetch_type, client, import_phase,
+      referrer);
 }
 
 void ModulatorImplBase::FetchDescendantsForInlineScript(
diff --git a/third_party/blink/renderer/core/script/modulator_impl_base.h b/third_party/blink/renderer/core/script/modulator_impl_base.h
index 72d537fc2..f3b0ad4 100644
--- a/third_party/blink/renderer/core/script/modulator_impl_base.h
+++ b/third_party/blink/renderer/core/script/modulator_impl_base.h
@@ -63,6 +63,7 @@
                  const ScriptFetchOptions&,
                  ModuleScriptCustomFetchType,
                  ModuleTreeClient*,
+                 v8::ModuleImportPhase,
                  String referrer) override;
   void FetchDescendantsForInlineScript(
       ModuleScript*,
diff --git a/third_party/blink/renderer/core/script/module_map.cc b/third_party/blink/renderer/core/script/module_map.cc
index ca96e9c..869b79d4 100644
--- a/third_party/blink/renderer/core/script/module_map.cc
+++ b/third_party/blink/renderer/core/script/module_map.cc
@@ -28,16 +28,17 @@
   const char* NameInHeapSnapshot() const override { return "ModuleMap::Entry"; }
 
   // Notify fetched |m_moduleScript| to the client asynchronously.
-  void AddClient(SingleModuleClient*);
+  void AddClient(SingleModuleClient*, ModuleImportPhase);
 
   // This is only to be used from ModuleRecordResolver implementations.
   ModuleScript* GetModuleScript() const;
 
  private:
-  void DispatchFinishedNotificationAsync(SingleModuleClient*);
+  void DispatchFinishedNotificationAsync(SingleModuleClient*,
+                                         ModuleImportPhase);
 
   // Implements ModuleScriptLoaderClient
-  void NotifyNewSingleModuleFinished(ModuleScript*) override;
+  void NotifyNewSingleModuleFinished(ModuleScript*, ModuleImportPhase) override;
 
   Member<ModuleScript> module_script_;
   Member<ModuleMap> map_;
@@ -59,18 +60,21 @@
 }
 
 void ModuleMap::Entry::DispatchFinishedNotificationAsync(
-    SingleModuleClient* client) {
+    SingleModuleClient* client,
+    ModuleImportPhase import_phase) {
   map_->GetModulator()->TaskRunner()->PostTask(
-      FROM_HERE, WTF::BindOnce(&SingleModuleClient::NotifyModuleLoadFinished,
-                               WrapPersistent(client),
-                               WrapPersistent(module_script_.Get())));
+      FROM_HERE,
+      WTF::BindOnce(&SingleModuleClient::NotifyModuleLoadFinished,
+                    WrapPersistent(client),
+                    WrapPersistent(module_script_.Get()), import_phase));
 }
 
-void ModuleMap::Entry::AddClient(SingleModuleClient* new_client) {
+void ModuleMap::Entry::AddClient(SingleModuleClient* new_client,
+                                 ModuleImportPhase import_phase) {
   DCHECK(!clients_.Contains(new_client));
   if (!is_fetching_) {
     DCHECK(clients_.empty());
-    DispatchFinishedNotificationAsync(new_client);
+    DispatchFinishedNotificationAsync(new_client, import_phase);
     return;
   }
 
@@ -78,13 +82,14 @@
 }
 
 void ModuleMap::Entry::NotifyNewSingleModuleFinished(
-    ModuleScript* module_script) {
+    ModuleScript* module_script,
+    ModuleImportPhase import_phase) {
   CHECK(is_fetching_);
   module_script_ = module_script;
   is_fetching_ = false;
 
   for (const auto& client : clients_) {
-    DispatchFinishedNotificationAsync(client);
+    DispatchFinishedNotificationAsync(client, import_phase);
   }
   clients_.clear();
 }
@@ -140,7 +145,7 @@
   // <spec step="14">Set moduleMap[url] to module script, and asynchronously
   // complete this algorithm with module script.</spec>
   if (client)
-    entry->AddClient(client);
+    entry->AddClient(client, request.GetModuleImportPhase());
 }
 
 ModuleScript* ModuleMap::GetFetchedModuleScript(const KURL& url,
diff --git a/third_party/blink/renderer/core/script/module_map_test.cc b/third_party/blink/renderer/core/script/module_map_test.cc
index 89c2828..35b3ffa 100644
--- a/third_party/blink/renderer/core/script/module_map_test.cc
+++ b/third_party/blink/renderer/core/script/module_map_test.cc
@@ -17,6 +17,7 @@
 #include "third_party/blink/renderer/core/script/modulator.h"
 #include "third_party/blink/renderer/core/script/module_record_resolver.h"
 #include "third_party/blink/renderer/core/script/module_script.h"
+#include "third_party/blink/renderer/core/script/wasm_module_script.h"
 #include "third_party/blink/renderer/core/testing/dummy_modulator.h"
 #include "third_party/blink/renderer/core/testing/dummy_page_holder.h"
 #include "third_party/blink/renderer/core/testing/module_test_base.h"
@@ -41,17 +42,21 @@
     SingleModuleClient::Trace(visitor);
   }
 
-  void NotifyModuleLoadFinished(ModuleScript* module_script) override {
+  void NotifyModuleLoadFinished(ModuleScript* module_script,
+                                ModuleImportPhase import_phase) override {
     was_notify_finished_ = true;
     module_script_ = module_script;
+    import_phase_ = import_phase;
   }
 
   bool WasNotifyFinished() const { return was_notify_finished_; }
   ModuleScript* GetModuleScript() { return module_script_.Get(); }
+  ModuleImportPhase GetModuleImportPhase() { return import_phase_; }
 
  private:
   bool was_notify_finished_ = false;
   Member<ModuleScript> module_script_;
+  ModuleImportPhase import_phase_;
 };
 
 class TestModuleRecordResolver final : public ModuleRecordResolver {
@@ -117,10 +122,11 @@
                ModuleType module_type,
                ResourceFetcher*,
                ModuleGraphLevel,
-               ModuleScriptFetcher::Client* client) override {
+               ModuleScriptFetcher::Client* client,
+               ModuleImportPhase import_phase) override {
       CHECK_EQ(request.GetScriptType(), mojom::blink::ScriptType::kModule);
-      TestRequest* test_request =
-          MakeGarbageCollected<TestRequest>(request.Url(), client);
+      TestRequest* test_request = MakeGarbageCollected<TestRequest>(
+          request.Url(), client, import_phase);
       modulator_->test_requests_.push_back(test_request);
     }
     String DebugName() const override { return "TestModuleScriptFetcher"; }
@@ -144,20 +150,44 @@
   }
 
   struct TestRequest final : public GarbageCollected<TestRequest> {
-    TestRequest(const KURL& url, ModuleScriptFetcher::Client* client)
-        : url_(url), client_(client) {}
+    TestRequest(const KURL& url,
+                ModuleScriptFetcher::Client* client,
+                ModuleImportPhase import_phase)
+        : url_(url), client_(client), import_phase_(import_phase) {}
     void NotifyFetchFinished() {
+      ResolvedModuleType resolved_module_type = ResolvedModuleTypeFromUrl();
+      String script_string = EmptyModuleString(resolved_module_type);
       client_->NotifyFetchFinishedSuccess(ModuleScriptCreationParams(
           url_, url_, ScriptSourceLocationType::kExternalFile,
-          ResolvedModuleType::kJavaScript,
-          ParkableString(String("").ReleaseImpl()), nullptr,
-          network::mojom::ReferrerPolicy::kDefault));
+          resolved_module_type, ParkableString(script_string.ReleaseImpl()),
+          nullptr, network::mojom::ReferrerPolicy::kDefault, nullptr,
+          ScriptStreamer::NotStreamingReason::kStreamingDisabled,
+          import_phase_));
     }
     void Trace(Visitor* visitor) const { visitor->Trace(client_); }
 
    private:
+    ResolvedModuleType ResolvedModuleTypeFromUrl() {
+      const AtomicString& string_url = url_.GetString();
+      if (string_url.Find(".js") != WTF::kNotFound) {
+        return ResolvedModuleType::kJavaScript;
+      }
+      CHECK_NE(string_url.Find(".wasm"), WTF::kNotFound);
+      return ResolvedModuleType::kWasm;
+    }
+
+    String EmptyModuleString(ResolvedModuleType module_type) {
+      if (module_type == ResolvedModuleType::kJavaScript) {
+        return String("");
+      }
+      CHECK_EQ(module_type, ResolvedModuleType::kWasm);
+      return String(base::span<const uint8_t, 8>(
+          WasmModuleScript::kEmptyWasmByteSequence));
+    }
+
     const KURL url_;
     Member<ModuleScriptFetcher::Client> client_;
+    ModuleImportPhase import_phase_;
   };
   HeapVector<Member<TestRequest>> test_requests_;
 
@@ -196,6 +226,105 @@
   ModuleMapTestModulator* Modulator() { return modulator_.Get(); }
   ModuleMap* Map() { return map_; }
 
+  void TestSequentialRequest(const KURL& url,
+                             ModuleGraphLevel graph_level,
+                             ModuleImportPhase import_phase,
+                             bool is_wasm_module_record) {
+    // First request
+    TestSingleModuleClient* client =
+        MakeGarbageCollected<TestSingleModuleClient>();
+    Map()->FetchSingleModuleScript(
+        ModuleScriptFetchRequest::CreateForTest(
+            url, ModuleType::kJavaScriptOrWasm, import_phase),
+        GetDocument().Fetcher(), graph_level,
+        ModuleScriptCustomFetchType::kNone, client);
+    Modulator()->ResolveFetches();
+    EXPECT_FALSE(client->WasNotifyFinished())
+        << "fetchSingleModuleScript shouldn't complete synchronously";
+    test::RunPendingTasks();
+
+    EXPECT_EQ(Modulator()
+                  ->GetTestModuleRecordResolver()
+                  ->RegisterModuleScriptCallCount(),
+              1);
+    EXPECT_TRUE(client->WasNotifyFinished());
+    ModuleScript* module_script = client->GetModuleScript();
+    EXPECT_TRUE(module_script);
+    EXPECT_EQ(module_script->IsWasmModuleRecord(), is_wasm_module_record);
+    EXPECT_EQ(client->GetModuleImportPhase(), import_phase);
+
+    // Secondary request
+    TestSingleModuleClient* client2 =
+        MakeGarbageCollected<TestSingleModuleClient>();
+    Map()->FetchSingleModuleScript(
+        ModuleScriptFetchRequest::CreateForTest(
+            url, ModuleType::kJavaScriptOrWasm, import_phase),
+        GetDocument().Fetcher(), graph_level,
+        ModuleScriptCustomFetchType::kNone, client2);
+    Modulator()->ResolveFetches();
+    EXPECT_FALSE(client2->WasNotifyFinished())
+        << "fetchSingleModuleScript shouldn't complete synchronously";
+    test::RunPendingTasks();
+
+    EXPECT_EQ(Modulator()
+                  ->GetTestModuleRecordResolver()
+                  ->RegisterModuleScriptCallCount(),
+              1)
+        << "registerModuleScript shouldn't be called in secondary request.";
+    EXPECT_TRUE(client2->WasNotifyFinished());
+    module_script = client->GetModuleScript();
+    EXPECT_TRUE(module_script);
+    EXPECT_EQ(module_script->IsWasmModuleRecord(), is_wasm_module_record);
+    EXPECT_EQ(client->GetModuleImportPhase(), import_phase);
+  }
+
+  void TestConcurrentRequestsShouldJoin(const KURL& url,
+                                        ModuleGraphLevel graph_level,
+                                        ModuleImportPhase import_phase,
+                                        bool is_wasm_module_record) {
+    // First request
+    TestSingleModuleClient* client =
+        MakeGarbageCollected<TestSingleModuleClient>();
+    Map()->FetchSingleModuleScript(
+        ModuleScriptFetchRequest::CreateForTest(
+            url, ModuleType::kJavaScriptOrWasm, import_phase),
+        GetDocument().Fetcher(), graph_level,
+        ModuleScriptCustomFetchType::kNone, client);
+
+    // Secondary request (which should join the first request)
+    TestSingleModuleClient* client2 =
+        MakeGarbageCollected<TestSingleModuleClient>();
+    Map()->FetchSingleModuleScript(
+        ModuleScriptFetchRequest::CreateForTest(
+            url, ModuleType::kJavaScriptOrWasm, import_phase),
+        GetDocument().Fetcher(), graph_level,
+        ModuleScriptCustomFetchType::kNone, client2);
+
+    Modulator()->ResolveFetches();
+    EXPECT_FALSE(client->WasNotifyFinished())
+        << "fetchSingleModuleScript shouldn't complete synchronously";
+    EXPECT_FALSE(client2->WasNotifyFinished())
+        << "fetchSingleModuleScript shouldn't complete synchronously";
+    test::RunPendingTasks();
+
+    EXPECT_EQ(Modulator()
+                  ->GetTestModuleRecordResolver()
+                  ->RegisterModuleScriptCallCount(),
+              1);
+
+    EXPECT_TRUE(client->WasNotifyFinished());
+    ModuleScript* module_script = client->GetModuleScript();
+    EXPECT_TRUE(module_script);
+    EXPECT_EQ(module_script->IsWasmModuleRecord(), is_wasm_module_record);
+    EXPECT_EQ(client->GetModuleImportPhase(), import_phase);
+
+    EXPECT_TRUE(client2->WasNotifyFinished());
+    module_script = client2->GetModuleScript();
+    EXPECT_TRUE(module_script);
+    EXPECT_EQ(module_script->IsWasmModuleRecord(), is_wasm_module_record);
+    EXPECT_EQ(client2->GetModuleImportPhase(), import_phase);
+  }
+
  protected:
   Persistent<ModuleMapTestModulator> modulator_;
   Persistent<ModuleMap> map_;
@@ -218,85 +347,33 @@
 TEST_F(ModuleMapTest, sequentialRequests) {
   KURL url(NullURL(), "https://example.com/foo.js");
 
-  // First request
-  TestSingleModuleClient* client =
-      MakeGarbageCollected<TestSingleModuleClient>();
-  Map()->FetchSingleModuleScript(ModuleScriptFetchRequest::CreateForTest(
-                                     url, ModuleType::kJavaScriptOrWasm),
-                                 GetDocument().Fetcher(),
-                                 ModuleGraphLevel::kTopLevelModuleFetch,
-                                 ModuleScriptCustomFetchType::kNone, client);
-  Modulator()->ResolveFetches();
-  EXPECT_FALSE(client->WasNotifyFinished())
-      << "fetchSingleModuleScript shouldn't complete synchronously";
-  test::RunPendingTasks();
-
-  EXPECT_EQ(Modulator()
-                ->GetTestModuleRecordResolver()
-                ->RegisterModuleScriptCallCount(),
-            1);
-  EXPECT_TRUE(client->WasNotifyFinished());
-  EXPECT_TRUE(client->GetModuleScript());
-
-  // Secondary request
-  TestSingleModuleClient* client2 =
-      MakeGarbageCollected<TestSingleModuleClient>();
-  Map()->FetchSingleModuleScript(ModuleScriptFetchRequest::CreateForTest(
-                                     url, ModuleType::kJavaScriptOrWasm),
-                                 GetDocument().Fetcher(),
-                                 ModuleGraphLevel::kTopLevelModuleFetch,
-                                 ModuleScriptCustomFetchType::kNone, client2);
-  Modulator()->ResolveFetches();
-  EXPECT_FALSE(client2->WasNotifyFinished())
-      << "fetchSingleModuleScript shouldn't complete synchronously";
-  test::RunPendingTasks();
-
-  EXPECT_EQ(Modulator()
-                ->GetTestModuleRecordResolver()
-                ->RegisterModuleScriptCallCount(),
-            1)
-      << "registerModuleScript sholudn't be called in secondary request.";
-  EXPECT_TRUE(client2->WasNotifyFinished());
-  EXPECT_TRUE(client2->GetModuleScript());
+  TestSequentialRequest(url, ModuleGraphLevel::kTopLevelModuleFetch,
+                        ModuleImportPhase::kEvaluation,
+                        /*is_wasm_module_record =*/false);
 }
 
 TEST_F(ModuleMapTest, concurrentRequestsShouldJoin) {
   KURL url(NullURL(), "https://example.com/foo.js");
 
-  // First request
-  TestSingleModuleClient* client =
-      MakeGarbageCollected<TestSingleModuleClient>();
-  Map()->FetchSingleModuleScript(ModuleScriptFetchRequest::CreateForTest(
-                                     url, ModuleType::kJavaScriptOrWasm),
-                                 GetDocument().Fetcher(),
-                                 ModuleGraphLevel::kTopLevelModuleFetch,
-                                 ModuleScriptCustomFetchType::kNone, client);
+  TestConcurrentRequestsShouldJoin(url, ModuleGraphLevel::kTopLevelModuleFetch,
+                                   ModuleImportPhase::kEvaluation,
+                                   /*is_wasm_module_record =*/false);
+}
 
-  // Secondary request (which should join the first request)
-  TestSingleModuleClient* client2 =
-      MakeGarbageCollected<TestSingleModuleClient>();
-  Map()->FetchSingleModuleScript(ModuleScriptFetchRequest::CreateForTest(
-                                     url, ModuleType::kJavaScriptOrWasm),
-                                 GetDocument().Fetcher(),
-                                 ModuleGraphLevel::kTopLevelModuleFetch,
-                                 ModuleScriptCustomFetchType::kNone, client2);
+TEST_F(ModuleMapTest, WasmSourcePhaseSequentialRequests) {
+  KURL url(NullURL(), "https://example.com/foo.wasm");
 
-  Modulator()->ResolveFetches();
-  EXPECT_FALSE(client->WasNotifyFinished())
-      << "fetchSingleModuleScript shouldn't complete synchronously";
-  EXPECT_FALSE(client2->WasNotifyFinished())
-      << "fetchSingleModuleScript shouldn't complete synchronously";
-  test::RunPendingTasks();
+  TestSequentialRequest(url, ModuleGraphLevel::kDependentModuleFetch,
+                        ModuleImportPhase::kSource,
+                        /*is_wasm_module_record =*/true);
+}
 
-  EXPECT_EQ(Modulator()
-                ->GetTestModuleRecordResolver()
-                ->RegisterModuleScriptCallCount(),
-            1);
+TEST_F(ModuleMapTest, WasmSourcePhaseConcurrentRequestsShouldJoin) {
+  KURL url(NullURL(), "https://example.com/foo.wasm");
 
-  EXPECT_TRUE(client->WasNotifyFinished());
-  EXPECT_TRUE(client->GetModuleScript());
-  EXPECT_TRUE(client2->WasNotifyFinished());
-  EXPECT_TRUE(client2->GetModuleScript());
+  TestConcurrentRequestsShouldJoin(url, ModuleGraphLevel::kDependentModuleFetch,
+                                   ModuleImportPhase::kSource,
+                                   /*is_wasm_module_record =*/true);
 }
 
 }  // namespace blink
diff --git a/third_party/blink/renderer/core/script/module_script.cc b/third_party/blink/renderer/core/script/module_script.cc
index a2da9e4..7a6bc41e 100644
--- a/third_party/blink/renderer/core/script/module_script.cc
+++ b/third_party/blink/renderer/core/script/module_script.cc
@@ -63,6 +63,7 @@
 Vector<ModuleRequest> ModuleScript::GetModuleRecordRequests() const {
   CHECK(!record_.IsEmpty());
   v8::Isolate* isolate = settings_object_->GetScriptState()->GetIsolate();
+  v8::HandleScope scope(isolate);
   v8::Local<v8::Module> record = record_.Get(isolate).As<v8::Module>();
   return ModuleRecord::ModuleRequests(settings_object_->GetScriptState(),
                                       record);
diff --git a/third_party/blink/renderer/core/script/script_loader.cc b/third_party/blink/renderer/core/script/script_loader.cc
index 228d8a1..187b9da 100644
--- a/third_party/blink/renderer/core/script/script_loader.cc
+++ b/third_party/blink/renderer/core/script/script_loader.cc
@@ -1338,7 +1338,8 @@
                        fetch_client_settings_object_fetcher,
                        mojom::blink::RequestContextType::SCRIPT,
                        network::mojom::RequestDestination::kScript, options,
-                       ModuleScriptCustomFetchType::kNone, module_tree_client);
+                       ModuleScriptCustomFetchType::kNone, module_tree_client,
+                       ModuleImportPhase::kEvaluation);
   prepared_pending_script_ = MakeGarbageCollected<ModulePendingScript>(
       element_, module_tree_client, is_external_script_,
       GetRunningTask(modulator->GetScriptState()));
diff --git a/third_party/blink/renderer/core/script/wasm_module_script.h b/third_party/blink/renderer/core/script/wasm_module_script.h
index 48495edd..00e842f 100644
--- a/third_party/blink/renderer/core/script/wasm_module_script.h
+++ b/third_party/blink/renderer/core/script/wasm_module_script.h
@@ -47,6 +47,8 @@
   }
 
  private:
+  friend class ModuleMapTestModulator;
+
   // This byte sequence corresponds to an empty WebAssembly module with only
   // the magic bytes and version number provided.
   static constexpr const uint8_t kEmptyWasmByteSequence[8] = {
diff --git a/third_party/blink/renderer/core/testing/dummy_modulator.cc b/third_party/blink/renderer/core/testing/dummy_modulator.cc
index 5a80a27..3fe7ca6 100644
--- a/third_party/blink/renderer/core/testing/dummy_modulator.cc
+++ b/third_party/blink/renderer/core/testing/dummy_modulator.cc
@@ -75,6 +75,7 @@
                                const ScriptFetchOptions&,
                                ModuleScriptCustomFetchType,
                                ModuleTreeClient*,
+                               ModuleImportPhase,
                                String referrer) {
   NOTREACHED();
 }
diff --git a/third_party/blink/renderer/core/testing/dummy_modulator.h b/third_party/blink/renderer/core/testing/dummy_modulator.h
index 7041fbf..627e075 100644
--- a/third_party/blink/renderer/core/testing/dummy_modulator.h
+++ b/third_party/blink/renderer/core/testing/dummy_modulator.h
@@ -46,6 +46,7 @@
                  const ScriptFetchOptions&,
                  ModuleScriptCustomFetchType,
                  ModuleTreeClient*,
+                 v8::ModuleImportPhase,
                  String referrer) override;
   void FetchSingle(const ModuleScriptFetchRequest&,
                    ResourceFetcher*,
diff --git a/third_party/blink/renderer/core/url_pattern/url_pattern_canon.cc b/third_party/blink/renderer/core/url_pattern/url_pattern_canon.cc
index 957ef40a..610c787 100644
--- a/third_party/blink/renderer/core/url_pattern/url_pattern_canon.cc
+++ b/third_party/blink/renderer/core/url_pattern/url_pattern_canon.cc
@@ -287,12 +287,9 @@
   url::Component component;
   if (stripped.Is8Bit()) {
     StringUTF8Adaptor utf8(stripped);
-    url::CanonicalizeRef(utf8.data(), url::Component(0, utf8.size()),
-                         &canon_output, &component);
+    url::CanonicalizeRef(utf8.AsStringView(), &canon_output, &component);
   } else {
-    url::CanonicalizeRef(stripped.Characters16(),
-                         url::Component(0, stripped.length()), &canon_output,
-                         &component);
+    url::CanonicalizeRef(stripped.View16(), &canon_output, &component);
   }
 
   return StringFromCanonOutput(canon_output, component);
diff --git a/third_party/blink/renderer/core/workers/worker_or_worklet_global_scope.cc b/third_party/blink/renderer/core/workers/worker_or_worklet_global_scope.cc
index 29fff47..74b74d6 100644
--- a/third_party/blink/renderer/core/workers/worker_or_worklet_global_scope.cc
+++ b/third_party/blink/renderer/core/workers/worker_or_worklet_global_scope.cc
@@ -597,11 +597,14 @@
 
   Modulator* modulator = Modulator::From(ScriptController()->GetScriptState());
   // Step 3. "Perform the internal module script graph fetching procedure ..."
+  // The main script for a worker or worklet is always imported in the
+  // evaluation import phase.
   modulator->FetchTree(
       module_url_record, ModuleType::kJavaScriptOrWasm,
       CreateOutsideSettingsFetcher(fetch_client_settings_object,
                                    resource_timing_notifier),
-      context_type, destination, options, custom_fetch_type, client);
+      context_type, destination, options, custom_fetch_type, client,
+      v8::ModuleImportPhase::kEvaluation);
 }
 
 void WorkerOrWorkletGlobalScope::SetDefersLoadingForResourceFetchers(
diff --git a/third_party/blink/renderer/core/workers/worklet_module_responses_map_test.cc b/third_party/blink/renderer/core/workers/worklet_module_responses_map_test.cc
index 3753157..99f8a3f 100644
--- a/third_party/blink/renderer/core/workers/worklet_module_responses_map_test.cc
+++ b/third_party/blink/renderer/core/workers/worklet_module_responses_map_test.cc
@@ -120,7 +120,8 @@
             global_scope_, ModuleScriptLoader::CreatePassKeyForTests());
     module_fetcher->Fetch(fetch_params, ModuleType::kJavaScriptOrWasm,
                           fetcher_.Get(),
-                          ModuleGraphLevel::kTopLevelModuleFetch, client);
+                          ModuleGraphLevel::kTopLevelModuleFetch, client,
+                          ModuleImportPhase::kEvaluation);
   }
 
   void RunUntilIdle() {
diff --git a/third_party/blink/renderer/modules/ad_auction/auction_ad_config.idl b/third_party/blink/renderer/modules/ad_auction/auction_ad_config.idl
index e0717f2..c9e1e7a 100644
--- a/third_party/blink/renderer/modules/ad_auction/auction_ad_config.idl
+++ b/third_party/blink/renderer/modules/ad_auction/auction_ad_config.idl
@@ -74,7 +74,7 @@
   Promise<record<USVString, unsigned long long>?> perBuyerTimeouts;
   Promise<record<USVString, unsigned long long>?> perBuyerCumulativeTimeouts;
   [RuntimeEnabled=FledgeTrustedSignalsKVv2ContextualData]
-  record<USVString, any> perBuyerTKVSignals;
+  record<USVString, Promise<any>> perBuyerTKVSignals;
 
   unsigned long long reportingTimeout;
 
diff --git a/third_party/blink/renderer/modules/ad_auction/navigator_auction.cc b/third_party/blink/renderer/modules/ad_auction/navigator_auction.cc
index 8aaef0d..37a1b66 100644
--- a/third_party/blink/renderer/modules/ad_auction/navigator_auction.cc
+++ b/third_party/blink/renderer/modules/ad_auction/navigator_auction.cc
@@ -207,6 +207,29 @@
     const String seller_name_;
   };
 
+  // Handles resolution of a single promise in perBuyerTkvSignals.
+  //
+  // TODO(crbug.com/412588114): Handle rejected promises by sending
+  // a rejected MaybePromiseBuyerTkvSignals promise, rather than by aborting the
+  // auction.
+  class BuyerTkvSignalsResolved
+      : public AuctionHandleFunctionImpl<IDLAny, BuyerTkvSignalsResolved> {
+   public:
+    BuyerTkvSignalsResolved(
+        AuctionHandle* auction_handle,
+        scoped_refptr<const SecurityOrigin> buyer,
+        const MemberScriptPromise<IDLAny>& promise,
+        mojom::blink::AuctionAdConfigAuctionIdPtr auction_id,
+        const String& seller_name);
+
+    void React(ScriptState* script_state, ScriptValue value);
+
+   private:
+    scoped_refptr<const SecurityOrigin> buyer_;
+    const mojom::blink::AuctionAdConfigAuctionIdPtr auction_id_;
+    const String seller_name_;
+  };
+
   // This is used for perBuyerTimeouts and perBuyerCumulativeTimeouts, with
   // `field` indicating which of the two fields an object is being used for.
   class BuyerTimeoutsResolved
@@ -2028,34 +2051,32 @@
 }
 
 bool CopyPerBuyerTKVSignalsFromIdlToMojo(
-    const ScriptState& script_state,
     ExceptionState& exception_state,
+    NavigatorAuction::AuctionHandle* auction_handle,
+    const mojom::blink::AuctionAdConfigAuctionId* auction_id,
     const AuctionAdConfig& input,
     mojom::blink::AuctionAdConfig& output) {
   if (!input.hasPerBuyerTKVSignals()) {
     return true;
   }
 
-  for (const auto& per_buyer_tkv_signal : input.perBuyerTKVSignals()) {
+  for (const auto& per_buyer_tkv_signals : input.perBuyerTKVSignals()) {
     scoped_refptr<const SecurityOrigin> buyer =
-        ParseOrigin(per_buyer_tkv_signal.first);
+        ParseOrigin(per_buyer_tkv_signals.first);
     if (!buyer) {
       exception_state.ThrowTypeError(ErrorInvalidAuctionConfig(
-          input, "perBuyerTKVSignals buyer", per_buyer_tkv_signal.first,
+          input, "perBuyerTKVSignals buyer", per_buyer_tkv_signals.first,
           "must be a valid https origin."));
       return false;
     }
 
-    String tkv_signals_str;
-    if (!Jsonify(script_state, per_buyer_tkv_signal.second.V8Value(),
-                 tkv_signals_str)) {
-      exception_state.ThrowTypeError(ErrorInvalidAuctionConfigSellerJson(
-          input.seller(), "perBuyerTKVSignals"));
-      return false;
-    }
-
+    auction_handle->QueueAttachPromiseHandler(
+        MakeGarbageCollected<
+            NavigatorAuction::AuctionHandle::BuyerTkvSignalsResolved>(
+            auction_handle, buyer, per_buyer_tkv_signals.second,
+            auction_id->Clone(), input.seller()));
     output.auction_ad_config_non_shared_params->per_buyer_tkv_signals.insert(
-        buyer, tkv_signals_str);
+        buyer, mojom::blink::AuctionAdConfigMaybePromiseJson::NewPromise(0));
   }
 
   return true;
@@ -2720,8 +2741,9 @@
                                                      *mojo_config) ||
       !CopyPerBuyerRealTimeReportingTypesFromIdlToMojo(exception_state, config,
                                                        *mojo_config) ||
-      !CopyPerBuyerTKVSignalsFromIdlToMojo(script_state, exception_state,
-                                           config, *mojo_config)) {
+      !CopyPerBuyerTKVSignalsFromIdlToMojo(exception_state, auction_handle,
+                                           auction_id.get(), config,
+                                           *mojo_config)) {
     return mojom::blink::AuctionAdConfigPtr();
   }
 
@@ -3246,6 +3268,41 @@
   }
 }
 
+NavigatorAuction::AuctionHandle::BuyerTkvSignalsResolved::
+    BuyerTkvSignalsResolved(
+        AuctionHandle* auction_handle,
+        scoped_refptr<const SecurityOrigin> buyer,
+        const MemberScriptPromise<IDLAny>& promise,
+        mojom::blink::AuctionAdConfigAuctionIdPtr auction_id,
+        const String& seller_name)
+    : AuctionHandleFunctionImpl(auction_handle, promise, /*is_input=*/true),
+      buyer_(buyer),
+      auction_id_(std::move(auction_id)),
+      seller_name_(seller_name) {}
+
+void NavigatorAuction::AuctionHandle::BuyerTkvSignalsResolved::React(
+    ScriptState* script_state,
+    ScriptValue value) {
+  OnResolved();
+
+  if (!script_state->ContextIsValid()) {
+    return;
+  }
+
+  String maybe_json;
+  if (!value.IsEmpty()) {
+    if (!Jsonify(*script_state, value.V8Value(), maybe_json)) {
+      maybe_json = String();
+      // TODO(crbug.com/412588114): Consider throwing an exception here. It
+      // won't be possible to catch the exception, but it will be visible via an
+      // `unhandledrejection` event.
+    }
+  }
+
+  auction_handle()->mojo_pipe()->ResolvedBuyerTkvSignalsPromise(
+      auction_id_->Clone(), buyer_, maybe_json);
+}
+
 NavigatorAuction::AuctionHandle::DeprecatedRenderURLReplacementsResolved::
     DeprecatedRenderURLReplacementsResolved(
         AuctionHandle* auction_handle,
diff --git a/third_party/blink/renderer/modules/canvas/canvas2d/canvas_rendering_context_2d.cc b/third_party/blink/renderer/modules/canvas/canvas2d/canvas_rendering_context_2d.cc
index d8737b8..fe4d813 100644
--- a/third_party/blink/renderer/modules/canvas/canvas2d/canvas_rendering_context_2d.cc
+++ b/third_party/blink/renderer/modules/canvas/canvas2d/canvas_rendering_context_2d.cc
@@ -712,7 +712,7 @@
     std::optional<double> dwidth,
     std::optional<double> dheight,
     ExceptionState& exception_state) {
-  CHECK(RuntimeEnabledFeatures::CanvasElementDrawElementEnabled());
+  CHECK(RuntimeEnabledFeatures::CanvasDrawElementEnabled());
   if (!IsDrawElementEligible(element, exception_state)) {
     return;
   }
diff --git a/third_party/blink/renderer/modules/canvas/canvas2d/canvas_rendering_context_2d.idl b/third_party/blink/renderer/modules/canvas/canvas2d/canvas_rendering_context_2d.idl
index e3e1f583..5dec348 100644
--- a/third_party/blink/renderer/modules/canvas/canvas2d/canvas_rendering_context_2d.idl
+++ b/third_party/blink/renderer/modules/canvas/canvas2d/canvas_rendering_context_2d.idl
@@ -58,10 +58,10 @@
     [RuntimeEnabled=CanvasPlaceElement, RaisesException] void placeElement(Element element, unrestricted double x,
                                         unrestricted double y);
 
-    [RuntimeEnabled=CanvasElementDrawElement, RaisesException]
+    [RuntimeEnabled=CanvasDrawElement, RaisesException]
     void drawElement(Element element, unrestricted double x, unrestricted double y);
 
-    [RuntimeEnabled=CanvasElementDrawElement, RaisesException]
+    [RuntimeEnabled=CanvasDrawElement, RaisesException]
     void drawElement(Element element, unrestricted double x, unrestricted double y,
                      unrestricted double dwidth, unrestricted double dheight);
 
diff --git a/third_party/blink/renderer/modules/ml/webnn/ml_graph_builder.cc b/third_party/blink/renderer/modules/ml/webnn/ml_graph_builder.cc
index e85dfb7..93c0e10 100644
--- a/third_party/blink/renderer/modules/ml/webnn/ml_graph_builder.cc
+++ b/third_party/blink/renderer/modules/ml/webnn/ml_graph_builder.cc
@@ -1423,7 +1423,7 @@
   }
 }
 
-base::expected<blink_mojom::GraphInfoPtr, String> BuildWebNNGraphInfo(
+blink_mojom::GraphInfoPtr BuildWebNNGraphInfo(
     const MLNamedOperands& named_outputs,
     const webnn::ContextProperties& context_properties) {
   // The `GraphInfo` represents an entire information of WebNN graph.
@@ -1499,13 +1499,8 @@
       operand_to_id_map.insert(operand, operand_id);
     }
     // Create `mojo::Operation` with the id of the input and output operands.
-    std::optional<String> error =
-        SerializeMojoOperation(operand_to_id_map, context_properties,
-                               current_operator.Get(), graph_info.get());
-    if (error.has_value()) {
-      // Return here if the operator is not implemented.
-      return base::unexpected(*error);
-    }
+    SerializeMojoOperation(operand_to_id_map, context_properties,
+                           current_operator.Get(), graph_info.get());
   }
 
   return graph_info;
@@ -3258,33 +3253,24 @@
   }
 
   scoped_trace.AddStep("BuildWebNNGraphInfo");
-  auto graph_info =
+  blink_mojom::GraphInfoPtr graph_info =
       BuildWebNNGraphInfo(named_outputs, ml_context_->GetProperties());
-  if (!graph_info.has_value()) {
-    // TODO(crbug.com/345271830): Move the platform-specific checks into the
-    // respective synchronous operator builder methods, such that
-    // `BuildWebNNGraphInfo` always succeeds.
-    exception_state.ThrowDOMException(
-        DOMExceptionCode::kNotSupportedError,
-        "Failed to build graph: " + graph_info.error());
-    return EmptyPromise();
-  }
 
   // Set `has_built_` after all inputs have been validated.
   has_built_ = true;
 
   scoped_trace.AddStep("FoldReshapableConstants");
-  FoldReshapableConstants(**graph_info);
+  FoldReshapableConstants(*graph_info);
 
   scoped_trace.AddStep("RecordOperatorsUsed");
-  RecordOperatorsUsed(**graph_info);
+  RecordOperatorsUsed(*graph_info);
 
   pending_resolver_ = MakeGarbageCollected<ScriptPromiseResolver<MLGraph>>(
       script_state, exception_state.GetContext());
 
   scoped_trace.AddStep("post mojo message: CreateGraph");
   remote_->CreateGraph(
-      *std::move(graph_info),
+      std::move(graph_info),
       WTF::BindOnce(&MLGraphBuilder::DidCreateWebNNGraph, WrapPersistent(this),
                     WrapPersistent(pending_resolver_.Get()),
                     *std::move(graph_constraints)));
diff --git a/third_party/blink/renderer/modules/ml/webnn/ml_graph_type_converter.cc b/third_party/blink/renderer/modules/ml/webnn/ml_graph_type_converter.cc
index da93c94d..0d0e651b 100644
--- a/third_party/blink/renderer/modules/ml/webnn/ml_graph_type_converter.cc
+++ b/third_party/blink/renderer/modules/ml/webnn/ml_graph_type_converter.cc
@@ -712,7 +712,7 @@
 }
 
 template <typename MLConv2dOptionsType>
-std::optional<String> SerializeConv2dOperation(
+void SerializeConv2dOperation(
     const OperandToIdMap& operand_to_id_map,
     const webnn::ContextProperties& context_properties,
     const MLOperator* conv2d,
@@ -816,8 +816,6 @@
     graph_info->operations.push_back(
         blink_mojom::Operation::NewTranspose(std::move(output_transpose)));
   }
-
-  return std::nullopt;
 }
 
 OperationPtr CreateCumulativeSumOperation(
@@ -1023,9 +1021,8 @@
   return blink_mojom::Operation::NewGru(std::move(gru_mojo));
 }
 
-base::expected<OperationPtr, String> CreateGruCellOperation(
-    const OperandToIdMap& operand_to_id_map,
-    const MLOperator* gru_cell) {
+OperationPtr CreateGruCellOperation(const OperandToIdMap& operand_to_id_map,
+                                    const MLOperator* gru_cell) {
   webnn::OperandId input_operand_id =
       GetOperatorInputId(gru_cell, operand_to_id_map, 0);
   webnn::OperandId weight_operand_id =
@@ -1241,9 +1238,8 @@
   return blink_mojom::Operation::NewLstm(std::move(lstm_mojo));
 }
 
-base::expected<OperationPtr, String> CreateLstmCellOperation(
-    const OperandToIdMap& operand_to_id_map,
-    const MLOperator* lstm_cell) {
+OperationPtr CreateLstmCellOperation(const OperandToIdMap& operand_to_id_map,
+                                     const MLOperator* lstm_cell) {
   webnn::OperandId input_operand_id =
       GetOperatorInputId(lstm_cell, operand_to_id_map, 0);
   webnn::OperandId weight_operand_id =
@@ -1806,7 +1802,7 @@
 }
 
 // TODO(crbug.com/1504405): Use a lookup table to simplifie the switch logic.
-std::optional<String> SerializeMojoOperation(
+void SerializeMojoOperation(
     const HeapHashMap<Member<const MLOperand>, webnn::OperandId>&
         operand_to_id_map,
     const webnn::ContextProperties& context_properties,
@@ -1830,22 +1826,18 @@
           CreateConcatOperation(operand_to_id_map, op));
       break;
     case blink_mojom::Operation::Tag::kConv2d: {
-      std::optional<String> error;
       switch (op->SubKind<blink_mojom::Conv2d::Kind>()) {
         case blink_mojom::Conv2d::Kind::kDirect: {
-          error = SerializeConv2dOperation<MLConv2dOptions>(
+          SerializeConv2dOperation<MLConv2dOptions>(
               operand_to_id_map, context_properties, op, graph_info);
           break;
         }
         case blink_mojom::Conv2d::Kind::kTransposed: {
-          error = SerializeConv2dOperation<MLConvTranspose2dOptions>(
+          SerializeConv2dOperation<MLConvTranspose2dOptions>(
               operand_to_id_map, context_properties, op, graph_info);
           break;
         }
       }
-      if (error) {
-        return error.value();
-      }
       break;
     }
     case blink_mojom::Operation::Tag::kCumulativeSum:
@@ -1898,12 +1890,10 @@
       graph_info->operations.push_back(
           CreateGruOperation(operand_to_id_map, op));
       break;
-    case blink_mojom::Operation::Tag::kGruCell: {
-      ASSIGN_OR_RETURN(auto mojo_op,
-                       CreateGruCellOperation(operand_to_id_map, op));
-      graph_info->operations.push_back(std::move(mojo_op));
+    case blink_mojom::Operation::Tag::kGruCell:
+      graph_info->operations.push_back(
+          CreateGruCellOperation(operand_to_id_map, op));
       break;
-    }
     case blink_mojom::Operation::Tag::kHardSigmoid:
       graph_info->operations.push_back(blink_mojom::Operation::NewHardSigmoid(
           CreateHardSigmoid(operand_to_id_map, op)));
@@ -1933,9 +1923,8 @@
           CreateLstmOperation(operand_to_id_map, op));
       break;
     case blink_mojom::Operation::Tag::kLstmCell: {
-      ASSIGN_OR_RETURN(auto mojo_op,
-                       CreateLstmCellOperation(operand_to_id_map, op));
-      graph_info->operations.push_back(std::move(mojo_op));
+      graph_info->operations.push_back(
+          CreateLstmCellOperation(operand_to_id_map, op));
       break;
     }
     case blink_mojom::Operation::Tag::kMatmul:
@@ -2031,7 +2020,6 @@
           CreateWhereOperation(operand_to_id_map, op));
       break;
   }
-  return std::nullopt;
 }
 
 }  // namespace blink
diff --git a/third_party/blink/renderer/modules/ml/webnn/ml_graph_type_converter.h b/third_party/blink/renderer/modules/ml/webnn/ml_graph_type_converter.h
index a81f4b1..73144d4 100644
--- a/third_party/blink/renderer/modules/ml/webnn/ml_graph_type_converter.h
+++ b/third_party/blink/renderer/modules/ml/webnn/ml_graph_type_converter.h
@@ -24,7 +24,7 @@
 webnn::OperandId NextOperandId(
     const webnn::mojom::blink::GraphInfo& graph_info);
 
-std::optional<String> SerializeMojoOperation(
+void SerializeMojoOperation(
     const HeapHashMap<Member<const MLOperand>, webnn::OperandId>&
         operand_to_id_map,
     const webnn::ContextProperties& context_properties,
diff --git a/third_party/blink/renderer/modules/service_worker/service_worker_module_tree_client.cc b/third_party/blink/renderer/modules/service_worker/service_worker_module_tree_client.cc
index 49b202b..76121a6 100644
--- a/third_party/blink/renderer/modules/service_worker/service_worker_module_tree_client.cc
+++ b/third_party/blink/renderer/modules/service_worker/service_worker_module_tree_client.cc
@@ -41,6 +41,7 @@
 
   // With top-level await: https://github.com/w3c/ServiceWorker/pull/1444
   if (!module_script->HasEmptyRecord()) {
+    v8::HandleScope scope(script_state_->GetIsolate());
     v8::Local<v8::Module> record = module_script->V8Module();
     if (record->GetStatus() >= v8::Module::kInstantiated &&
         record->IsGraphAsync()) {
diff --git a/third_party/blink/renderer/platform/media/web_media_player_impl.cc b/third_party/blink/renderer/platform/media/web_media_player_impl.cc
index 39b32a01..9686f67 100644
--- a/third_party/blink/renderer/platform/media/web_media_player_impl.cc
+++ b/third_party/blink/renderer/platform/media/web_media_player_impl.cc
@@ -819,6 +819,7 @@
 }
 
 void WebMediaPlayerImpl::BecameDominantVisibleContent(bool is_dominant) {
+  is_dominant_visible_content_ = is_dominant;
   if (observer_)
     observer_->OnBecameDominantVisibleContent(is_dominant);
 }
@@ -832,6 +833,9 @@
         fullscreen_video_status !=
         WebFullscreenVideoStatus::kNotEffectivelyFullscreen);
   }
+  is_effectively_fullscreen_ =
+      fullscreen_video_status !=
+      WebFullscreenVideoStatus::kNotEffectivelyFullscreen;
 }
 
 void WebMediaPlayerImpl::OnHasNativeControlsChanged(bool has_native_controls) {
@@ -2661,6 +2665,15 @@
   // This should never be called when stale state testing overrides are used.
   DCHECK(!stale_state_override_for_testing_.has_value());
 
+  // An idle media player that's the dominant visible content/full screen is
+  // likely to be resumed. We shouldn't release the resources yet so clear the
+  // stale flag.
+  if (!IsPageHidden() &&
+      (is_dominant_visible_content_ || is_effectively_fullscreen_)) {
+    delegate_->ClearStaleFlag(delegate_id_);
+    return;
+  }
+
   // If we are attempting preroll, clear the stale flag.
   if (IsPrerollAttemptNeeded()) {
     delegate_->ClearStaleFlag(delegate_id_);
diff --git a/third_party/blink/renderer/platform/media/web_media_player_impl.h b/third_party/blink/renderer/platform/media/web_media_player_impl.h
index 939fc57f..12f29f1 100644
--- a/third_party/blink/renderer/platform/media/web_media_player_impl.h
+++ b/third_party/blink/renderer/platform/media/web_media_player_impl.h
@@ -1128,6 +1128,10 @@
 
   bool should_pause_when_frame_is_hidden_ = false;
 
+  bool is_dominant_visible_content_ = false;
+
+  bool is_effectively_fullscreen_ = false;
+
   base::CancelableOnceClosure have_enough_after_lazy_load_cb_;
 
   media::RendererType renderer_type_ = media::RendererType::kRendererImpl;
diff --git a/third_party/blink/renderer/platform/runtime_enabled_features.json5 b/third_party/blink/renderer/platform/runtime_enabled_features.json5
index 2f969b3..a446c03 100644
--- a/third_party/blink/renderer/platform/runtime_enabled_features.json5
+++ b/third_party/blink/renderer/platform/runtime_enabled_features.json5
@@ -757,7 +757,7 @@
     },
     {
       // https://github.com/WICG/html-in-canvas/blob/main/README.md
-      name: "CanvasElementDrawElement",
+      name: "CanvasDrawElement",
       status: "test",
       implied_by: ["CanvasPlaceElement"],
     },
@@ -2245,6 +2245,13 @@
       status: "test",
     },
     {
+      // Don't create LayoutMultiColumnFlowThread objects, or any of the other
+      // legacy multicol objects.
+      name: "FlowThreadLess",
+      depends_on: ["LayoutBoxVisualLocation"],
+      status: "stable",
+    },
+    {
       name: "FluentOverlayScrollbars",
       // The associated base feature is defined in
       // ui/native_theme/native_theme_features.cc.
diff --git a/third_party/blink/web_tests/TestExpectations b/third_party/blink/web_tests/TestExpectations
index 734c38c..1cb7dbe 100644
--- a/third_party/blink/web_tests/TestExpectations
+++ b/third_party/blink/web_tests/TestExpectations
@@ -2888,8 +2888,6 @@
 crbug.com/408039416 [ Mac12 ] virtual/speech-with-unified-autoplay/external/wpt/speech-api/SpeechRecognition-onerror.https.html [ Timeout ]
 crbug.com/408039416 [ Mac12 ] virtual/speech-with-unified-autoplay/external/wpt/speech-api/SpeechRecognition-onstart-onend.https.html [ Timeout ]
 [ Mac13 ] external/wpt/webrtc/RTCDataChannel-send-close.html [ Skip Timeout ]
-crbug.com/407404837 [ Win ] external/wpt/speech-api/SpeechRecognition-installOnDevice.https.html [ Failure Timeout ]
-crbug.com/407404837 [ Win ] virtual/speech-with-unified-autoplay/external/wpt/speech-api/SpeechRecognition-installOnDevice.https.html [ Failure Timeout ]
 crbug.com/406805472 [ Mac12 ] external/wpt/import-maps/data-url-specifiers.sub.html [ Skip Timeout ]
 crbug.com/406805472 [ Mac13 ] virtual/speculation-rules-prerender-target-hint/external/wpt/speculation-rules/prerender/protocol-handler-register.https.html [ Skip Timeout ]
 crbug.com/406805472 [ Mac13 ] virtual/speculation-rules-prerender-target-hint/external/wpt/speculation-rules/prerender/protocol-handler-unregister.https.html [ Skip Timeout ]
@@ -8866,7 +8864,7 @@
 # Gardener 2024-08-13
 crbug.com/358437652 [ Linux ] external/wpt/dom/events/scrolling/wheel-event-transactions-multiple-action-chains.html [ Failure ]
 crbug.com/358437652 [ Mac ] external/wpt/dom/events/scrolling/wheel-event-transactions-multiple-action-chains.html [ Failure ]
-crbug.com/358437652 [ Win11-arm64 ] external/wpt/dom/events/scrolling/wheel-event-transactions-multiple-action-chains.html [ Failure ]
+crbug.com/358437652 [ Win ] external/wpt/dom/events/scrolling/wheel-event-transactions-multiple-action-chains.html [ Failure ]
 crbug.com/359519774 [ Linux ] external/wpt/webdriver/tests/bidi/input/perform_actions/pointer_touch.py [ Failure Pass ]
 
 # TODO(crbug.com/360166067) This test timeout on headless shell and Chrome
diff --git a/third_party/blink/web_tests/external/wpt/speech-api/SpeechRecognition-installOnDevice.https.html b/third_party/blink/web_tests/external/wpt/speech-api/SpeechRecognition-installOnDevice.https.html
index 2f5b359c..7422f5d 100644
--- a/third_party/blink/web_tests/external/wpt/speech-api/SpeechRecognition-installOnDevice.https.html
+++ b/third_party/blink/web_tests/external/wpt/speech-api/SpeechRecognition-installOnDevice.https.html
@@ -8,91 +8,106 @@
 promise_test(async (t) => {
   const validLang = "en-US";
   const invalidLang = "invalid language code";
-  const validDownloadableLang = "fr-FR";
   window.SpeechRecognition =
     window.SpeechRecognition || window.webkitSpeechRecognition;
 
-  // Attempt to call installOnDevice directly, without a user gesture with a
-  // language that is downloadable but not installed.
-  const installWithoutUserGesturePromise =
-    SpeechRecognition.installOnDevice(validDownloadableLang);
-
-  // Assert that the promise rejects with NotAllowedError.
-  await promise_rejects_dom(
-    t,
-    "NotAllowedError",
-    window.DOMException,
-    installWithoutUserGesturePromise,
-    "SpeechRecognition.installOnDevice() must reject with NotAllowedError if " +
-      "called without a user gesture."
-  );
-
-  // Test that it returns a promise when called with a valid language.
-  const validResultPromise = test_driver.bless(
-    "Call SpeechRecognition.installOnDevice with a valid language",
-    () => SpeechRecognition.installOnDevice(validLang)
-  );
-  assert_true(
-    validResultPromise instanceof Promise,
-    "installOnDevice (with gesture) should return a Promise."
-  );
-
-  // Verify the resolved value is a boolean.
-  const validResult = await validResultPromise;
-  assert_true(
-    typeof validResult === "boolean",
-    "The resolved value of the installOnDevice promise should be a boolean."
-  );
-
-  // Verify that the method returns true when called with a supported language.
-  assert_equals(
-    validResult,
-    true,
-    "installOnDevice should resolve with `true` when called with a " +
-      "supported language code."
-  );
-
-  // Verify that the newly installed language pack is available.
-  const availableOnDeviceResultPromise =
+  // Check the availablility of the on-device language pack.
+  const initialAvailabilityPromise =
     SpeechRecognition.availableOnDevice(validLang);
   assert_true(
-    availableOnDeviceResultPromise instanceof Promise,
+    initialAvailabilityPromise instanceof Promise,
     "availableOnDevice should return a Promise."
   );
 
-  const availableOnDeviceResult = await availableOnDeviceResultPromise;
+  const initialAvailabilityResult = await initialAvailabilityPromise;
   assert_true(
-    typeof availableOnDeviceResult === "string",
+    typeof initialAvailabilityResult === "string",
     "The resolved value of the availableOnDevice promise should be a string."
   );
 
-  assert_true(
-    availableOnDeviceResult === "available",
-    "The resolved value of the availableOnDevice promise should be 'available'."
-  );
+  if (initialAvailabilityResult === "downloadable") {
+    // Attempt to call installOnDevice directly, without a user gesture with a
+    // language that is downloadable but not installed.
+    const installWithoutUserGesturePromise =
+      SpeechRecognition.installOnDevice(validLang);
 
-  // Verify that installing an already installed language resolves to true.
-  const secondResultPromise = test_driver.bless(
-    "Call SpeechRecognition.installOnDevice for an already installed language",
-    () => SpeechRecognition.installOnDevice(validLang)
-  );
-  assert_true(
-    secondResultPromise instanceof Promise,
-    "installOnDevice (with gesture, for already installed language) should " +
+    // Assert that the promise rejects with NotAllowedError.
+    await promise_rejects_dom(
+      t,
+      "NotAllowedError",
+      window.DOMException,
+      installWithoutUserGesturePromise, "SpeechRecognition.installOnDevice() " +
+      "must reject with NotAllowedError if called without a user gesture."
+    );
+
+    // Test that it returns a promise when called with a valid language.
+    const validResultPromise = test_driver.bless(
+      "Call SpeechRecognition.installOnDevice with a valid language",
+      () => SpeechRecognition.installOnDevice(validLang)
+    );
+    assert_true(
+      validResultPromise instanceof Promise,
+      "installOnDevice (with gesture) should return a Promise."
+    );
+
+    // Verify the resolved value is a boolean.
+    const validResult = await validResultPromise;
+    assert_true(
+      typeof validResult === "boolean",
+      "The resolved value of the installOnDevice promise should be a boolean."
+    );
+
+    // Verify that the method returns true when called with a supported language.
+    assert_equals(
+      validResult,
+      true,
+      "installOnDevice should resolve with `true` when called with a " +
+      "supported language code."
+    );
+
+    // Verify that the newly installed language pack is available.
+    const availableOnDeviceResultPromise =
+      SpeechRecognition.availableOnDevice(validLang);
+    assert_true(
+      availableOnDeviceResultPromise instanceof Promise,
+      "availableOnDevice should return a Promise."
+    );
+
+    const availableOnDeviceResult = await availableOnDeviceResultPromise;
+    assert_true(
+      typeof availableOnDeviceResult === "string",
+      "The resolved value of the availableOnDevice promise should be a string."
+    );
+
+    assert_true(
+      availableOnDeviceResult === "available",
+      "The resolved value of the availableOnDevice promise should be " +
+      "'available'."
+    );
+
+    // Verify that installing an already installed language resolves to true.
+    const secondResultPromise = test_driver.bless(
+      "Call SpeechRecognition.installOnDevice for an already installed language",
+      () => SpeechRecognition.installOnDevice(validLang)
+    );
+    assert_true(
+      secondResultPromise instanceof Promise,
+      "installOnDevice (with gesture, for already installed language) should " +
       "return a Promise."
-  );
-  const secondResult = await secondResultPromise;
-  assert_true(
-    typeof secondResult === "boolean",
-    "The resolved value of the second installOnDevice promise should be a " +
-      "boolean."
-  );
-  assert_equals(
-    secondResult,
-    true,
-    "installOnDevice should resolve with `true` if the language is already " +
+    );
+    const secondResult = await secondResultPromise;
+    assert_true(
+      typeof secondResult === "boolean",
+      "The resolved value of the second installOnDevice promise should be a " +
+        "boolean."
+    );
+    assert_equals(
+      secondResult,
+      true,
+      "installOnDevice should resolve with `true` if the language is already " +
       "installed."
-  );
+    );
+  }
 
   // Test that it returns a promise and resolves to false for unsupported lang.
   const invalidResultPromise = test_driver.bless(
@@ -114,7 +129,7 @@
     invalidResult,
     false,
     "installOnDevice should resolve with `false` when called with an " +
-      "unsupported language code."
+    "unsupported language code."
   );
 }, "SpeechRecognition.installOnDevice resolves with a boolean value " +
    "(with user gesture).");
diff --git a/third_party/boringssl/src b/third_party/boringssl/src
index 136284f..2a514a5 160000
--- a/third_party/boringssl/src
+++ b/third_party/boringssl/src
@@ -1 +1 @@
-Subproject commit 136284f8548bc7fb43e99e7f69e03fab57168e8b
+Subproject commit 2a514a51baebd5a232fc64f7b082f7a8b28cd29d
diff --git a/third_party/catapult b/third_party/catapult
index 7701728..83c00a3 160000
--- a/third_party/catapult
+++ b/third_party/catapult
@@ -1 +1 @@
-Subproject commit 77017288fa6b829b2d31aa8195799ea78e594128
+Subproject commit 83c00a37f40a93932204f684172a13c62a379e7e
diff --git a/third_party/compiler-rt/src b/third_party/compiler-rt/src
index 948a465..b5f72d2 160000
--- a/third_party/compiler-rt/src
+++ b/third_party/compiler-rt/src
@@ -1 +1 @@
-Subproject commit 948a46574ce380c306ae4f01d2a630d93847b88f
+Subproject commit b5f72d2ab6190b6cea33512880e6cbfe3648d513
diff --git a/third_party/depot_tools b/third_party/depot_tools
index 7d18f85..14bfda1 160000
--- a/third_party/depot_tools
+++ b/third_party/depot_tools
@@ -1 +1 @@
-Subproject commit 7d18f854503a26cb29540012327ad3f78926de96
+Subproject commit 14bfda17088cb03d7bc0ba7df6cac2699e051e14
diff --git a/third_party/devtools-frontend/src b/third_party/devtools-frontend/src
index 98f5274..5822117 160000
--- a/third_party/devtools-frontend/src
+++ b/third_party/devtools-frontend/src
@@ -1 +1 @@
-Subproject commit 98f52748ef939c7af67e84f07dd27ceadf35f275
+Subproject commit 58221174205b00e188e181a3cbb1d63e05d23331
diff --git a/third_party/libc++abi/src b/third_party/libc++abi/src
index 1efb5e6..5a49db9 160000
--- a/third_party/libc++abi/src
+++ b/third_party/libc++abi/src
@@ -1 +1 @@
-Subproject commit 1efb5e6d7c5eee01624f3730a935285405c9cd22
+Subproject commit 5a49db9990ee2ecee2bc7340be9756c0455bde6f
diff --git a/third_party/libunwind/src b/third_party/libunwind/src
index 09f6f7b..2bd5f3c 160000
--- a/third_party/libunwind/src
+++ b/third_party/libunwind/src
@@ -1 +1 @@
-Subproject commit 09f6f7b1685888b16c0fe52a7a68f9dff8b8d60e
+Subproject commit 2bd5f3cae13e8ad6727d0a77a2ec9cdc6b06becb
diff --git a/third_party/llvm-libc/src b/third_party/llvm-libc/src
index 4a2940b4..a0ab545 160000
--- a/third_party/llvm-libc/src
+++ b/third_party/llvm-libc/src
@@ -1 +1 @@
-Subproject commit 4a2940b40b394ca57312aa9bbc8af430fe9a5340
+Subproject commit a0ab545a0eaa7a8cd7154025d1902904e1800cfc
diff --git a/third_party/perfetto b/third_party/perfetto
index bff3408..4402fd7 160000
--- a/third_party/perfetto
+++ b/third_party/perfetto
@@ -1 +1 @@
-Subproject commit bff34086ee3abf81659b5184c243f4cf2f799992
+Subproject commit 4402fd7953d5dedf65659e99ab80404f6e94a004
diff --git a/third_party/vulkan-deps b/third_party/vulkan-deps
index 6b14cce..cd7971b 160000
--- a/third_party/vulkan-deps
+++ b/third_party/vulkan-deps
@@ -1 +1 @@
-Subproject commit 6b14cce1d656d873db79be9c88dd5d343bd66f59
+Subproject commit cd7971b83f29d029d06895f39630cffa5ec421f1
diff --git a/third_party/vulkan-validation-layers/src b/third_party/vulkan-validation-layers/src
index 21baa6b..f74722e 160000
--- a/third_party/vulkan-validation-layers/src
+++ b/third_party/vulkan-validation-layers/src
@@ -1 +1 @@
-Subproject commit 21baa6bb2e0d5f4ae093397733f4534ea6e8cd6e
+Subproject commit f74722ee649f9c9cc7daa7d13d434febc424e7a4
diff --git a/third_party/webgpu-cts/src b/third_party/webgpu-cts/src
index e25f9bec..71e9fad 160000
--- a/third_party/webgpu-cts/src
+++ b/third_party/webgpu-cts/src
@@ -1 +1 @@
-Subproject commit e25f9bec845320e76ac95e0cf57d2656f4dd5801
+Subproject commit 71e9fad6cf757c1d6ce2ec56c5201dd42eec0aa3
diff --git a/tools/clang/spanify/Spanifier.cpp b/tools/clang/spanify/Spanifier.cpp
index d604d04..279fe7c 100644
--- a/tools/clang/spanify/Spanifier.cpp
+++ b/tools/clang/spanify/Spanifier.cpp
@@ -172,6 +172,34 @@
   return true;
 }
 
+struct UnsafeFreeFuncToMacro {
+  // The name of an unsafe free function to be rewritten.
+  const std::string_view function_name;
+  // The helper macro name to be rewritten to.
+  const std::string_view macro_name;
+};
+
+std::optional<UnsafeFreeFuncToMacro> FindUnsafeFreeFuncToBeRewrittenToMacro(
+    const clang::FunctionDecl* function_decl) {
+  // The table of unsafe free functions to be rewritten to helper macro calls.
+  // Note that C++20 is not supported in tools/clang/spanify/ and we cannot use
+  // std::to_array.
+  static constexpr UnsafeFreeFuncToMacro unsafe_free_func_table[] = {
+      // https://source.chromium.org/chromium/chromium/src/+/main:third_party/perl/c/include/harfbuzz/hb-buffer.h;drc=6f3e5028eb65d0b4c5fdd792106ac4c84eee1eb3;l=442
+      {"hb_buffer_get_glyph_positions", "UNSAFE_HB_BUFFER_GET_GLYPH_POSITIONS"},
+  };
+
+  const std::string& function_name = function_decl->getQualifiedNameAsString();
+
+  for (const auto& entry : unsafe_free_func_table) {
+    if (function_name == entry.function_name) {
+      return entry;
+    }
+  }
+
+  return std::nullopt;
+}
+
 struct UnsafeCxxMethodToMacro {
   // The qualified class name of an unsafe method to be rewritten.
   const std::string_view class_name;
@@ -207,9 +235,13 @@
   return std::nullopt;
 }
 
-AST_MATCHER(clang::CXXMethodDecl, unsafeCxxMethodToBeRewrittenToMacro) {
-  const clang::CXXMethodDecl* method_decl = &Node;
-  return bool(FindUnsafeCxxMethodToBeRewrittenToMacro(method_decl));
+AST_MATCHER(clang::FunctionDecl, unsafeFunctionToBeRewrittenToMacro) {
+  const clang::FunctionDecl* function_decl = &Node;
+  if (const clang::CXXMethodDecl* method_decl =
+          clang::dyn_cast<clang::CXXMethodDecl>(function_decl)) {
+    return bool(FindUnsafeCxxMethodToBeRewrittenToMacro(method_decl));
+  }
+  return bool(FindUnsafeFreeFuncToBeRewrittenToMacro(function_decl));
 }
 
 // Convert a number to a string with leading zeros. This is useful to ensure
@@ -1107,7 +1139,7 @@
                ", 1u)", source_manager));
 }
 
-// Rewrites unsafe third-party function calls to helper macro calls.
+// Rewrites unsafe third-party member function calls to helper macro calls.
 //
 // Example)
 //     SkBitmap sk_bitmap;
@@ -1121,18 +1153,19 @@
 //     int tmp_width = sk_bitmap.width();
 //     base::span<uint32_t> image_row(tmp_row, tmp_width - x);
 //
-// Tests are in: unsafe-cxx-methods-original.cc and
+// Tests are in: unsafe-function-to-macro-original.cc and
 // //base/containers/auto_spanification_helper_unittest.cc
-static std::string getNodeFromUnsafeCxxMethodCall(
+static std::string GetNodeFromUnsafeCxxMethodCall(
     const clang::Expr* size_expr,
     const clang::CXXMemberCallExpr* member_call_expr,
     const MatchFinder::MatchResult& result) {
   const clang::SourceManager& source_manager = *result.SourceManager;
 
   const auto* method_decl = GetNodeOrCrash<clang::CXXMethodDecl>(
-      result, "unsafe_cxx_method_decl",
-      "`unsafe_cxx_method_call_expr` implies `unsafe_cxx_method_decl`");
-  // The match with using `unsafeCxxMethodToBeRewrittenToMacro` guarantees that
+      result, "unsafe_function_decl",
+      "`unsafe_function_call_expr` in clang::CXXMemberCallExpr implies "
+      "`unsafe_function_decl` in clang::CXXMethodDecl");
+  // The match with using `unsafeFunctionToBeRewrittenToMacro` guarantees that
   // there exists an `UnsafeCxxMethodToMacro` instance, so the following
   // "Find..." always succeeds.
   const UnsafeCxxMethodToMacro entry =
@@ -1188,6 +1221,65 @@
   return key;
 }
 
+// Rewrites unsafe third-party free function calls to helper macro calls.
+//
+// Example)
+//     struct hb_glyph_position_t* positions =
+//         hb_buffer_get_glyph_positions(&buffer, &length);
+// will be rewritten to
+//     base::span<hb_glyph_position_t> positions =
+//         UNSAFE_HB_BUFFER_GET_GLYPH_POSITIONS(&buffer, &length);
+// where the macro performs essentially the following.
+//     hb_glyph_position_t* tmp_pos =
+//         hb_buffer_get_glyph_positions(&buffer, &length);
+//     base::span<hb_glyph_position_t> positions(tmp_pos, length);
+//
+// Tests are in: unsafe-function-to-macro-original.cc and
+// //base/containers/auto_spanification_helper_unittest.cc
+static std::string GetNodeFromUnsafeFreeFuncCall(
+    const clang::Expr* size_expr,
+    const clang::CallExpr* call_expr,
+    const MatchFinder::MatchResult& result) {
+  const clang::SourceManager& source_manager = *result.SourceManager;
+
+  const auto* function_decl = GetNodeOrCrash<clang::FunctionDecl>(
+      result, "unsafe_function_decl",
+      "`unsafe_function_call_expr` implies `unsafe_function_decl`");
+  // The match with using `unsafeFunctionToBeRewrittenToMacro` guarantees that
+  // there exists an `UnsafeFreeFuncToMacro` instance, so the following
+  // "Find..." always succeeds.
+  const UnsafeFreeFuncToMacro entry =
+      FindUnsafeFreeFuncToBeRewrittenToMacro(function_decl).value();
+
+  // `key` is compatible with getNodeFromSizeExpr.
+  const std::string& key = NodeKey(size_expr, source_manager);
+
+  // Replace the function name with the macro name.
+  const clang::SourceLocation& func_loc = call_expr->getCallee()->getBeginLoc();
+  EmitReplacement(
+      key, GetReplacementDirective(
+               clang::SourceRange(func_loc, func_loc.getLocWithOffset(
+                                                entry.function_name.length())),
+               std::string(entry.macro_name), source_manager));
+
+  EmitReplacement(
+      key, GetIncludeDirective(size_expr->getSourceRange(), source_manager,
+                               kBaseAutoSpanificationHelperIncludePath));
+  EmitSink(key);
+  return key;
+}
+
+static std::string GetNodeFromUnsafeFunctionCall(
+    const clang::Expr* size_expr,
+    const clang::CallExpr* call_expr,
+    const MatchFinder::MatchResult& result) {
+  if (const clang::CXXMemberCallExpr* member_call_expr =
+          clang::dyn_cast<clang::CXXMemberCallExpr>(call_expr)) {
+    return GetNodeFromUnsafeCxxMethodCall(size_expr, member_call_expr, result);
+  }
+  return GetNodeFromUnsafeFreeFuncCall(size_expr, call_expr, result);
+}
+
 static std::string getNodeFromSizeExpr(const clang::Expr* size_expr,
                                        const MatchFinder::MatchResult& result) {
   const clang::SourceManager& source_manager = *result.SourceManager;
@@ -2191,16 +2283,11 @@
           result.Nodes.getNodeAs<clang::Expr>("size_node")) {
     // "size_node" assumes that third party functions that return a buffer
     // provide some way to know the size, however special handling is required
-    // to extract that, thus here we add support for classes that have methods
-    // returning a buffer that also have size support.
-    //
-    // TODO(yukishiino): Add support for non-member functions that return
-    // buffers.
-    if (const auto* member_call_expr =
-            result.Nodes.getNodeAs<clang::CXXMemberCallExpr>(
-                "unsafe_cxx_method_call_expr")) {
-      return getNodeFromUnsafeCxxMethodCall(size_expr, member_call_expr,
-                                            result);
+    // to extract that, thus here we add support for functions returning a
+    // buffer that also have size support.
+    if (const auto* unsafe_call_expr = result.Nodes.getNodeAs<clang::CallExpr>(
+            "unsafe_function_call_expr")) {
+      return GetNodeFromUnsafeFunctionCall(size_expr, unsafe_call_expr, result);
     }
     return getNodeFromSizeExpr(size_expr, result);
   }
@@ -2441,18 +2528,17 @@
     //                  which is a subset of size_node.
     auto size_node_matcher = expr(anyOf(
         member_data_call,
-        expr(anyOf(
-                 cxxMemberCallExpr(
-                     callee(cxxMethodDecl(unsafeCxxMethodToBeRewrittenToMacro())
-                                .bind("unsafe_cxx_method_decl")))
-                     .bind("unsafe_cxx_method_call_expr"),
-                 callExpr(callee(functionDecl(
-                     hasReturnTypeLoc(pointerTypeLoc()),
-                     anyOf(raw_ptr_plugin::isInThirdPartyLocation(),
-                           isExpansionInSystemHeader(),
-                           raw_ptr_plugin::isInExternCContext())))),
-                 cxxNullPtrLiteralExpr().bind("nullptr_expr"), cxxNewExpr(),
-                 buff_address_from_container, buff_address_from_single_var))
+        expr(anyOf(callExpr(
+                       callee(functionDecl(unsafeFunctionToBeRewrittenToMacro())
+                                  .bind("unsafe_function_decl")))
+                       .bind("unsafe_function_call_expr"),
+                   callExpr(callee(functionDecl(
+                       hasReturnTypeLoc(pointerTypeLoc()),
+                       anyOf(raw_ptr_plugin::isInThirdPartyLocation(),
+                             isExpansionInSystemHeader(),
+                             raw_ptr_plugin::isInExternCContext())))),
+                   cxxNullPtrLiteralExpr().bind("nullptr_expr"), cxxNewExpr(),
+                   buff_address_from_container, buff_address_from_single_var))
             .bind("size_node")));
 
     auto rhs_expr =
diff --git a/tools/clang/spanify/tests/third_party/do_not_rewrite/third_party_api.h b/tools/clang/spanify/tests/third_party/do_not_rewrite/third_party_api.h
index 8aecdb9..ff3a119 100644
--- a/tools/clang/spanify/tests/third_party/do_not_rewrite/third_party_api.h
+++ b/tools/clang/spanify/tests/third_party/do_not_rewrite/third_party_api.h
@@ -22,4 +22,9 @@
   uint32_t* getAddr32(int x, int y) const;
 };
 
+struct hb_glyph_position_t {};
+struct hb_buffer_t {};
+hb_glyph_position_t* hb_buffer_get_glyph_positions(hb_buffer_t* buffer,
+                                                   unsigned int* length);
+
 #endif  // TOOLS_CLANG_SPANIFY_TESTS_THIRD_PARTY_DO_NOT_REWRITE_THIRD_PARTY_API_H_
diff --git a/tools/clang/spanify/tests/unsafe-cxx-methods-expected.cc b/tools/clang/spanify/tests/unsafe-function-to-macro-expected.cc
similarity index 81%
rename from tools/clang/spanify/tests/unsafe-cxx-methods-expected.cc
rename to tools/clang/spanify/tests/unsafe-function-to-macro-expected.cc
index 1883a74..33fda8c99 100644
--- a/tools/clang/spanify/tests/unsafe-cxx-methods-expected.cc
+++ b/tools/clang/spanify/tests/unsafe-function-to-macro-expected.cc
@@ -10,6 +10,8 @@
 #include "base/memory/raw_ptr.h"
 #include "third_party/do_not_rewrite/third_party_api.h"
 
+// ---- Test cases of C++ member function calls ----------------------------
+
 SkBitmap* GetSkBitmap();
 
 void test_no_arg() {
@@ -75,3 +77,16 @@
   base::span<uint32_t> image_row = UNSAFE_SKBITMAP_GETADDR32(sk_bitmap, 1, 2);
   std::ignore = image_row[0];
 }
+
+// ---- Test cases of C/C++ free function calls ----------------------------
+
+void test_hb_buffer_get_glyph_positions() {
+  struct hb_buffer_t buffer;
+  unsigned int length;
+  // Expected rewrite:
+  // base::span<struct hb_glyph_position_t> positions =
+  //     UNSAFE_HB_BUFFER_GET_GLYPH_POSITIONS(&buffer, &length);
+  base::span<struct hb_glyph_position_t> positions =
+      UNSAFE_HB_BUFFER_GET_GLYPH_POSITIONS(&buffer, &length);
+  std::ignore = positions[0];
+}
diff --git a/tools/clang/spanify/tests/unsafe-cxx-methods-original.cc b/tools/clang/spanify/tests/unsafe-function-to-macro-original.cc
similarity index 80%
rename from tools/clang/spanify/tests/unsafe-cxx-methods-original.cc
rename to tools/clang/spanify/tests/unsafe-function-to-macro-original.cc
index 5029520..08963d4 100644
--- a/tools/clang/spanify/tests/unsafe-cxx-methods-original.cc
+++ b/tools/clang/spanify/tests/unsafe-function-to-macro-original.cc
@@ -8,6 +8,8 @@
 #include "base/memory/raw_ptr.h"
 #include "third_party/do_not_rewrite/third_party_api.h"
 
+// ---- Test cases of C++ member function calls ----------------------------
+
 SkBitmap* GetSkBitmap();
 
 void test_no_arg() {
@@ -71,3 +73,16 @@
   uint32_t* image_row = sk_bitmap->getAddr32(1, 2);
   std::ignore = image_row[0];
 }
+
+// ---- Test cases of C/C++ free function calls ----------------------------
+
+void test_hb_buffer_get_glyph_positions() {
+  struct hb_buffer_t buffer;
+  unsigned int length;
+  // Expected rewrite:
+  // base::span<struct hb_glyph_position_t> positions =
+  //     UNSAFE_HB_BUFFER_GET_GLYPH_POSITIONS(&buffer, &length);
+  struct hb_glyph_position_t* positions =
+      hb_buffer_get_glyph_positions(&buffer, &length);
+  std::ignore = positions[0];
+}
diff --git a/tools/crates/create_update_cl.py b/tools/crates/create_update_cl.py
index dfbbd06d..c0cf940 100755
--- a/tools/crates/create_update_cl.py
+++ b/tools/crates/create_update_cl.py
@@ -9,6 +9,7 @@
 
 import argparse
 import datetime
+import fnmatch
 import os
 import re
 import shlex
@@ -704,11 +705,15 @@
 
     todo_crate_updates = FindUpdateableCrates(args)
     if args.skip:
-        todo_crate_updates = list([
-            (old_crate_id, new_crate_id)
-            for (old_crate_id, new_crate_id) in todo_crate_updates
-            if not ConvertCrateIdToCrateName(old_crate_id) in args.skip
-        ])
+        todo_crate_updates_without_skips = []
+        for old_crate_id, new_crate_id in todo_crate_updates:
+            crate_name = ConvertCrateIdToCrateName(old_crate_id)
+            if not any(
+                    fnmatch.fnmatch(crate_name, pattern)
+                    for pattern in args.skip):
+                todo_crate_updates_without_skips.append(
+                    (old_crate_id, new_crate_id))
+        todo_crate_updates = todo_crate_updates_without_skips
 
     if not todo_crate_updates:
         print("There were no updates - exiting early...")
diff --git a/tools/crates/gnrt/vendor.rs b/tools/crates/gnrt/vendor.rs
index 9b4f3c04..62f1bb18 100644
--- a/tools/crates/gnrt/vendor.rs
+++ b/tools/crates/gnrt/vendor.rs
@@ -140,7 +140,11 @@
             if skip_patches {
                 log::warn!("Skipped applying patches for {}", &crate_dirname);
             } else {
-                apply_patches(p.name(), p.version(), paths)?
+                apply_patches(p.name(), p.version(), paths).context(
+                    "Applying patches failed - hopefully \
+                     `third_party/rust/chromium_crates_io/patches/README.md` \
+                     provides some useful guidance for the next steps...",
+                )?;
             }
         }
     }
diff --git a/tools/metrics/actions/actions.xml b/tools/metrics/actions/actions.xml
index b50aecb1..16374e9 100644
--- a/tools/metrics/actions/actions.xml
+++ b/tools/metrics/actions/actions.xml
@@ -30654,7 +30654,6 @@
 </action>
 
 <action name="Options_EasyUnlockRequireProximity_Disable">
-  <owner>isherman@chromium.org</owner>
   <owner>tengs@chromium.org</owner>
   <description>
     The user disabled the option to require their phone to be in very close
@@ -30664,7 +30663,6 @@
 </action>
 
 <action name="Options_EasyUnlockRequireProximity_Enable">
-  <owner>isherman@chromium.org</owner>
   <owner>tengs@chromium.org</owner>
   <description>
     The user enabled the option to require their phone to be in very close
diff --git a/tools/metrics/histograms/enums.xml b/tools/metrics/histograms/enums.xml
index b9904e0..8fd3c98 100644
--- a/tools/metrics/histograms/enums.xml
+++ b/tools/metrics/histograms/enums.xml
@@ -2957,6 +2957,7 @@
   <int value="35" label="RESULT_CODE_NORMAL_EXIT_UPGRADE_RELAUNCHED"/>
   <int value="36" label="RESULT_CODE_NORMAL_EXIT_PACK_EXTENSION_SUCCESS"/>
   <int value="37" label="RESULT_CODE_SYSTEM_RESOURCE_EXHAUSTED"/>
+  <int value="38" label="RESULT_CODE_NORMAL_EXIT_AUTO_DE_ELEVATED"/>
   <int value="131" label="SIGQUIT"/>
   <int value="132" label="SIGILL"/>
   <int value="133" label="SIGTRAP"/>
diff --git a/tools/metrics/histograms/metadata/accessibility/histograms.xml b/tools/metrics/histograms/metadata/accessibility/histograms.xml
index 30b679e..5a6129e6c 100644
--- a/tools/metrics/histograms/metadata/accessibility/histograms.xml
+++ b/tools/metrics/histograms/metadata/accessibility/histograms.xml
@@ -40,6 +40,11 @@
   <variant name="VirtualKeyboard"/>
 </variants>
 
+<variants name="ScreenAIServices">
+  <variant name="MainContentExtraction" summary="Main Content Extraction"/>
+  <variant name="OCR" summary="OCR"/>
+</variants>
+
 <variants name="SodaLanguageCode">
   <variant name="de-DE" summary="German language code"/>
   <variant name="en-US" summary="English language code"/>
@@ -2188,6 +2193,34 @@
   </summary>
 </histogram>
 
+<histogram name="Accessibility.OCR.Service.MemoryBefore.{status}.Available"
+    units="MB" expires_after="2026-03-01">
+  <owner>rhalavati@chromium.org</owner>
+  <owner>chrome-a11y-core@google.com</owner>
+  <summary>
+    Available memory before launching OCR service, when service {status}.
+  </summary>
+  <token key="status" variants="TerminationStatus"/>
+</histogram>
+
+<histogram name="Accessibility.OCR.Service.MemoryBefore.{status}.Pressure"
+    enum="MemoryPressureLevel" expires_after="2026-03-01">
+  <owner>rhalavati@chromium.org</owner>
+  <owner>chrome-a11y-core@google.com</owner>
+  <summary>
+    Memory pressure level before launching OCR service, when service {status}.
+  </summary>
+  <token key="status" variants="TerminationStatus"/>
+</histogram>
+
+<histogram name="Accessibility.OCR.Service.MemoryBefore.{status}.Total"
+    units="MB" expires_after="2026-03-01">
+  <owner>rhalavati@chromium.org</owner>
+  <owner>chrome-a11y-core@google.com</owner>
+  <summary>Total memory when OCR service {status}.</summary>
+  <token key="status" variants="TerminationStatus"/>
+</histogram>
+
 <histogram name="Accessibility.OOBEStartupSoundDelay" units="ms"
     expires_after="never">
 <!-- expires-never: Core metric for monitoring OOBE accessibility status. -->
@@ -3368,83 +3401,6 @@
   </summary>
 </histogram>
 
-<histogram name="Accessibility.ScreenAI.Service.CrashCountBeforeResume"
-    units="count" expires_after="2026-03-06">
-  <owner>rhalavati@chromium.org</owner>
-  <owner>chrome-a11y-core@google.com</owner>
-  <summary>
-    Records the number of ScreenAI service crashes before one successful
-    shutdown. It is recorded when the service shuts down due to being idle, and
-    had crashed before and restarted.
-  </summary>
-</histogram>
-
-<histogram name="Accessibility.ScreenAI.Service.Initialization"
-    enum="BooleanSuccess" expires_after="2025-10-26">
-  <owner>rhalavati@chromium.org</owner>
-  <owner>chrome-a11y-core@google.com</owner>
-  <summary>
-    Records if launching the ScreenAI service was successful or not.
-  </summary>
-</histogram>
-
-<histogram name="Accessibility.ScreenAI.Service.InitializationTime.{Result}"
-    units="ms" expires_after="2025-10-26">
-  <owner>rhalavati@chromium.org</owner>
-  <owner>chrome-a11y-core@google.com</owner>
-  <summary>
-    Records how long it took for the ScreenAI service to {Result}.
-  </summary>
-  <token key="Result">
-    <variant name="Failure" summary="fail initialization"/>
-    <variant name="Success" summary="initialize successfully"/>
-  </token>
-</histogram>
-
-<histogram name="Accessibility.ScreenAI.Service.IsSuspended" enum="Boolean"
-    expires_after="2026-03-06">
-  <owner>rhalavati@chromium.org</owner>
-  <owner>chrome-a11y-core@google.com</owner>
-  <summary>
-    Records if ScreenAI service was suspended due to crash. Recorded when a
-    client asks for service availablity or for connection to the service.
-  </summary>
-</histogram>
-
-<histogram
-    name="Accessibility.ScreenAI.Service.MemoryBefore.{status}.Available"
-    units="MB" expires_after="2026-03-01">
-  <owner>rhalavati@chromium.org</owner>
-  <owner>chrome-a11y-core@google.com</owner>
-  <summary>
-    Available memory before launching ScreenAI service, when service {status}.
-    This metric is recorded only if OCR feature is initialized.
-  </summary>
-  <token key="status" variants="TerminationStatus"/>
-</histogram>
-
-<histogram name="Accessibility.ScreenAI.Service.MemoryBefore.{status}.Pressure"
-    enum="MemoryPressureLevel" expires_after="2026-03-01">
-  <owner>rhalavati@chromium.org</owner>
-  <owner>chrome-a11y-core@google.com</owner>
-  <summary>
-    Memory pressure level before launching ScreenAI service, when service
-    {status}. This metric is recorded only if OCR feature is initialized.
-  </summary>
-  <token key="status" variants="TerminationStatus"/>
-</histogram>
-
-<histogram name="Accessibility.ScreenAI.Service.MemoryBefore.{status}.Total"
-    units="MB" expires_after="2026-03-01">
-  <owner>rhalavati@chromium.org</owner>
-  <owner>chrome-a11y-core@google.com</owner>
-  <summary>
-    Total memory when ScreenAI service {status}. This metric is recorded only if
-    OCR feature is initialized.
-  </summary>
-  <token key="status" variants="TerminationStatus"/>
-</histogram>
-
 <histogram name="Accessibility.ScreenAI.Service.NotReponsive.IsOCR"
     enum="BooleanYesNo" expires_after="2026-03-21">
   <owner>rhalavati@chromium.org</owner>
@@ -3455,18 +3411,15 @@
   </summary>
 </histogram>
 
-<histogram name="Accessibility.ScreenAI.{feature}.InitializationLatency"
+<histogram name="Accessibility.ScreenAI.{ServiceName}.InitializationLatency"
     units="ms" expires_after="2025-09-07">
   <owner>rhalavati@chromium.org</owner>
   <owner>chrome-a11y-core@google.com</owner>
   <summary>
-    Records how long it takes to initialize {feature} in Screen AI service.
+    Records how long it takes to initialize {ServiceName} functionality in
+    Screen AI service.
   </summary>
-  <token key="feature">
-    <variant name="MainContentExtraction"
-        summary="Main Content Extraction functionality"/>
-    <variant name="OCR" summary="OCR functionality"/>
-  </token>
+  <token key="ServiceName" variants="ScreenAIServices"/>
 </histogram>
 
 <histogram name="Accessibility.ScreenAI.{Step}.Initialized"
@@ -3693,6 +3646,54 @@
   </token>
 </histogram>
 
+<histogram name="Accessibility.{ServiceName}.Service.CrashCountBeforeResume"
+    units="count" expires_after="2026-03-06">
+  <owner>rhalavati@chromium.org</owner>
+  <owner>chrome-a11y-core@google.com</owner>
+  <summary>
+    Records the number of {ServiceName} service crashes before one successful
+    shutdown. It is recorded when the service shuts down due to being idle, and
+    had crashed before and restarted.
+  </summary>
+  <token key="ServiceName" variants="ScreenAIServices"/>
+</histogram>
+
+<histogram name="Accessibility.{ServiceName}.Service.Initialization"
+    enum="BooleanSuccess" expires_after="2025-10-26">
+  <owner>rhalavati@chromium.org</owner>
+  <owner>chrome-a11y-core@google.com</owner>
+  <summary>
+    Records if launching the {ServiceName} service was successful or not.
+  </summary>
+  <token key="ServiceName" variants="ScreenAIServices"/>
+</histogram>
+
+<histogram
+    name="Accessibility.{ServiceName}.Service.InitializationTime.{Result}"
+    units="ms" expires_after="2025-10-26">
+  <owner>rhalavati@chromium.org</owner>
+  <owner>chrome-a11y-core@google.com</owner>
+  <summary>
+    Records how long it took for the {ServiceName} service to {Result}.
+  </summary>
+  <token key="Result">
+    <variant name="Failure" summary="fail initialization"/>
+    <variant name="Success" summary="initialize successfully"/>
+  </token>
+  <token key="ServiceName" variants="ScreenAIServices"/>
+</histogram>
+
+<histogram name="Accessibility.{ServiceName}.Service.IsSuspended"
+    enum="Boolean" expires_after="2026-03-06">
+  <owner>rhalavati@chromium.org</owner>
+  <owner>chrome-a11y-core@google.com</owner>
+  <summary>
+    Records if {ServiceName} service was suspended due to crash. Recorded when a
+    client asks for service availablity or for connection to the service.
+  </summary>
+  <token key="ServiceName" variants="ScreenAIServices"/>
+</histogram>
+
 <histogram name="DomDistiller.AdaBoostModel.NegativeScore" units="count"
     expires_after="2025-10-26">
   <owner>wylieb@google.com</owner>
diff --git a/tools/metrics/histograms/metadata/apps/histograms.xml b/tools/metrics/histograms/metadata/apps/histograms.xml
index f6cfb07..7ef8414 100644
--- a/tools/metrics/histograms/metadata/apps/histograms.xml
+++ b/tools/metrics/histograms/metadata/apps/histograms.xml
@@ -652,7 +652,7 @@
 </histogram>
 
 <histogram name="Apps.AppList.DriveSearchProvider.DriveFSLatency" units="ms"
-    expires_after="2025-09-14">
+    expires_after="2026-03-01">
   <owner>chenjih@google.com</owner>
   <owner>ypitsishin@google.com</owner>
   <owner>chromeos-launcher-search@google.com</owner>
@@ -1133,7 +1133,7 @@
 </histogram>
 
 <histogram name="Apps.AppList.Search.ContinueResultCount.{Type}" units="count"
-    expires_after="2025-08-10">
+    expires_after="2026-05-01">
   <owner>chenjih@google.com</owner>
   <owner>ypitsishin@google.com</owner>
   <owner>chromeos-launcher-search@google.com</owner>
@@ -1507,7 +1507,7 @@
 </histogram>
 
 <histogram name="Apps.AppList.SearchSuccess.Apps" enum="AppListLaunchedFrom"
-    expires_after="2025-07-31">
+    expires_after="2026-05-01">
   <owner>chenjih@google.com</owner>
   <owner>ypitsishin@google.com</owner>
   <owner>chromeos-launcher-search@google.com</owner>
@@ -1573,7 +1573,7 @@
 </histogram>
 
 <histogram name="Apps.AppList.SuggestedContent.Enabled" enum="BooleanEnabled"
-    expires_after="2025-07-31">
+    expires_after="2026-05-01">
   <owner>chenjih@google.com</owner>
   <owner>ypitsishin@google.com</owner>
   <owner>chromeos-launcher-search@google.com</owner>
@@ -1750,7 +1750,7 @@
 </histogram>
 
 <histogram name="Apps.AppList.ZeroStateFileProvider.Latency" units="ms"
-    expires_after="2025-07-31">
+    expires_after="2026-03-01">
   <owner>chenjih@google.com</owner>
   <owner>ypitsishin@google.com</owner>
   <owner>chromeos-launcher-search@google.com</owner>
@@ -1775,7 +1775,7 @@
 </histogram>
 
 <histogram name="Apps.AppList.ZeroStateResults.LaunchedItemType"
-    enum="ZeroStateResultType" expires_after="2025-07-31">
+    enum="ZeroStateResultType" expires_after="2026-05-01">
   <owner>chenjih@google.com</owner>
   <owner>ypitsishin@google.com</owner>
   <owner>chromeos-launcher-search@google.com</owner>
@@ -2132,7 +2132,7 @@
 </histogram>
 
 <histogram name="Apps.AppListPlayStoreQueryState"
-    enum="AppListPlayStoreQueryState" expires_after="2025-06-15">
+    enum="AppListPlayStoreQueryState" expires_after="2026-05-01">
   <owner>tby@chromium.org</owner>
   <owner>ypitsishin@google.com</owner>
   <owner>chrome-knowledge-eng@google.com</owner>
diff --git a/tools/metrics/histograms/metadata/blink/enums.xml b/tools/metrics/histograms/metadata/blink/enums.xml
index 867aa47..ec8de8e9 100644
--- a/tools/metrics/histograms/metadata/blink/enums.xml
+++ b/tools/metrics/histograms/metadata/blink/enums.xml
@@ -6206,6 +6206,7 @@
   <int value="5576" label="CredentialsGetImmediateMediationFailure"/>
   <int value="5577" label="ClearSiteData"/>
   <int value="5578" label="ScrollIntoViewContainerNearest"/>
+  <int value="5579" label="PopoverShown"/>
 </enum>
 
 <!-- LINT.ThenChange(//third_party/blink/public/mojom/use_counter/metrics/web_feature.mojom:WebFeature) -->
diff --git a/tools/metrics/histograms/metadata/bookmarks/histograms.xml b/tools/metrics/histograms/metadata/bookmarks/histograms.xml
index 60d572f..136fdf2 100644
--- a/tools/metrics/histograms/metadata/bookmarks/histograms.xml
+++ b/tools/metrics/histograms/metadata/bookmarks/histograms.xml
@@ -234,9 +234,6 @@
 
 <histogram name="Bookmarks.Count.OnProfileLoad3" units="bookmarks"
     expires_after="2025-10-26">
-  <owner>supertri@chromium.org</owner>
-  <owner>isherman@chromium.org</owner>
-  <owner>aidanday@google.com</owner>
   <owner>mamir@chromium.org</owner>
   <summary>
     The total number of bookmarks a user has saved, excluding folders. Recorded
@@ -428,7 +425,7 @@
 </histogram>
 
 <histogram name="Bookmarks.ParentFolderType" enum="BookmarkFolderType"
-    expires_after="2025-10-12">
+    expires_after="2026-01-31">
   <owner>emshack@chromium.org</owner>
   <owner>top-chrome-desktop-ui@google.com</owner>
   <summary>
@@ -734,7 +731,7 @@
 </histogram>
 
 <histogram name="Bookmarks.UtilizationPerBookmark.OnProfileLoad.DaysSinceUsed"
-    units="Days" expires_after="2025-06-22">
+    units="Days" expires_after="2026-01-31">
   <owner>emshack@chromium.org</owner>
   <owner>chrome-collections@google.com</owner>
   <summary>
@@ -764,7 +761,7 @@
 </histogram>
 
 <histogram name="PowerBookmarks.SidePanel.BookmarksShown" units="Bookmarks"
-    expires_after="2025-10-26">
+    expires_after="2026-01-31">
   <owner>emshack@chromium.org</owner>
   <owner>chrome-desktop-sea@google.com</owner>
   <component>1457140</component>
@@ -776,7 +773,7 @@
 </histogram>
 
 <histogram name="PowerBookmarks.SidePanel.Search.CTR"
-    enum="BookmarksSidePanelSearchCTREvent" expires_after="2025-10-26">
+    enum="BookmarksSidePanelSearchCTREvent" expires_after="2026-01-31">
   <owner>emshack@chromium.org</owner>
   <owner>chrome-desktop-sea@google.com</owner>
   <component>1457140</component>
@@ -788,7 +785,7 @@
 </histogram>
 
 <histogram name="PowerBookmarks.SidePanel.SearchOrFilter.BookmarksShown"
-    units="Bookmarks" expires_after="2025-10-26">
+    units="Bookmarks" expires_after="2026-01-31">
   <owner>emshack@chromium.org</owner>
   <owner>chrome-desktop-sea@google.com</owner>
   <component>1457140</component>
@@ -800,7 +797,7 @@
 </histogram>
 
 <histogram name="PowerBookmarks.SidePanel.SortTypeShown"
-    enum="BookmarksSidePanelSortType" expires_after="2025-06-22">
+    enum="BookmarksSidePanelSortType" expires_after="2026-01-31">
   <owner>emshack@chromium.org</owner>
   <owner>chrome-desktop-sea@google.com</owner>
   <component>1457140</component>
@@ -811,7 +808,7 @@
 </histogram>
 
 <histogram name="PowerBookmarks.SidePanel.ViewTypeShown"
-    enum="BookmarksSidePanelViewType" expires_after="2025-06-22">
+    enum="BookmarksSidePanelViewType" expires_after="2026-01-31">
   <owner>emshack@chromium.org</owner>
   <owner>chrome-desktop-sea@google.com</owner>
   <component>1457140</component>
diff --git a/tools/metrics/histograms/metadata/chromeos/enums.xml b/tools/metrics/histograms/metadata/chromeos/enums.xml
index 94f98f79..4d00e9c 100644
--- a/tools/metrics/histograms/metadata/chromeos/enums.xml
+++ b/tools/metrics/histograms/metadata/chromeos/enums.xml
@@ -826,6 +826,12 @@
   <int value="2" label="kLocked"/>
 </enum>
 
+<enum name="ClassManagementEnabled">
+  <int value="0" label="Class Management disabled"/>
+  <int value="1" label="Class Management enabled for student"/>
+  <int value="2" label="Class Management enabled for teacher"/>
+</enum>
+
 <enum name="ComboDeviceClassification">
   <int value="0" label="KnownKeyboardImposter"/>
   <int value="1" label="KnownMouseImposter"/>
diff --git a/tools/metrics/histograms/metadata/chromeos/histograms.xml b/tools/metrics/histograms/metadata/chromeos/histograms.xml
index 679edf2..af66af80 100644
--- a/tools/metrics/histograms/metadata/chromeos/histograms.xml
+++ b/tools/metrics/histograms/metadata/chromeos/histograms.xml
@@ -1153,6 +1153,16 @@
       variants="AllCertProvisioningProtocolVersions"/>
 </histogram>
 
+<histogram name="ChromeOS.ClassManagementEnabled" enum="ClassManagementEnabled"
+    expires_after="2025-11-01">
+  <owner>zifanzhang@google.com</owner>
+  <owner>cros-client-wa@google.com</owner>
+  <summary>
+    Tracks the Class Management enabled status for users. Recorded when the user
+    session starts.
+  </summary>
+</histogram>
+
 <histogram name="ChromeOS.CountryCode.MissingVariationData"
     enum="ChromeOSFallbackCountry" expires_after="2026-03-20">
   <owner>cschlosser@chromium.org</owner>
diff --git a/tools/metrics/histograms/metadata/enterprise/enums.xml b/tools/metrics/histograms/metadata/enterprise/enums.xml
index 83ea423..8f1d648 100644
--- a/tools/metrics/histograms/metadata/enterprise/enums.xml
+++ b/tools/metrics/histograms/metadata/enterprise/enums.xml
@@ -2227,7 +2227,7 @@
   <int value="1357" label="BuiltInAIAPIsEnabled"/>
   <int value="1358" label="TabGroupSharingSettings"/>
   <int value="1359" label="NTPFootermanagementNoticeEnabled"/>
-  <int value="1360" label="NTPFooterThemeAttributionEnabled"/>
+  <int value="1360" label="NTPFooterExtensionAttributionEnabled"/>
   <int value="1361" label="ClearWindowNameForNewBrowsingContextGroup"/>
 </enum>
 
diff --git a/tools/metrics/histograms/metadata/extensions/histograms.xml b/tools/metrics/histograms/metadata/extensions/histograms.xml
index b5fd6b71..d8eec06 100644
--- a/tools/metrics/histograms/metadata/extensions/histograms.xml
+++ b/tools/metrics/histograms/metadata/extensions/histograms.xml
@@ -797,7 +797,7 @@
 </histogram>
 
 <histogram name="Extensions.CookieAPIPartitionKeyWellFormatted"
-    enum="BooleanEnabled" expires_after="2025-06-22">
+    enum="BooleanEnabled" expires_after="2026-04-30">
   <owner>arichiv@google.com</owner>
   <owner>src/net/cookies/OWNERS</owner>
   <summary>
diff --git a/tools/metrics/histograms/metadata/file/enums.xml b/tools/metrics/histograms/metadata/file/enums.xml
index 9c15031..fd4e1ae 100644
--- a/tools/metrics/histograms/metadata/file/enums.xml
+++ b/tools/metrics/histograms/metadata/file/enums.xml
@@ -891,6 +891,7 @@
   <int value="19" label="Cancelled at fallback after open attempt"/>
   <int value="20" label="Cannot get fallback choice after open attempt"/>
   <int value="21" label="File already being opened"/>
+  <int value="22" label="Cannot get source type"/>
 </enum>
 
 <enum name="RestoreFailedNoParentType">
diff --git a/tools/metrics/histograms/metadata/glic/enums.xml b/tools/metrics/histograms/metadata/glic/enums.xml
index 24dacce0..837bd02 100644
--- a/tools/metrics/histograms/metadata/glic/enums.xml
+++ b/tools/metrics/histograms/metadata/glic/enums.xml
@@ -214,6 +214,16 @@
 
 <!-- LINT.ThenChange(//chrome/browser/glic/glic_metrics.h:ResponseSegmentation) -->
 
+<!-- LINT.IfChange(TieredRolloutEnablementStatus) -->
+
+<enum name="GlicTieredRolloutEnablementStatus">
+  <int value="0" label="All profiles enabled"/>
+  <int value="1" label="Some profiles enabled"/>
+  <int value="2" label="No profiles enabled"/>
+</enum>
+
+<!-- LINT.ThenChange(//chrome/browser/glic/glic_metrics_provider.h:TieredRolloutEnablementStatus) -->
+
 <!-- LINT.IfChange(WebClientMode) -->
 
 <enum name="GlicWebClientMode">
diff --git a/tools/metrics/histograms/metadata/glic/histograms.xml b/tools/metrics/histograms/metadata/glic/histograms.xml
index c465520..59e4d26 100644
--- a/tools/metrics/histograms/metadata/glic/histograms.xml
+++ b/tools/metrics/histograms/metadata/glic/histograms.xml
@@ -68,6 +68,7 @@
     <variant name="AttachPanel"/>
     <variant name="CaptureScreenshot"/>
     <variant name="ClosePanel"/>
+    <variant name="ClosePanelAndShutdown"/>
     <variant name="CreateTab"/>
     <variant name="DetachPanel"/>
     <variant name="EnableDragResize"/>
@@ -665,6 +666,17 @@
   </summary>
 </histogram>
 
+<histogram name="Glic.TieredRolloutEnablementStatus"
+    enum="GlicTieredRolloutEnablementStatus" expires_after="2026-01-15">
+  <owner>sophiechang@chromium.org</owner>
+  <owner>dewittj@chromium.org</owner>
+  <summary>
+    Records whether the Glic-eligible profiles are enabled for tiered rollout.
+    Emitted in every UMA record from the current session (at the time the record
+    is finalized), but only if there is at least one Glic-eligible profile.
+  </summary>
+</histogram>
+
 <histogram name="Glic.Usage.Hotkey" enum="GlicHotkeyUsage"
     expires_after="2026-01-15">
   <owner>erikchen@chromium.org</owner>
diff --git a/tools/metrics/histograms/metadata/lens/histograms.xml b/tools/metrics/histograms/metadata/lens/histograms.xml
index bb1fa88..10e0308 100644
--- a/tools/metrics/histograms/metadata/lens/histograms.xml
+++ b/tools/metrics/histograms/metadata/lens/histograms.xml
@@ -128,7 +128,7 @@
 </histogram>
 
 <histogram name="Lens.Overlay.ByDocumentType.{DocumentType}.Invoked"
-    enum="Boolean" expires_after="2025-09-07">
+    enum="Boolean" expires_after="2025-10-31">
   <owner>stanfield@google.com</owner>
   <owner>mercerd@google.com</owner>
   <owner>lens-chrome@google.com</owner>
@@ -208,7 +208,7 @@
 </histogram>
 
 <histogram name="Lens.Overlay.ByPageContentType.Pdf.PageCount" units="Pages"
-    expires_after="2025-10-12">
+    expires_after="2025-10-31">
   <owner>mercerd@google.com</owner>
   <owner>lens-chrome@google.com</owner>
   <summary>
@@ -219,7 +219,7 @@
 
 <histogram
     name="Lens.Overlay.ByPageContentType.{PageContentType}.DocumentSize2"
-    units="KB" expires_after="2025-08-10">
+    units="KB" expires_after="2025-10-31">
   <owner>mercerd@google.com</owner>
   <owner>lens-chrome@google.com</owner>
   <summary>
@@ -243,7 +243,7 @@
 
 <histogram
     name="Lens.Overlay.ContextualSearchBox.ByDocumentType.{DocumentType}.ShownInSession"
-    enum="BooleanShown" expires_after="2025-09-28">
+    enum="BooleanShown" expires_after="2025-10-31">
   <owner>stanfield@google.com</owner>
   <owner>mercerd@google.com</owner>
   <owner>lens-chrome@google.com</owner>
@@ -259,7 +259,7 @@
 
 <histogram
     name="Lens.Overlay.ContextualSearchBox.ByPageContentType.{PageContentType}.FocusedInSession"
-    enum="Boolean" expires_after="2025-10-05">
+    enum="Boolean" expires_after="2025-10-31">
   <owner>niharm@google.com</owner>
   <owner>lens-chrome@google.com</owner>
   <summary>
@@ -272,7 +272,7 @@
 
 <histogram
     name="Lens.Overlay.ContextualSearchBox.ByPageContentType.{PageContentType}.ShownInSession"
-    enum="BooleanShown" expires_after="2025-09-28">
+    enum="BooleanShown" expires_after="2025-10-31">
   <owner>stanfield@google.com</owner>
   <owner>mercerd@google.com</owner>
   <owner>lens-chrome@google.com</owner>
@@ -286,7 +286,7 @@
 
 <histogram
     name="Lens.Overlay.ContextualSearchBox.ByPageContentType.{PageContentType}.TimeFromInvocationToFirstFocus"
-    units="ms" expires_after="2025-09-27">
+    units="ms" expires_after="2025-10-31">
   <owner>mercerd@google.com</owner>
   <owner>lens-chrome@google.com</owner>
   <summary>
@@ -301,7 +301,7 @@
 
 <histogram
     name="Lens.Overlay.ContextualSearchBox.ByPageContentType.{PageContentType}.TimeFromNavigationToFirstFocus"
-    units="ms" expires_after="2025-09-27">
+    units="ms" expires_after="2025-10-31">
   <owner>mercerd@chromium.org</owner>
   <owner>lens-chrome@google.com</owner>
   <summary>
@@ -315,7 +315,7 @@
 
 <histogram
     name="Lens.Overlay.ContextualSearchBox.ByPageContentType.{PageContentType}.TimeFromNavigationToFirstInteraction"
-    units="ms" expires_after="2025-05-06">
+    units="ms" expires_after="2025-10-31">
   <owner>mercerd@chromium.org</owner>
   <owner>lens-chrome@google.com</owner>
   <summary>
@@ -329,7 +329,7 @@
 </histogram>
 
 <histogram name="Lens.Overlay.ContextualSearchBox.FocusedInSession"
-    enum="Boolean" expires_after="2025-09-28">
+    enum="Boolean" expires_after="2025-10-31">
   <owner>niharm@google.com</owner>
   <owner>lens-chrome@google.com</owner>
   <summary>
@@ -340,7 +340,7 @@
 </histogram>
 
 <histogram name="Lens.Overlay.ContextualSearchBox.ShownInSession"
-    enum="BooleanShown" expires_after="2025-09-07">
+    enum="BooleanShown" expires_after="2025-10-31">
   <owner>stanfield@google.com</owner>
   <owner>mercerd@google.com</owner>
   <owner>lens-chrome@google.com</owner>
@@ -352,7 +352,7 @@
 
 <histogram
     name="Lens.Overlay.ContextualSuggest.ByPageContentType.{PageContentType}.QueryIssuedInSession"
-    enum="Boolean" expires_after="2025-09-28">
+    enum="Boolean" expires_after="2025-10-31">
   <owner>niharm@google.com</owner>
   <owner>lens-chrome@google.com</owner>
   <summary>
@@ -368,7 +368,7 @@
 
 <histogram
     name="Lens.Overlay.ContextualSuggest.FollowUpQuery.ByPageContentType.{PageContentType}.QueryIssuedInSessionBeforeSuggestShown"
-    enum="Boolean" expires_after="2025-09-13">
+    enum="Boolean" expires_after="2025-10-31">
   <owner>mercerd@google.com</owner>
   <owner>lens-chrome@google.com</owner>
   <summary>
@@ -385,7 +385,7 @@
 
 <histogram
     name="Lens.Overlay.ContextualSuggest.FollowUpQuery.QueryIssuedInSessionBeforeSuggestShown"
-    enum="Boolean" expires_after="2025-09-13">
+    enum="Boolean" expires_after="2025-10-31">
   <owner>mercerd@google.com</owner>
   <owner>lens-chrome@google.com</owner>
   <summary>
@@ -401,7 +401,7 @@
 
 <histogram
     name="Lens.Overlay.ContextualSuggest.InitialQuery.ByPageContentType.{PageContentType}.QueryIssuedInSessionBeforeSuggestShown"
-    enum="Boolean" expires_after="2025-09-13">
+    enum="Boolean" expires_after="2025-10-31">
   <owner>mercerd@google.com</owner>
   <owner>lens-chrome@google.com</owner>
   <summary>
@@ -416,7 +416,7 @@
 
 <histogram
     name="Lens.Overlay.ContextualSuggest.InitialQuery.QueryIssuedInSessionBeforeSuggestShown"
-    enum="Boolean" expires_after="2025-09-13">
+    enum="Boolean" expires_after="2025-10-31">
   <owner>mercerd@google.com</owner>
   <owner>lens-chrome@google.com</owner>
   <summary>
@@ -428,7 +428,7 @@
 </histogram>
 
 <histogram name="Lens.Overlay.ContextualSuggest.QueryIssuedInSession"
-    enum="Boolean" expires_after="2025-09-07">
+    enum="Boolean" expires_after="2025-10-31">
   <owner>niharm@google.com</owner>
   <owner>lens-chrome@google.com</owner>
   <summary>
@@ -441,7 +441,7 @@
 
 <histogram
     name="Lens.Overlay.ContextualSuggest.ZPS.ByPageContentType.{PageContentType}.ShownInSession"
-    enum="Boolean" expires_after="2025-09-14">
+    enum="Boolean" expires_after="2025-10-31">
   <owner>niharm@google.com</owner>
   <owner>lens-chrome@google.com</owner>
   <summary>
@@ -457,7 +457,7 @@
 
 <histogram
     name="Lens.Overlay.ContextualSuggest.ZPS.ByPageContentType.{PageContentType}.SuggestionUsedInSession"
-    enum="Boolean" expires_after="2025-09-14">
+    enum="Boolean" expires_after="2025-10-31">
   <owner>niharm@google.com</owner>
   <owner>lens-chrome@google.com</owner>
   <summary>
@@ -471,7 +471,7 @@
 </histogram>
 
 <histogram name="Lens.Overlay.ContextualSuggest.ZPS.ShownInSession"
-    enum="Boolean" expires_after="2025-09-28">
+    enum="Boolean" expires_after="2025-10-31">
   <owner>niharm@google.com</owner>
   <owner>lens-chrome@google.com</owner>
   <summary>
@@ -482,7 +482,7 @@
 </histogram>
 
 <histogram name="Lens.Overlay.ContextualSuggest.ZPS.SuggestionUsedInSession"
-    enum="Boolean" expires_after="2025-09-07">
+    enum="Boolean" expires_after="2025-10-31">
   <owner>niharm@google.com</owner>
   <owner>lens-chrome@google.com</owner>
   <summary>
diff --git a/tools/metrics/histograms/metadata/media/histograms.xml b/tools/metrics/histograms/metadata/media/histograms.xml
index 560b905..49919e1 100644
--- a/tools/metrics/histograms/metadata/media/histograms.xml
+++ b/tools/metrics/histograms/metadata/media/histograms.xml
@@ -1355,7 +1355,7 @@
 </histogram>
 
 <histogram name="Media.Audio.TabAudioMuted" enum="Boolean"
-    expires_after="2025-06-22">
+    expires_after="2026-05-12">
   <owner>evliu@google.com</owner>
   <owner>chrome-media-ux@google.com</owner>
   <summary>
diff --git a/tools/metrics/histograms/metadata/net/histograms.xml b/tools/metrics/histograms/metadata/net/histograms.xml
index 6a50350..29b62c0a 100644
--- a/tools/metrics/histograms/metadata/net/histograms.xml
+++ b/tools/metrics/histograms/metadata/net/histograms.xml
@@ -7522,13 +7522,24 @@
     not included in the original request during URLLoader to handle a
     connection. It is time just before the OnAcceptCHFrameReceived() IPC call
     until the IPC callback is called. Refer
-    URLLoader::ProcessAcceptCHFrameOnConnected() for details.
+    AcceptCHFrameInterceptor::OnConnected() for details.
 
     Note: The metrics is reported from all clients. Some of clients may not have
     high resolution timer that can send metrics with microseconds precision.
   </summary>
 </histogram>
 
+<histogram name="Net.URLLoader.AcceptCH.RunObserverCall" enum="Boolean"
+    expires_after="2025-11-12">
+  <owner>yyanagisawa@chromium.org</owner>
+  <owner>chrome-loading@google.com</owner>
+  <summary>
+    Records if the received the ACCEPT_CH frame should be checked by the
+    observer. It is recorded only if the received header has the ACCEPT_CH
+    frame. Refer AcceptCHFrameInterceptor::OnConnected() for details.
+  </summary>
+</histogram>
+
 <histogram name="Net.URLLoader.AcceptCH.Status" enum="NetErrorCodes"
     expires_after="2025-09-27">
   <owner>yyanagisawa@chromium.org</owner>
@@ -7539,7 +7550,7 @@
     that were not included in the original request during URLLoader to handle a
     connection. It is time just before the OnAcceptCHFrameReceived() IPC call
     until the IPC callback is called. Refer
-    URLLoader::ProcessAcceptCHFrameOnConnected() for details.
+    AcceptCHFrameInterceptor::OnConnected() for details.
   </summary>
 </histogram>
 
diff --git a/tools/metrics/histograms/metadata/privacy/histograms.xml b/tools/metrics/histograms/metadata/privacy/histograms.xml
index c034385..a01ecf7 100644
--- a/tools/metrics/histograms/metadata/privacy/histograms.xml
+++ b/tools/metrics/histograms/metadata/privacy/histograms.xml
@@ -250,10 +250,9 @@
 </histogram>
 
 <histogram name="Privacy.CookieControlsActivated.PageRefreshCount"
-    units="count" expires_after="2025-10-12">
-  <owner>olesiamarukhno@google.com</owner>
-  <owner>sauski@google.com</owner>
-  <owner>kmg@google.com</owner>
+    units="count" expires_after="2025-12-10">
+  <owner>fmacintosh@google.com</owner>
+  <owner>koilos@google.com</owner>
   <summary>
     How many times the page was refreshed in the last 30 seconds, recorded when
     third-party party cookies are allowed using cookie controls UI.
@@ -261,10 +260,9 @@
 </histogram>
 
 <histogram name="Privacy.CookieControlsActivated.SaaRequested"
-    enum="BooleanEnabled" expires_after="2025-06-08">
-  <owner>olesiamarukhno@google.com</owner>
-  <owner>sauski@google.com</owner>
-  <owner>kmg@google.com</owner>
+    enum="BooleanEnabled" expires_after="2025-12-10">
+  <owner>fmacintosh@google.com</owner>
+  <owner>koilos@google.com</owner>
   <summary>
     Whether storage access API was granted or blocked for the site, recorded
     when third-party party cookies are allowed using cookie controls UI.
@@ -272,10 +270,9 @@
 </histogram>
 
 <histogram name="Privacy.CookieControlsActivated.SiteDataAccessType"
-    enum="ThirdPartySiteDataAccessType" expires_after="2025-10-12">
-  <owner>olesiamarukhno@google.com</owner>
-  <owner>sauski@google.com</owner>
-  <owner>kmg@google.com</owner>
+    enum="ThirdPartySiteDataAccessType" expires_after="2025-12-10">
+  <owner>fmacintosh@google.com</owner>
+  <owner>koilos@google.com</owner>
   <summary>
     Represents if site data was accessed by any third-party sites and if any of
     those accesses were blocked, recorded when third-party party cookies are
@@ -284,10 +281,9 @@
 </histogram>
 
 <histogram name="Privacy.CookieControlsActivated.SiteEngagementScore"
-    units="units" expires_after="2025-06-08">
-  <owner>olesiamarukhno@google.com</owner>
-  <owner>sauski@google.com</owner>
-  <owner>kmg@google.com</owner>
+    units="units" expires_after="2025-12-10">
+  <owner>fmacintosh@google.com</owner>
+  <owner>koilos@google.com</owner>
   <summary>
     The site engagement score, recorded when third-party party cookies are
     allowed using cookie controls UI.
diff --git a/tools/metrics/histograms/metadata/segmentation_platform/histograms.xml b/tools/metrics/histograms/metadata/segmentation_platform/histograms.xml
index 9d6a4eb..f75f87c2 100644
--- a/tools/metrics/histograms/metadata/segmentation_platform/histograms.xml
+++ b/tools/metrics/histograms/metadata/segmentation_platform/histograms.xml
@@ -152,7 +152,7 @@
 </variants>
 
 <histogram name="SegmentationPlatform.AdaptiveToolbar.SegmentSelected.Startup"
-    enum="AdaptiveToolbarButtonVariant" expires_after="2025-09-28">
+    enum="AdaptiveToolbarButtonVariant" expires_after="2026-05-12">
   <owner>shaktisahu@chromium.org</owner>
   <owner>chrome-segmentation-platform@google.com</owner>
   <summary>
@@ -163,7 +163,7 @@
 
 <histogram
     name="SegmentationPlatform.AdaptiveToolbar.SegmentSelection.Computed"
-    enum="AdaptiveToolbarButtonVariant" expires_after="2026-03-22">
+    enum="AdaptiveToolbarButtonVariant" expires_after="2026-05-12">
   <owner>shaktisahu@chromium.org</owner>
   <owner>chrome-segmentation-platform@google.com</owner>
   <summary>
@@ -173,7 +173,7 @@
 </histogram>
 
 <histogram name="SegmentationPlatform.AdaptiveToolbar.SegmentSwitched"
-    enum="AdaptiveToolbarSegmentSwitch" expires_after="2026-03-22">
+    enum="AdaptiveToolbarSegmentSwitch" expires_after="2026-05-12">
   <owner>shaktisahu@chromium.org</owner>
   <owner>chrome-segmentation-platform@google.com</owner>
   <summary>
@@ -185,7 +185,7 @@
 
 <histogram
     name="SegmentationPlatform.ClassificationRequest.TotalDuration.{SegmentationKey}"
-    units="ms" expires_after="2025-10-12">
+    units="ms" expires_after="2026-05-12">
   <owner>ritikagup@google.com</owner>
   <owner>chrome-segmentation-platform@google.com</owner>
   <summary>
@@ -201,7 +201,7 @@
 </histogram>
 
 <histogram name="SegmentationPlatform.Database.{DatabaseOperation}" units="ms"
-    expires_after="2025-10-05">
+    expires_after="2026-05-12">
   <owner>haileywang@google.com</owner>
   <owner>chrome-segmentation-platform@google.com</owner>
   <summary>
@@ -213,7 +213,7 @@
 
 <histogram
     name="SegmentationPlatform.DefaultModelDelivery.Metadata.FeatureCount.{SegmentationModel}"
-    units="features" expires_after="2026-03-22">
+    units="features" expires_after="2026-05-12">
   <owner>ritikagup@google.com</owner>
   <owner>chrome-segmentation-platform@google.com</owner>
   <summary>
@@ -228,7 +228,7 @@
 
 <histogram
     name="SegmentationPlatform.DefaultModelDelivery.Metadata.Validation.{ValidationPhase}.{SegmentationModel}"
-    enum="SegmentationPlatformValidationResult" expires_after="2025-08-10">
+    enum="SegmentationPlatformValidationResult" expires_after="2026-05-12">
   <owner>ritikagup@google.com</owner>
   <owner>chrome-segmentation-platform@google.com</owner>
   <summary>
@@ -244,7 +244,7 @@
 </histogram>
 
 <histogram name="SegmentationPlatform.DefaultModelDelivery.Received"
-    enum="SegmentationPlatformSegmentationModel" expires_after="2026-03-22">
+    enum="SegmentationPlatformSegmentationModel" expires_after="2026-05-12">
   <owner>ritikagup@google.com</owner>
   <owner>chrome-segmentation-platform@google.com</owner>
   <summary>
@@ -258,7 +258,7 @@
 
 <histogram
     name="SegmentationPlatform.DefaultModelDelivery.SaveResult.{SegmentationModel}"
-    enum="BooleanSuccess" expires_after="2025-10-12">
+    enum="BooleanSuccess" expires_after="2026-05-12">
   <owner>ritikagup@google.com</owner>
   <owner>chrome-segmentation-platform@google.com</owner>
   <summary>
@@ -273,7 +273,7 @@
 
 <histogram
     name="SegmentationPlatform.DefaultModelDelivery.SegmentIdMatches.{SegmentationModel}"
-    enum="BooleanYesNo" expires_after="2025-08-10">
+    enum="BooleanYesNo" expires_after="2026-05-12">
   <owner>ritikagup@google.com</owner>
   <owner>chrome-segmentation-platform@google.com</owner>
   <summary>
@@ -287,7 +287,7 @@
 </histogram>
 
 <histogram name="SegmentationPlatform.DeviceCountByOsType.{OsType}"
-    units="devices" expires_after="2025-10-12">
+    units="devices" expires_after="2026-05-12">
   <owner>junzou@chromium.org</owner>
   <owner>ssid@chromium.org</owner>
   <owner>chrome-segmentation-team@google.com</owner>
@@ -315,7 +315,7 @@
 <histogram
     name="SegmentationPlatform.FeatureProcessing.Error.{SegmentationModel}"
     enum="SegmentationPlatformFeatureProcessingError"
-    expires_after="2025-08-10">
+    expires_after="2026-05-12">
   <owner>haileywang@chromium.org</owner>
   <owner>chrome-segmentation-platform@google.com</owner>
   <summary>
@@ -328,7 +328,7 @@
 </histogram>
 
 <histogram name="SegmentationPlatform.Init.CreationToInitializationLatency"
-    units="ms" expires_after="2025-05-25">
+    units="ms" expires_after="2026-05-12">
   <owner>ssid@chromium.org</owner>
   <owner>chrome-segmentation-platform@google.com</owner>
   <summary>
@@ -339,7 +339,7 @@
 
 <histogram
     name="SegmentationPlatform.Init.ModelUpdatedTimeDifferenceInDays.{SegmentID}"
-    units="days" expires_after="2026-03-22">
+    units="days" expires_after="2026-05-12">
   <owner>haileywang@chromium.org</owner>
   <owner>chrome-segmentation-platform@google.com</owner>
   <summary>
@@ -353,7 +353,7 @@
 
 <histogram
     name="SegmentationPlatform.Init.ProcessCreationToServiceCreationLatency"
-    units="ms" expires_after="2026-03-22">
+    units="ms" expires_after="2026-05-12">
   <owner>ssid@chromium.org</owner>
   <owner>chrome-segmentation-platform@google.com</owner>
   <summary>
@@ -363,7 +363,7 @@
 </histogram>
 
 <histogram name="SegmentationPlatform.Maintenance.CleanupSignalSuccessCount"
-    units="signals" expires_after="2025-09-28">
+    units="signals" expires_after="2026-05-12">
   <owner>nyquist@chromium.org</owner>
   <owner>shaktisahu@chromium.org</owner>
   <owner>chrome-segmentation-platform@google.com</owner>
@@ -377,7 +377,7 @@
 
 <histogram
     name="SegmentationPlatform.Maintenance.CompactionResult.{SignalType}"
-    enum="BooleanSuccess" expires_after="2026-03-22">
+    enum="BooleanSuccess" expires_after="2026-05-12">
   <owner>nyquist@chromium.org</owner>
   <owner>shaktisahu@chromium.org</owner>
   <owner>chrome-segmentation-platform@google.com</owner>
@@ -391,7 +391,7 @@
 </histogram>
 
 <histogram name="SegmentationPlatform.Maintenance.SignalIdentifierCount"
-    units="ids" expires_after="2025-09-28">
+    units="ids" expires_after="2026-05-12">
   <owner>nyquist@chromium.org</owner>
   <owner>shaktisahu@chromium.org</owner>
   <owner>chrome-segmentation-platform@google.com</owner>
@@ -404,7 +404,7 @@
 </histogram>
 
 <histogram name="SegmentationPlatform.ModelAvailability.{SegmentationModel}"
-    enum="SegmentationModelAvailability" expires_after="2025-10-12">
+    enum="SegmentationModelAvailability" expires_after="2026-05-12">
   <owner>ssid@chromium.org</owner>
   <owner>chrome-segmentation-platform@google.com</owner>
   <summary>
@@ -423,7 +423,7 @@
 
 <histogram
     name="SegmentationPlatform.ModelDelivery.DeleteResult.{SegmentationModel}"
-    enum="BooleanSuccess" expires_after="2026-03-22">
+    enum="BooleanSuccess" expires_after="2026-05-12">
   <owner>salg@chromium.org</owner>
   <owner>ssid@chromium.org</owner>
   <owner>chrome-segmentation-platform@google.com</owner>
@@ -439,7 +439,7 @@
 
 <histogram
     name="SegmentationPlatform.ModelDelivery.HasMetadata.{SegmentationModel}"
-    enum="BooleanYesNo" expires_after="2025-10-12">
+    enum="BooleanYesNo" expires_after="2026-05-12">
   <owner>nyquist@chromium.org</owner>
   <owner>shaktisahu@chromium.org</owner>
   <owner>chrome-segmentation-platform@google.com</owner>
@@ -455,7 +455,7 @@
 
 <histogram
     name="SegmentationPlatform.ModelDelivery.Metadata.FeatureCount.{SegmentationModel}"
-    units="features" expires_after="2026-03-22">
+    units="features" expires_after="2026-05-12">
   <owner>nyquist@chromium.org</owner>
   <owner>shaktisahu@chromium.org</owner>
   <owner>chrome-segmentation-platform@google.com</owner>
@@ -471,7 +471,7 @@
 
 <histogram
     name="SegmentationPlatform.ModelDelivery.Metadata.Validation.{ValidationPhase}.{SegmentationModel}"
-    enum="SegmentationPlatformValidationResult" expires_after="2025-10-12">
+    enum="SegmentationPlatformValidationResult" expires_after="2026-05-12">
   <owner>nyquist@chromium.org</owner>
   <owner>shaktisahu@chromium.org</owner>
   <owner>chrome-segmentation-platform@google.com</owner>
@@ -488,7 +488,7 @@
 </histogram>
 
 <histogram name="SegmentationPlatform.ModelDelivery.Received"
-    enum="SegmentationPlatformSegmentationModel" expires_after="2025-09-28">
+    enum="SegmentationPlatformSegmentationModel" expires_after="2026-05-12">
   <owner>nyquist@chromium.org</owner>
   <owner>shaktisahu@chromium.org</owner>
   <owner>chrome-segmentation-platform@google.com</owner>
@@ -503,7 +503,7 @@
 
 <histogram
     name="SegmentationPlatform.ModelDelivery.SaveResult.{SegmentationModel}"
-    enum="BooleanSuccess" expires_after="2026-03-22">
+    enum="BooleanSuccess" expires_after="2026-05-12">
   <owner>nyquist@chromium.org</owner>
   <owner>shaktisahu@chromium.org</owner>
   <owner>chrome-segmentation-platform@google.com</owner>
@@ -519,7 +519,7 @@
 
 <histogram
     name="SegmentationPlatform.ModelDelivery.SegmentIdMatches.{SegmentationModel}"
-    enum="BooleanYesNo" expires_after="2026-03-22">
+    enum="BooleanYesNo" expires_after="2026-05-12">
   <owner>nyquist@chromium.org</owner>
   <owner>shaktisahu@chromium.org</owner>
   <owner>chrome-segmentation-platform@google.com</owner>
@@ -535,7 +535,7 @@
 
 <histogram
     name="SegmentationPlatform.ModelExecution.DefaultProvider.Status.{SegmentationModel}"
-    enum="SegmentationPlatformModelExecutionStatus" expires_after="2025-10-12">
+    enum="SegmentationPlatformModelExecutionStatus" expires_after="2026-05-12">
   <owner>nyquist@chromium.org</owner>
   <owner>shaktisahu@chromium.org</owner>
   <owner>chrome-segmentation-platform@google.com</owner>
@@ -549,7 +549,7 @@
 
 <histogram
     name="SegmentationPlatform.ModelExecution.Duration.FeatureProcessing.{SegmentationModel}"
-    units="ms" expires_after="2025-06-08">
+    units="ms" expires_after="2026-05-12">
   <owner>nyquist@chromium.org</owner>
   <owner>shaktisahu@chromium.org</owner>
   <owner>chrome-segmentation-platform@google.com</owner>
@@ -564,7 +564,7 @@
 
 <histogram
     name="SegmentationPlatform.ModelExecution.Duration.Model.{SegmentationModel}.{ModelExecutionStatus}"
-    units="ms" expires_after="2025-10-12">
+    units="ms" expires_after="2026-05-12">
   <owner>nyquist@chromium.org</owner>
   <owner>shaktisahu@chromium.org</owner>
   <owner>chrome-segmentation-platform@google.com</owner>
@@ -581,7 +581,7 @@
 
 <histogram
     name="SegmentationPlatform.ModelExecution.Duration.Total.{SegmentationModel}.{ModelExecutionStatus}"
-    units="ms" expires_after="2025-10-12">
+    units="ms" expires_after="2026-05-12">
   <owner>nyquist@chromium.org</owner>
   <owner>shaktisahu@chromium.org</owner>
   <owner>chrome-segmentation-platform@google.com</owner>
@@ -600,7 +600,7 @@
 
 <histogram
     name="SegmentationPlatform.ModelExecution.Result.{Index}.{SegmentationModel}"
-    units="score" expires_after="2025-10-05">
+    units="score" expires_after="2026-05-12">
   <owner>shaktisahu@chromium.org</owner>
   <owner>chrome-segmentation-platform@google.com</owner>
   <summary>
@@ -616,7 +616,7 @@
 
 <histogram
     name="SegmentationPlatform.ModelExecution.Result.{SegmentationModel}"
-    units="score" expires_after="2026-03-22">
+    units="score" expires_after="2026-05-12">
   <owner>nyquist@chromium.org</owner>
   <owner>shaktisahu@chromium.org</owner>
   <owner>chrome-segmentation-platform@google.com</owner>
@@ -632,7 +632,7 @@
 
 <histogram
     name="SegmentationPlatform.ModelExecution.SaveResult.{SegmentationModel}"
-    enum="BooleanSuccess" expires_after="2026-03-22">
+    enum="BooleanSuccess" expires_after="2026-05-12">
   <owner>nyquist@chromium.org</owner>
   <owner>shaktisahu@chromium.org</owner>
   <owner>chrome-segmentation-platform@google.com</owner>
@@ -648,7 +648,7 @@
 
 <histogram
     name="SegmentationPlatform.ModelExecution.Status.{SegmentationModel}"
-    enum="SegmentationPlatformModelExecutionStatus" expires_after="2025-10-05">
+    enum="SegmentationPlatformModelExecutionStatus" expires_after="2026-05-12">
   <owner>nyquist@chromium.org</owner>
   <owner>shaktisahu@chromium.org</owner>
   <owner>chrome-segmentation-platform@google.com</owner>
@@ -675,7 +675,7 @@
 
 <histogram
     name="SegmentationPlatform.ModelExecution.ZeroValuePercent.{SegmentationModel}"
-    units="%" expires_after="2025-10-12">
+    units="%" expires_after="2026-05-12">
   <owner>nyquist@chromium.org</owner>
   <owner>shaktisahu@chromium.org</owner>
   <owner>chrome-segmentation-platform@google.com</owner>
@@ -690,7 +690,7 @@
 
 <histogram
     name="SegmentationPlatform.SegmentInfoDatabase.ProtoDBUpdateResult.{SegmentationKey}"
-    enum="BooleanSuccess" expires_after="2026-03-22">
+    enum="BooleanSuccess" expires_after="2026-05-12">
   <owner>salg@google.com</owner>
   <owner>chrome-segmentation-platform@google.com</owner>
   <summary>
@@ -707,7 +707,7 @@
 
 <histogram
     name="SegmentationPlatform.SegmentSelectionOnDemand.Duration.{SegmentationKey}.{SelectedSegment}"
-    units="ms" expires_after="2026-03-22">
+    units="ms" expires_after="2026-05-12">
   <owner>shaktisahu@chromium.org</owner>
   <owner>chrome-segmentation-platform@google.com</owner>
   <summary>
@@ -729,7 +729,7 @@
 </histogram>
 
 <histogram name="SegmentationPlatform.SelectionFailedReason"
-    enum="SegmentationSelectionFailureReason" expires_after="2025-05-25">
+    enum="SegmentationSelectionFailureReason" expires_after="2026-05-12">
   <owner>ssid@chromium.org</owner>
   <owner>chrome-segmentation-platform@google.com</owner>
   <summary>
@@ -740,7 +740,7 @@
 </histogram>
 
 <histogram name="SegmentationPlatform.SelectionFailedReason.{SegmentationKey}"
-    enum="SegmentationSelectionFailureReason" expires_after="2025-10-05">
+    enum="SegmentationSelectionFailureReason" expires_after="2026-05-12">
   <owner>ssid@chromium.org</owner>
   <owner>chrome-segmentation-platform@google.com</owner>
   <summary>
@@ -758,7 +758,7 @@
 
 <histogram
     name="SegmentationPlatform.SignalDatabase.GetSamples.DatabaseEntryCount"
-    units="entries" expires_after="2025-09-28">
+    units="entries" expires_after="2026-05-12">
   <owner>nyquist@chromium.org</owner>
   <owner>shaktisahu@chromium.org</owner>
   <owner>chrome-segmentation-platform@google.com</owner>
@@ -773,7 +773,7 @@
 </histogram>
 
 <histogram name="SegmentationPlatform.SignalDatabase.GetSamples.Result"
-    enum="BooleanSuccess" expires_after="2025-09-28">
+    enum="BooleanSuccess" expires_after="2026-05-12">
   <owner>nyquist@chromium.org</owner>
   <owner>shaktisahu@chromium.org</owner>
   <owner>chrome-segmentation-platform@google.com</owner>
@@ -786,7 +786,7 @@
 </histogram>
 
 <histogram name="SegmentationPlatform.SignalDatabase.GetSamples.SampleCount"
-    units="samples" expires_after="2025-09-28">
+    units="samples" expires_after="2026-05-12">
   <owner>nyquist@chromium.org</owner>
   <owner>shaktisahu@chromium.org</owner>
   <owner>chrome-segmentation-platform@google.com</owner>
@@ -801,7 +801,7 @@
 </histogram>
 
 <histogram name="SegmentationPlatform.Signals.ListeningCount.{SignalType}"
-    units="signals" expires_after="2025-10-12">
+    units="signals" expires_after="2026-05-12">
   <owner>nyquist@chromium.org</owner>
   <owner>shaktisahu@chromium.org</owner>
   <owner>chrome-segmentation-platform@google.com</owner>
@@ -814,7 +814,7 @@
 </histogram>
 
 <histogram name="SegmentationPlatform.StructuredMetrics.TooManyTensors.Count"
-    units="tensors" expires_after="2026-03-22">
+    units="tensors" expires_after="2026-05-12">
   <owner>qinmin@chromium.org</owner>
   <owner>chrome-segmentation-platform@google.com</owner>
   <summary>
@@ -825,7 +825,7 @@
 </histogram>
 
 <histogram name="SegmentationPlatform.SyncSessions.RecordTabCountAtSyncUpdate"
-    units="tabs" expires_after="2026-03-22">
+    units="tabs" expires_after="2026-05-12">
   <owner>ritikagup@google.com</owner>
   <owner>chrome-segmentation-platform@google.com</owner>
   <summary>
@@ -835,7 +835,7 @@
 </histogram>
 
 <histogram name="SegmentationPlatform.SyncSessions.TabsCountAtStartup"
-    units="tabs" expires_after="2025-05-25">
+    units="tabs" expires_after="2026-05-12">
   <owner>ritikagup@google.com</owner>
   <owner>chrome-segmentation-platform@google.com</owner>
   <summary>
@@ -846,7 +846,7 @@
 
 <histogram
     name="SegmentationPlatform.SyncSessions.TimeFromStartupToFirstSyncUpdate"
-    units="ms" expires_after="2026-03-22">
+    units="ms" expires_after="2026-05-12">
   <owner>ritikagup@google.com</owner>
   <owner>chrome-segmentation-platform@google.com</owner>
   <summary>
@@ -858,7 +858,7 @@
 </histogram>
 
 <histogram name="SegmentationPlatform.SyncSessions.TimeFromStartupToSyncUpdate"
-    units="ms" expires_after="2026-03-22">
+    units="ms" expires_after="2026-05-12">
   <owner>ritikagup@google.com</owner>
   <owner>chrome-segmentation-platform@google.com</owner>
   <summary>
@@ -872,7 +872,7 @@
 
 <histogram
     name="SegmentationPlatform.SyncSessions.TimeFromTabLoadedToSyncUpdate"
-    units="ms" expires_after="2026-03-22">
+    units="ms" expires_after="2026-05-12">
   <owner>ritikagup@google.com</owner>
   <owner>chrome-segmentation-platform@google.com</owner>
   <summary>
@@ -886,7 +886,7 @@
 
 <histogram
     name="SegmentationPlatform.SyncSessions.{TimeInterval}TabCountAtFirstSyncUpdate"
-    units="tabs" expires_after="2026-03-22">
+    units="tabs" expires_after="2026-05-12">
   <owner>ritikagup@google.com</owner>
   <owner>chrome-segmentation-platform@google.com</owner>
   <summary>
@@ -907,7 +907,7 @@
 <histogram
     name="SegmentationPlatform.TrainingDataCollectionEvents.{SegmentationModel}"
     enum="SegmentationPlatformTrainingDataCollectionEvent"
-    expires_after="2025-10-12">
+    expires_after="2026-05-12">
   <owner>ssid@chromium.org</owner>
   <owner>qinmin@chromium.org</owner>
   <owner>chrome-segmentation-platform@google.com</owner>
@@ -920,7 +920,7 @@
 </histogram>
 
 <histogram name="SegmentationPlatform.{BooleanModel}.SegmentSwitched"
-    enum="SegmentationBooleanSegmentSwitch" expires_after="2026-03-22">
+    enum="SegmentationBooleanSegmentSwitch" expires_after="2026-05-12">
   <owner>shaktisahu@chromium.org</owner>
   <owner>chrome-segmentation-platform@google.com</owner>
   <summary>
@@ -933,7 +933,7 @@
 
 <histogram
     name="SegmentationPlatform.{SegmentationKey}.PostProcessing.TopLabel.Computed"
-    units="index" expires_after="2025-10-12">
+    units="index" expires_after="2026-05-12">
   <owner>shaktisahu@chromium.org</owner>
   <owner>chrome-segmentation-platform@google.com</owner>
   <summary>
@@ -947,7 +947,7 @@
 
 <histogram
     name="SegmentationPlatform.{SegmentationKey}.PostProcessing.TopLabel.Switched"
-    units="index" expires_after="2025-10-12">
+    units="index" expires_after="2026-05-12">
   <owner>shaktisahu@chromium.org</owner>
   <owner>chrome-segmentation-platform@google.com</owner>
   <summary>
@@ -961,7 +961,7 @@
 
 <histogram
     name="SegmentationPlatform.{SegmentationKey}.SegmentSelection.Computed2"
-    enum="SegmentationPlatformSegmentationModel" expires_after="2026-03-22">
+    enum="SegmentationPlatformSegmentationModel" expires_after="2026-05-12">
   <owner>shaktisahu@chromium.org</owner>
   <owner>chrome-segmentation-platform@google.com</owner>
   <summary>
diff --git a/tools/metrics/histograms/metadata/settings/histograms.xml b/tools/metrics/histograms/metadata/settings/histograms.xml
index 3be75fd5..a07cfc1 100644
--- a/tools/metrics/histograms/metadata/settings/histograms.xml
+++ b/tools/metrics/histograms/metadata/settings/histograms.xml
@@ -267,7 +267,7 @@
 </histogram>
 
 <histogram name="Settings.FingerprintingProtection.Enabled"
-    enum="BooleanEnabled" expires_after="2025-06-22">
+    enum="BooleanEnabled" expires_after="2026-02-01">
   <owner>fmacintosh@google.com</owner>
   <owner>koilos@google.com</owner>
   <summary>
@@ -358,7 +358,7 @@
 </histogram>
 
 <histogram name="Settings.IpProtection.Enabled" enum="BooleanEnabled"
-    expires_after="2025-10-26">
+    expires_after="2026-02-01">
   <owner>fmacintosh@google.com</owner>
   <owner>koilos@google.com</owner>
   <summary>
diff --git a/tools/metrics/histograms/metadata/stability/enums.xml b/tools/metrics/histograms/metadata/stability/enums.xml
index c6f64304..51ad94a 100644
--- a/tools/metrics/histograms/metadata/stability/enums.xml
+++ b/tools/metrics/histograms/metadata/stability/enums.xml
@@ -1035,6 +1035,10 @@
   <int value="33" label="chrome::RESULT_CODE_DOWNGRADE_AND_RELAUNCH"/>
   <int value="34" label="chrome::RESULT_CODE_GPU_EXIT_ON_CONTEXT_LOST"/>
   <int value="35" label="chrome::RESULT_CODE_NORMAL_EXIT_UPGRADE_RELAUNCHED"/>
+  <int value="36"
+      label="chrome::RESULT_CODE_NORMAL_EXIT_PACK_EXTENSION_SUCCESS"/>
+  <int value="37" label="chrome::RESULT_CODE_SYSTEM_RESOURCE_EXHAUSTED"/>
+  <int value="38" label="chrome::RESULT_CODE_NORMAL_EXIT_AUTO_DE_ELEVATED"/>
   <int value="259" label="0x103 - STILL_ACTIVE."/>
   <int value="1285" label="ERROR_DELAY_LOAD_FAILED"/>
   <int value="1717" label="RPC_S_UNKNOWN_IF"/>
diff --git a/tools/metrics/histograms/metadata/tab/histograms.xml b/tools/metrics/histograms/metadata/tab/histograms.xml
index 5afe27e1..660a42fb 100644
--- a/tools/metrics/histograms/metadata/tab/histograms.xml
+++ b/tools/metrics/histograms/metadata/tab/histograms.xml
@@ -362,7 +362,6 @@
 </histogram>
 
 <histogram name="Tab.NewTab" enum="NewTabType" expires_after="2025-09-14">
-  <owner>tbergquist@chromium.org</owner>
   <owner>bsep@chromium.org</owner>
   <summary>
     Recorded when a new tab is opened. Tracks the method in which the tab was
@@ -373,7 +372,7 @@
 </histogram>
 
 <histogram name="Tab.Organization.Declutter.DeclutterTabCount" units="tabs"
-    expires_after="2025-08-24">
+    expires_after="2026-01-31">
   <owner>emshack@chromium.org</owner>
   <owner>shibalik@chromium.org</owner>
   <owner>top-chrome-desktop-ui@google.com</owner>
@@ -384,7 +383,7 @@
 </histogram>
 
 <histogram name="Tab.Organization.Declutter.EntryPoint"
-    enum="TabDeclutterEntryPoint" expires_after="2025-11-02">
+    enum="TabDeclutterEntryPoint" expires_after="2026-01-31">
   <owner>emshack@chromium.org</owner>
   <owner>shibalik@chromium.org</owner>
   <owner>top-chrome-desktop-ui@google.com</owner>
@@ -395,7 +394,7 @@
 </histogram>
 
 <histogram name="Tab.Organization.Declutter.ExcludedTabCount" units="tabs"
-    expires_after="2025-08-24">
+    expires_after="2026-01-31">
   <owner>emshack@chromium.org</owner>
   <owner>shibalik@chromium.org</owner>
   <owner>top-chrome-desktop-ui@google.com</owner>
@@ -406,7 +405,7 @@
 </histogram>
 
 <histogram name="Tab.Organization.Declutter.TotalTabCount" units="tabs"
-    expires_after="2025-08-24">
+    expires_after="2026-01-31">
   <owner>emshack@chromium.org</owner>
   <owner>shibalik@chromium.org</owner>
   <owner>top-chrome-desktop-ui@google.com</owner>
@@ -418,7 +417,7 @@
 </histogram>
 
 <histogram name="Tab.Organization.Declutter.TotalUsageCount" units="units"
-    expires_after="2025-08-24">
+    expires_after="2026-01-31">
   <owner>emshack@chromium.org</owner>
   <owner>shibalik@chromium.org</owner>
   <owner>top-chrome-desktop-ui@google.com</owner>
@@ -429,7 +428,7 @@
 </histogram>
 
 <histogram name="Tab.Organization.Declutter.Trigger.BucketedCTR"
-    enum="TabOrganizationDeclutterTriggerCTRBucket" expires_after="2025-08-24">
+    enum="TabOrganizationDeclutterTriggerCTRBucket" expires_after="2026-01-31">
   <owner>emshack@chromium.org</owner>
   <owner>shibalik@chromium.org</owner>
   <owner>top-chrome-desktop-ui@google.com</owner>
@@ -442,7 +441,7 @@
 </histogram>
 
 <histogram name="Tab.Organization.Declutter.Trigger.Outcome"
-    enum="TabOrganizationTriggerOutcome" expires_after="2025-08-24">
+    enum="TabOrganizationTriggerOutcome" expires_after="2026-01-31">
   <owner>emshack@chromium.org</owner>
   <owner>shibalik@chromium.org</owner>
   <owner>top-chrome-desktop-ui@google.com</owner>
@@ -453,7 +452,7 @@
 </histogram>
 
 <histogram name="Tab.Organization.DeclutterCTR"
-    enum="TabOrganizationDeclutterCTREvent" expires_after="2025-11-02">
+    enum="TabOrganizationDeclutterCTREvent" expires_after="2026-01-31">
   <owner>emshack@chromium.org</owner>
   <owner>shibalik@chromium.org</owner>
   <owner>top-chrome-desktop-ui@google.com</owner>
@@ -465,7 +464,7 @@
 </histogram>
 
 <histogram name="Tab.Organization.Dedupe.DeclutterTabCount" units="tabs"
-    expires_after="2025-06-22">
+    expires_after="2026-01-31">
   <owner>emshack@chromium.org</owner>
   <owner>shibalik@chromium.org</owner>
   <owner>top-chrome-desktop-ui@google.com</owner>
@@ -478,7 +477,7 @@
 </histogram>
 
 <histogram name="Tab.Organization.Dedupe.ExcludedTabCount" units="tabs"
-    expires_after="2025-06-22">
+    expires_after="2026-01-31">
   <owner>emshack@chromium.org</owner>
   <owner>shibalik@chromium.org</owner>
   <owner>top-chrome-desktop-ui@google.com</owner>
@@ -491,10 +490,10 @@
 </histogram>
 
 <histogram name="Tab.Organization.Organization.LabelEdited" enum="Boolean"
-    expires_after="2025-06-22">
+    expires_after="2026-01-31">
   <owner>dpenning@chromium.org</owner>
   <owner>emshack@chromium.org</owner>
-  <owner>tbergquist@chromium.org</owner>
+  <owner>top-chrome-desktop-ui@google.com</owner>
   <summary>
     Whether the user has editted the label (title) of the suggested tab
     organization. Logged when a tab organization session has ended (all of the
@@ -505,10 +504,10 @@
 </histogram>
 
 <histogram name="Tab.Organization.Organization.TabRemovedCount" units="tabs"
-    expires_after="2025-07-27">
+    expires_after="2026-01-31">
   <owner>dpenning@chromium.org</owner>
   <owner>emshack@chromium.org</owner>
-  <owner>tbergquist@chromium.org</owner>
+  <owner>top-chrome-desktop-ui@google.com</owner>
   <summary>
     The count of the tabs that the user removed from the suggestd tab
     organizations after it was shown to the user. Logged when a tab organization
@@ -519,10 +518,10 @@
 </histogram>
 
 <histogram name="Tab.Organization.Response.Latency" units="ms"
-    expires_after="2025-08-24">
+    expires_after="2026-01-31">
   <owner>dpenning@chromium.org</owner>
   <owner>emshack@chromium.org</owner>
-  <owner>tbergquist@chromium.org</owner>
+  <owner>top-chrome-desktop-ui@google.com</owner>
   <summary>
     The time delta between when a tab organization request was started, and when
     the implementation of the request has completed and returned to the tab
@@ -532,10 +531,10 @@
 </histogram>
 
 <histogram name="Tab.Organization.Response.Succeeded" enum="Boolean"
-    expires_after="2025-08-24">
+    expires_after="2026-01-31">
   <owner>dpenning@chromium.org</owner>
   <owner>emshack@chromium.org</owner>
-  <owner>tbergquist@chromium.org</owner>
+  <owner>top-chrome-desktop-ui@google.com</owner>
   <summary>
     Whether the tab organization request returned valid results for exposing to
     the Tab Search UI. The request may come in the form of a remote call to a
@@ -546,10 +545,10 @@
 </histogram>
 
 <histogram name="Tab.Organization.Response.TabCount" units="tabs"
-    expires_after="2025-08-24">
+    expires_after="2026-01-31">
   <owner>dpenning@chromium.org</owner>
   <owner>emshack@chromium.org</owner>
-  <owner>tbergquist@chromium.org</owner>
+  <owner>top-chrome-desktop-ui@google.com</owner>
   <summary>
     A count of the full number of tabs that the tab organization request
     implementation has returned for grouping. Logged on the successful
@@ -558,7 +557,7 @@
 </histogram>
 
 <histogram name="Tab.Organization.SelectorCTR"
-    enum="TabOrganizationSelectorCTREvent" expires_after="2025-06-22">
+    enum="TabOrganizationSelectorCTREvent" expires_after="2026-01-31">
   <owner>emshack@chromium.org</owner>
   <owner>shibalik@chromium.org</owner>
   <owner>top-chrome-desktop-ui@google.com</owner>
@@ -572,7 +571,6 @@
 
 <histogram name="Tab.Organization.Trigger.Outcome"
     enum="TabOrganizationTriggerOutcome" expires_after="2025-08-10">
-  <owner>tbergquist@chromium.org</owner>
   <owner>dpenning@chromium.org</owner>
   <owner>top-chrome-desktop-ui@google.com</owner>
   <summary>
@@ -583,7 +581,6 @@
 
 <histogram name="Tab.Organization.TriggeredInPeriod" enum="Boolean"
     expires_after="2025-11-20">
-  <owner>tbergquist@chromium.org</owner>
   <owner>dpenning@chromium.org</owner>
   <owner>top-chrome-desktop-ui@google.com</owner>
   <summary>
@@ -594,10 +591,10 @@
 </histogram>
 
 <histogram name="Tab.Organization{EntryPoint}.Clicked" enum="Boolean"
-    expires_after="2025-08-24">
+    expires_after="2026-01-31">
   <owner>dpenning@chromium.org</owner>
   <owner>emshack@chromium.org</owner>
-  <owner>tbergquist@chromium.org</owner>
+  <owner>top-chrome-desktop-ui@google.com</owner>
   <summary>
     Logged when the user clicks {EntryPoint}. In cases where the entrypoint is
     proactively shown, logs false if the user did not interact with the
@@ -609,10 +606,10 @@
 </histogram>
 
 <histogram name="Tab.Organization{EntryPoint}.GroupCount" units="groups"
-    expires_after="2025-08-24">
+    expires_after="2026-01-31">
   <owner>dpenning@chromium.org</owner>
   <owner>emshack@chromium.org</owner>
-  <owner>tbergquist@chromium.org</owner>
+  <owner>top-chrome-desktop-ui@google.com</owner>
   <summary>
     Number of organizations that were suggested as a result of {EntryPoint}
     being clicked. Logs when the suggestion is destroyed, regardless of whether
@@ -622,10 +619,10 @@
 </histogram>
 
 <histogram name="Tab.Organization{EntryPoint}.UserChoice"
-    enum="TabOrganizationUserChoice" expires_after="2025-08-24">
+    enum="TabOrganizationUserChoice" expires_after="2026-01-31">
   <owner>dpenning@chromium.org</owner>
   <owner>emshack@chromium.org</owner>
-  <owner>tbergquist@chromium.org</owner>
+  <owner>top-chrome-desktop-ui@google.com</owner>
   <summary>
     Whether the user eventually accepted one of the Organizations that were
     suggested as a result of {EntryPoint} being clicked.
@@ -833,7 +830,7 @@
 </histogram>
 
 <histogram name="TabGroups.NumberOfRootIdsFixed" units="groups"
-    expires_after="2025-06-26">
+    expires_after="2025-11-26">
   <owner>ckitagawa@chromium.org</owner>
   <owner>clank-tab-dev@chromium.org</owner>
   <summary>
@@ -2151,7 +2148,7 @@
 </histogram>
 
 <histogram name="Tabs.NewTabButton.AccidentalClicks" enum="AccidentalClickType"
-    expires_after="2025-10-12">
+    expires_after="2026-01-31">
   <owner>emshack@chromium.org</owner>
   <owner>shibalik@chromium.org</owner>
   <owner>top-chrome-desktop-ui@google.com</owner>
@@ -2204,7 +2201,7 @@
 </histogram>
 
 <histogram name="Tabs.PageLoad.TimeSinceActive2" units="ms"
-    expires_after="2025-09-07">
+    expires_after="2026-01-31">
   <owner>emshack@chromium.org</owner>
   <owner>top-chrome-desktop-ui@google.com</owner>
   <summary>
@@ -2214,7 +2211,7 @@
 </histogram>
 
 <histogram name="Tabs.PageLoad.TimeSinceCreated2" units="ms"
-    expires_after="2025-10-12">
+    expires_after="2026-01-31">
   <owner>emshack@chromium.org</owner>
   <owner>top-chrome-desktop-ui@google.com</owner>
   <summary>
@@ -2864,7 +2861,7 @@
 </histogram>
 
 <histogram name="Tabs.TabSearch.AccidentalClicks" enum="AccidentalClickType"
-    expires_after="2025-10-12">
+    expires_after="2026-01-31">
   <owner>emshack@chromium.org</owner>
   <owner>shibalik@chromium.org</owner>
   <owner>top-chrome-desktop-ui@google.com</owner>
@@ -3097,7 +3094,7 @@
 </histogram>
 
 <histogram name="Tabs.TabSearch.TimeToClose" units="ms"
-    expires_after="2025-09-07">
+    expires_after="2026-01-31">
   <owner>emshack@chromium.org</owner>
   <owner>shibalik@chromium.org</owner>
   <owner>top-chrome-desktop-ui@google.com</owner>
@@ -3490,7 +3487,7 @@
 </histogram>
 
 <histogram name="Tabs.{DuplicateType}.{MetricType}.{WindowType}" units="tabs"
-    expires_after="2025-10-12">
+    expires_after="2026-01-31">
   <owner>emshack@chromium.org</owner>
   <owner>top-chrome-desktop-ui@google.com</owner>
   <summary>
@@ -3517,7 +3514,7 @@
 <histogram name="Tabs.{TabActiveState}TabWidth" units="px"
     expires_after="2025-09-14">
   <owner>dpenning@chromium.org</owner>
-  <owner>tbergquist@chromium.org</owner>
+  <owner>top-chrome-desktop-ui@google.com</owner>
   <summary>
     [Desktop] The size in pixels of the {TabActiveState} tab logged when a tab
     is added. Used to collect size data for scrolling tabs.
diff --git a/tools/metrics/histograms/metadata/variations/histograms.xml b/tools/metrics/histograms/metadata/variations/histograms.xml
index ed5e8e3..d430b15 100644
--- a/tools/metrics/histograms/metadata/variations/histograms.xml
+++ b/tools/metrics/histograms/metadata/variations/histograms.xml
@@ -392,7 +392,6 @@
 
 <histogram name="Variations.SafeMode.LoadSafeSeed.Result"
     enum="VariationsSeedLoadResult" expires_after="2025-09-14">
-  <owner>isherman@chromium.org</owner>
   <owner>asvitkine@chromium.org</owner>
   <owner>src/base/metrics/OWNERS</owner>
   <summary>
@@ -419,7 +418,6 @@
 
 <histogram name="Variations.SafeMode.StoreSafeSeed.Result"
     enum="VariationsSeedStoreResult" expires_after="2025-09-14">
-  <owner>isherman@chromium.org</owner>
   <owner>asvitkine@chromium.org</owner>
   <owner>src/base/metrics/OWNERS</owner>
   <summary>
@@ -432,7 +430,6 @@
 
 <histogram name="Variations.SafeMode.StoreSafeSeed.SignatureValidity"
     enum="VariationSeedSignature" expires_after="2023-06-04">
-  <owner>isherman@chromium.org</owner>
   <owner>asvitkine@chromium.org</owner>
   <owner>src/base/metrics/OWNERS</owner>
   <summary>
@@ -445,7 +442,6 @@
 
 <histogram name="Variations.SafeMode.Streak.Crashes" units="crashes"
     expires_after="2025-09-14">
-  <owner>isherman@chromium.org</owner>
   <owner>asvitkine@chromium.org</owner>
   <owner>src/base/metrics/OWNERS</owner>
   <summary>
@@ -459,7 +455,6 @@
 
 <histogram name="Variations.SafeMode.Streak.FetchFailures" units="failures"
     expires_after="2025-09-14">
-  <owner>isherman@chromium.org</owner>
   <owner>asvitkine@chromium.org</owner>
   <owner>src/base/metrics/OWNERS</owner>
   <summary>
@@ -671,7 +666,6 @@
 
 <histogram name="Variations.SeedProcessingTime" units="ms"
     expires_after="2025-09-14">
-  <owner>isherman@chromium.org</owner>
   <owner>asvitkine@chromium.org</owner>
   <owner>src/base/metrics/OWNERS</owner>
   <summary>
diff --git a/tools/metrics/histograms/metadata/windows/histograms.xml b/tools/metrics/histograms/metadata/windows/histograms.xml
index a3cbc23..a4c66d47 100644
--- a/tools/metrics/histograms/metadata/windows/histograms.xml
+++ b/tools/metrics/histograms/metadata/windows/histograms.xml
@@ -33,6 +33,17 @@
   </summary>
 </histogram>
 
+<histogram name="Windows.AutoDeElevateResult" enum="Hresult"
+    expires_after="2026-05-07">
+  <owner>wfh@chromium.org</owner>
+  <owner>ssmole@microsoft.com</owner>
+  <summary>
+    Records the HRESULT from the attempt to automatically de-elevate the browser
+    process, which only happens on startup, if it's launched with an elevated
+    linked process token.
+  </summary>
+</histogram>
+
 <histogram name="Windows.CetAvailable" enum="BooleanAvailable"
     expires_after="2025-09-09">
   <owner>ajgo@chromium.org</owner>
diff --git a/tools/perf/core/perfetto_binary_roller/binary_deps.json b/tools/perf/core/perfetto_binary_roller/binary_deps.json
index 9262698..56aaac7 100644
--- a/tools/perf/core/perfetto_binary_roller/binary_deps.json
+++ b/tools/perf/core/perfetto_binary_roller/binary_deps.json
@@ -5,8 +5,8 @@
             "full_remote_path": "perfetto-luci-artifacts/v50.1/linux-arm64/trace_processor_shell"
         },
         "win": {
-            "hash": "9e346195dd5c25e2964e78a79818aea953627f9f",
-            "full_remote_path": "chromium-telemetry/perfetto_binaries/trace_processor_shell/win/bff34086ee3abf81659b5184c243f4cf2f799992/trace_processor_shell.exe"
+            "hash": "8d81c216e59a8ccb7a111ff732216ccc8693e006",
+            "full_remote_path": "chromium-telemetry/perfetto_binaries/trace_processor_shell/win/4402fd7953d5dedf65659e99ab80404f6e94a004/trace_processor_shell.exe"
         },
         "linux_arm": {
             "hash": "99f971ca131f6d11c73f4b918099d434bdd8093c",
@@ -21,8 +21,8 @@
             "full_remote_path": "perfetto-luci-artifacts/v50.1/mac-arm64/trace_processor_shell"
         },
         "linux": {
-            "hash": "cff89dc4786275cada405f33015001ded93501c6",
-            "full_remote_path": "chromium-telemetry/perfetto_binaries/trace_processor_shell/linux/bff34086ee3abf81659b5184c243f4cf2f799992/trace_processor_shell"
+            "hash": "f39f997a5a3378fd919736e3e2241afed2e26829",
+            "full_remote_path": "chromium-telemetry/perfetto_binaries/trace_processor_shell/linux/4402fd7953d5dedf65659e99ab80404f6e94a004/trace_processor_shell"
         }
     },
     "power_profile.sql": {
diff --git a/ui/accessibility/platform/test_ax_platform_tree_manager_delegate.cc b/ui/accessibility/platform/test_ax_platform_tree_manager_delegate.cc
index 33903f82..c8491c3 100644
--- a/ui/accessibility/platform/test_ax_platform_tree_manager_delegate.cc
+++ b/ui/accessibility/platform/test_ax_platform_tree_manager_delegate.cc
@@ -68,7 +68,7 @@
 
 content::WebContentsAccessibility*
 TestAXPlatformTreeManagerDelegate::AccessibilityGetWebContentsAccessibility() {
-  return nullptr;
+  return web_contents_accessibility_;
 }
 
 bool TestAXPlatformTreeManagerDelegate::AccessibilityIsWebContentSource() {
diff --git a/ui/accessibility/platform/test_ax_platform_tree_manager_delegate.h b/ui/accessibility/platform/test_ax_platform_tree_manager_delegate.h
index 8fb74f9c..f105fe48 100644
--- a/ui/accessibility/platform/test_ax_platform_tree_manager_delegate.h
+++ b/ui/accessibility/platform/test_ax_platform_tree_manager_delegate.h
@@ -14,6 +14,11 @@
  public:
   TestAXPlatformTreeManagerDelegate();
 
+  void SetWebContentsAccessibility(
+      content::WebContentsAccessibility* web_contents_accessibility) {
+    web_contents_accessibility_ = web_contents_accessibility;
+  }
+
   void AccessibilityPerformAction(const AXActionData& data) override;
   bool AccessibilityViewHasFocus() override;
   void AccessibilityViewSetFocus() override;
@@ -40,6 +45,8 @@
 
   bool is_root_frame_;
   gfx::AcceleratedWidget accelerated_widget_;
+  raw_ptr<content::WebContentsAccessibility> web_contents_accessibility_ =
+      nullptr;
 };
 
 }  // namespace ui
diff --git a/ui/base/cursor/cursor.h b/ui/base/cursor/cursor.h
index 36cf172d..415dea5 100644
--- a/ui/base/cursor/cursor.h
+++ b/ui/base/cursor/cursor.h
@@ -73,10 +73,8 @@
 
   // Note: custom cursor comparison may perform expensive pixel equality checks!
   bool operator==(const Cursor& cursor) const;
-  bool operator!=(const Cursor& cursor) const { return !(*this == cursor); }
 
   bool operator==(mojom::CursorType type) const { return type_ == type; }
-  bool operator!=(mojom::CursorType type) const { return type_ != type; }
 
   // Limit the size of cursors so that they cannot be used to cover UI
   // elements in chrome.
diff --git a/ui/display/manager/display_layout_store.h b/ui/display/manager/display_layout_store.h
index 00f981f..c5f09ec 100644
--- a/ui/display/manager/display_layout_store.h
+++ b/ui/display/manager/display_layout_store.h
@@ -52,7 +52,7 @@
                             bool default_unified);
 
  private:
-  friend void DisplayManager::UpdateDisplaysWith(
+  friend bool DisplayManager::UpdateDisplaysWith(
       const std::vector<ManagedDisplayInfo>& updated_display_info_list);
 
   // Returns a layout for the given `display_id_list` or create one if no layout
diff --git a/ui/display/manager/display_manager.cc b/ui/display/manager/display_manager.cc
index 0cb7b18..696c90c6 100644
--- a/ui/display/manager/display_manager.cc
+++ b/ui/display/manager/display_manager.cc
@@ -978,7 +978,7 @@
                                      : gfx::Insets();
 }
 
-void DisplayManager::OnNativeDisplaysChanged(
+bool DisplayManager::OnNativeDisplaysChanged(
     const DisplayInfoList& updated_displays) {
   DISPLAY_LOG(EVENT) << "Native displays updated"
                      << ". Unified desktop allowed: "
@@ -1028,7 +1028,7 @@
         }
       }
     }
-    return;
+    return false;
   }
 
   first_display_id_ = updated_displays[0].id();
@@ -1127,7 +1127,7 @@
   mirroring_source_id_ = mirroring_source_id;
   connected_display_id_list_ = CreateDisplayIdList(updated_displays);
 
-  UpdateDisplaysWith(new_display_info_list);
+  return UpdateDisplaysWith(new_display_info_list);
 }
 
 void DisplayManager::UpdateDisplays() {
@@ -1139,7 +1139,7 @@
   UpdateDisplaysWith(display_info_list);
 }
 
-void DisplayManager::UpdateDisplaysWith(
+bool DisplayManager::UpdateDisplaysWith(
     const DisplayInfoList& updated_display_info_list) {
   base::AutoReset<bool> is_updating_displays_resetter(&is_updating_displays_,
                                                       true);
@@ -1492,7 +1492,7 @@
   // Create the mirroring window asynchronously after all displays
   // are added so that it can mirror the display newly added. This can
   // happen when switching from dock mode to software mirror mode.
-  CreateMirrorWindowAsyncIfAny();
+  return CreateMirrorWindowAsyncIfAny();
 }
 
 const Display& DisplayManager::GetDisplayAt(size_t index) const {
@@ -2017,16 +2017,18 @@
   return true;
 }
 
-void DisplayManager::CreateMirrorWindowAsyncIfAny() {
+bool DisplayManager::CreateMirrorWindowAsyncIfAny() {
   // Do not post a task if the software mirroring doesn't exist, or
   // during initialization when compositor's init task isn't posted yet.
   // ash::Shell::Init() will call this after the compositor is initialized.
   if (software_mirroring_display_list_.empty() || !delegate_) {
-    return;
+    return false;
   }
   base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
       FROM_HERE, base::BindOnce(&DisplayManager::CreateMirrorWindowIfAny,
                                 weak_ptr_factory_.GetWeakPtr()));
+
+  return true;
 }
 
 void DisplayManager::UpdateInternalManagedDisplayModeListForTest() {
@@ -2599,13 +2601,7 @@
 }
 
 void DisplayManager::RunPendingTasksForTest() {
-  if (software_mirroring_display_list_.empty() ||
-      // When there is 0 displays, wait for display reconnection to update
-      // layout.
-      (active_display_list_.empty() || !active_display_list_[0].detected())) {
-    return;
-  }
-
+  CHECK(!software_mirroring_display_list_.empty() && delegate_);
   base::RunLoop run_loop;
   created_mirror_window_ = run_loop.QuitClosure();
   run_loop.Run();
diff --git a/ui/display/manager/display_manager.h b/ui/display/manager/display_manager.h
index 106cad9..217a150 100644
--- a/ui/display/manager/display_manager.h
+++ b/ui/display/manager/display_manager.h
@@ -274,7 +274,7 @@
   // Called when display configuration has changed. The new display
   // configurations is passed as a vector of Display object, which contains each
   // display's new information.
-  void OnNativeDisplaysChanged(
+  bool OnNativeDisplaysChanged(
       const std::vector<ManagedDisplayInfo>& display_info_list);
 
   // Updates current displays using current |display_info_|.
@@ -488,7 +488,7 @@
 
   // Creates mirror window asynchronously if the software mirror mode is
   // enabled.
-  void CreateMirrorWindowAsyncIfAny();
+  bool CreateMirrorWindowAsyncIfAny();
 
   // A unit test may change the internal display id (which never happens on a
   // real device). This will update the mode list for internal display for this
@@ -590,7 +590,7 @@
 
   // Updates the internal display data using `updated_display_info_list` and
   // notifies observers about the changes.
-  void UpdateDisplaysWith(
+  bool UpdateDisplaysWith(
       const std::vector<ManagedDisplayInfo>& updated_display_info_list);
 
   // Creates software mirroring display related information. The display used to
diff --git a/ui/display/test/display_manager_test_api.cc b/ui/display/test/display_manager_test_api.cc
index 10ea36746..3a4c287 100644
--- a/ui/display/test/display_manager_test_api.cc
+++ b/ui/display/test/display_manager_test_api.cc
@@ -189,9 +189,11 @@
     }
   }
 
-  display_manager_->OnNativeDisplaysChanged(display_list_copy);
+  bool tasks = display_manager_->OnNativeDisplaysChanged(display_list_copy);
   display_manager_->UpdateInternalManagedDisplayModeListForTest();
-  display_manager_->RunPendingTasksForTest();
+  if (tasks) {
+    display_manager_->RunPendingTasksForTest();
+  }
 }
 
 int64_t DisplayManagerTestApi::SetFirstDisplayAsInternalDisplay() {
diff --git a/ui/events/BUILD.gn b/ui/events/BUILD.gn
index f9bfa14..ca4ac5ec 100644
--- a/ui/events/BUILD.gn
+++ b/ui/events/BUILD.gn
@@ -222,6 +222,7 @@
     "event_targeter.h",
     "event_utils.h",
     "events_export.h",
+    "features.h",
     "gestures/gesture_recognizer.h",
     "gestures/gesture_types.h",
     "null_event_targeter.h",
@@ -241,6 +242,7 @@
     "event_utils.cc",
     "events_exports.cc",
     "events_stub.cc",
+    "features.cc",
     "gestures/gesture_recognizer.cc",
     "gestures/gesture_types.cc",
     "null_event_targeter.cc",
diff --git a/ui/events/event.cc b/ui/events/event.cc
index cff87755..280ed1b7 100644
--- a/ui/events/event.cc
+++ b/ui/events/event.cc
@@ -10,6 +10,7 @@
 #include <string>
 #include <utility>
 
+#include "base/feature_list.h"
 #include "base/logging.h"
 #include "base/memory/ptr_util.h"
 #include "base/metrics/histogram.h"
@@ -26,6 +27,7 @@
 #include "ui/events/base_event_utils.h"
 #include "ui/events/event_constants.h"
 #include "ui/events/event_utils.h"
+#include "ui/events/features.h"
 #include "ui/events/keycodes/dom/dom_code.h"
 #include "ui/events/keycodes/dom/dom_key.h"
 #include "ui/events/keycodes/dom/keycode_converter.h"
@@ -886,8 +888,11 @@
 
   // Check if this is a key repeat. This must be called before initial flags
   // processing, e.g: NormalizeFlags(), to avoid issues like crbug.com/1069690.
-  if (synthesize_key_repeat_enabled_ && IsRepeated(GetLastKeyEvent()))
+  if (synthesize_key_repeat_enabled_ &&
+      base::FeatureList::IsEnabled(kLegacyKeyRepeatSynthesis) &&
+      IsRepeated(GetLastKeyEvent())) {
     SetFlags(flags() | EF_IS_REPEAT);
+  }
 
 #if BUILDFLAG(IS_LINUX)
   NormalizeFlags();
diff --git a/ui/events/event_unittest.cc b/ui/events/event_unittest.cc
index c1b35d8..2f5d175 100644
--- a/ui/events/event_unittest.cc
+++ b/ui/events/event_unittest.cc
@@ -19,6 +19,7 @@
 
 #include "base/strings/strcat.h"
 #include "base/test/metrics/histogram_tester.h"
+#include "base/test/scoped_feature_list.h"
 #include "base/test/simple_test_tick_clock.h"
 #include "base/test/task_environment.h"
 #include "build/build_config.h"
@@ -27,6 +28,7 @@
 #include "ui/base/ui_base_features.h"
 #include "ui/events/event_constants.h"
 #include "ui/events/event_utils.h"
+#include "ui/events/features.h"
 #include "ui/events/keycodes/dom/dom_code.h"
 #include "ui/events/keycodes/dom/keycode_converter.h"
 #include "ui/events/keycodes/keyboard_code_conversion.h"
@@ -127,10 +129,13 @@
   EXPECT_FALSE(MouseEvent::IsRepeatedClickEvent(event1, event2));
 }
 
-// Automatic repeat flag setting is disabled on Wayland,
-// because the repeated event is generated inside ui/ozone/platform/wayland
-// and reliable.
+// TODO(https://crbug.com/411681432) Remove this test when IsRepeated is
+// removed.
 TEST(EventTest, RepeatedKeyEvent) {
+  // Ensure legacy key repeat synthesis feature is enabled.
+  base::test::ScopedFeatureList feature_list;
+  feature_list.InitAndEnableFeature(kLegacyKeyRepeatSynthesis);
+
   base::TimeTicks start = base::TimeTicks::Now();
   base::TimeTicks time1 = start + base::Milliseconds(1);
   base::TimeTicks time2 = start + base::Milliseconds(2);
@@ -153,13 +158,12 @@
   EXPECT_NE(event4.flags() & EF_IS_REPEAT, 0);
 }
 
+// TODO(https://crbug.com/411681432) Remove this test when IsRepeated is
+// removed.
 TEST(EventTest, NoRepeatedKeyEvent) {
-  // Temporarily set the global synthesize_key_repeat_enabled to false.
-  absl::Cleanup scoped_restore_settings =
-      [old_value = KeyEvent::IsSynthesizeKeyRepeatEnabled()] {
-        KeyEvent::SetSynthesizeKeyRepeatEnabled(old_value);
-      };
-  KeyEvent::SetSynthesizeKeyRepeatEnabled(false);
+  // Ensure legacy key repeat synthesis feature is disabled.
+  base::test::ScopedFeatureList feature_list;
+  feature_list.InitAndDisableFeature(kLegacyKeyRepeatSynthesis);
 
   base::TimeTicks start = base::TimeTicks::Now();
   base::TimeTicks time1 = start + base::Milliseconds(1);
diff --git a/ui/events/features.cc b/ui/events/features.cc
new file mode 100644
index 0000000..64ce4c3
--- /dev/null
+++ b/ui/events/features.cc
@@ -0,0 +1,15 @@
+// Copyright 2025 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "ui/events/features.h"
+
+#include "build/build_config.h"
+
+namespace ui {
+
+BASE_FEATURE(kLegacyKeyRepeatSynthesis,
+             "LegacyKeyRepeatSynthesis",
+             base::FEATURE_ENABLED_BY_DEFAULT);
+
+}  // namespace ui
diff --git a/ui/events/features.h b/ui/events/features.h
new file mode 100644
index 0000000..3d803aed
--- /dev/null
+++ b/ui/events/features.h
@@ -0,0 +1,27 @@
+// Copyright 2025 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef UI_EVENTS_FEATURES_H_
+#define UI_EVENTS_FEATURES_H_
+
+#include "base/feature_list.h"
+#include "ui/events/events_export.h"
+
+namespace ui {
+
+// Until recently, Chrome on most platforms relied solely on a heuristic in
+// ui::KeyEvent construction to determine if they are repeat key events. Chrome
+// recently shifted to reading key repeat information from the native OS key
+// event on all platforms (see https://crbug.com/40940886), which theoretically
+// makes this heuristic redundant.
+//
+// This feature flag is used to gradually turn off this heuristic and also
+// serves as an emergency killswitch in case turning it off causes major
+// issues. See tracking bug https://crbug.com/411681432 for more info.
+EVENTS_EXPORT
+BASE_DECLARE_FEATURE(kLegacyKeyRepeatSynthesis);
+
+}  // namespace ui
+
+#endif  // UI_EVENTS_FEATURES_H_
diff --git a/ui/gl/gl_implementation.h b/ui/gl/gl_implementation.h
index f3bd65a3..9f29bca 100644
--- a/ui/gl/gl_implementation.h
+++ b/ui/gl/gl_implementation.h
@@ -59,25 +59,15 @@
   GLImplementation gl = kGLImplementationNone;
   ANGLEImplementation angle = ANGLEImplementation::kNone;
 
-  constexpr bool operator==(const GLImplementationParts& other) const {
-    return (gl == other.gl && angle == other.angle);
-  }
-  constexpr bool operator!=(const GLImplementationParts& other) const {
-    return !operator==(other);
-  }
+  friend constexpr bool operator==(const GLImplementationParts&,
+                                   const GLImplementationParts&) = default;
 
   constexpr bool operator==(const ANGLEImplementation angle_impl) const {
-    return operator==(GLImplementationParts(angle_impl));
-  }
-  constexpr bool operator!=(const ANGLEImplementation angle_impl) const {
-    return !operator==(angle_impl);
+    return *this == GLImplementationParts(angle_impl);
   }
 
   constexpr bool operator==(const GLImplementation gl_impl) const {
-    return operator==(GLImplementationParts(gl_impl));
-  }
-  constexpr bool operator!=(const GLImplementation gl_impl) const {
-    return !operator==(gl_impl);
+    return *this == GLImplementationParts(gl_impl);
   }
 
   bool IsValid() const;
diff --git a/ui/ozone/platform/x11/test/events_x_unittest.cc b/ui/ozone/platform/x11/test/events_x_unittest.cc
index c515b54..6c3c8fa0 100644
--- a/ui/ozone/platform/x11/test/events_x_unittest.cc
+++ b/ui/ozone/platform/x11/test/events_x_unittest.cc
@@ -11,6 +11,7 @@
 #include <utility>
 
 #include "base/test/metrics/histogram_tester.h"
+#include "base/test/scoped_feature_list.h"
 #include "base/test/simple_test_tick_clock.h"
 #include "build/build_config.h"
 #include "testing/gtest/include/gtest/gtest.h"
@@ -19,6 +20,7 @@
 #include "ui/events/event.h"
 #include "ui/events/event_constants.h"
 #include "ui/events/event_utils.h"
+#include "ui/events/features.h"
 #include "ui/events/keycodes/dom/dom_code.h"
 #include "ui/events/keycodes/dom/keycode_converter.h"
 #include "ui/events/test/events_test_utils.h"
@@ -689,6 +691,10 @@
 }  // namespace
 
 TEST_F(EventsXTest, AutoRepeat) {
+  // Ensure legacy key repeat synthesis is enabled.
+  base::test::ScopedFeatureList feature_list;
+  feature_list.InitAndEnableFeature(kLegacyKeyRepeatSynthesis);
+
   const uint16_t kNativeCodeA =
       ui::KeycodeConverter::DomCodeToNativeKeycode(DomCode::US_A);
   const uint16_t kNativeCodeB =
diff --git a/ui/ozone/platform/x11/test/x11_event_translation_unittest.cc b/ui/ozone/platform/x11/test/x11_event_translation_unittest.cc
index 2525890..984e284b 100644
--- a/ui/ozone/platform/x11/test/x11_event_translation_unittest.cc
+++ b/ui/ozone/platform/x11/test/x11_event_translation_unittest.cc
@@ -6,12 +6,14 @@
 
 #include <xcb/xcb.h>
 
+#include "base/test/scoped_feature_list.h"
 #include "base/time/time.h"
 #include "testing/gtest/include/gtest/gtest.h"
 #include "ui/events/base_event_utils.h"
 #include "ui/events/event.h"
 #include "ui/events/event_constants.h"
 #include "ui/events/event_utils.h"
+#include "ui/events/features.h"
 #include "ui/events/keycodes/dom/dom_code.h"
 #include "ui/events/keycodes/dom/dom_key.h"
 #include "ui/events/keycodes/dom/keycode_converter.h"
@@ -254,6 +256,10 @@
 // their counterparts are mixed. Ensures regressions like crbug.com/1069690
 // are not reintroduced in the future.
 TEST(XEventTranslationTest, KeyModifiersCounterpartRepeat) {
+  // Ensure legacy key repeat synthesis is enabled.
+  base::test::ScopedFeatureList feature_list;
+  feature_list.InitAndEnableFeature(kLegacyKeyRepeatSynthesis);
+
   // Use a TestTickClock so we have the power to control the time :)
   test::ScopedEventTestTickClock test_clock;
 
diff --git a/ui/views/interaction/interactive_views_test_internal.cc b/ui/views/interaction/interactive_views_test_internal.cc
index 2d94f039..1930228 100644
--- a/ui/views/interaction/interactive_views_test_internal.cc
+++ b/ui/views/interaction/interactive_views_test_internal.cc
@@ -356,7 +356,10 @@
 std::string InteractiveViewsTestPrivate::DebugDumpWidget(
     const Widget& widget) const {
   std::string description = widget.GetName();
-  return base::StrCat({widget.GetClassName(), " \"", widget.GetName(), "\" at ",
+  return base::StrCat({// At any time, at most one widget can be active. It is
+                       // the widget that accepts keyboard inputs.
+                       widget.IsActive() ? "[ACTIVE] " : "",
+                       widget.GetClassName(), " \"", widget.GetName(), "\" at ",
                        DebugDumpBounds(widget.GetWindowBoundsInScreen())});
 }
 
diff --git a/ui/webui/resources/images/BUILD.gn b/ui/webui/resources/images/BUILD.gn
index 18c1b4c..d4b003f 100644
--- a/ui/webui/resources/images/BUILD.gn
+++ b/ui/webui/resources/images/BUILD.gn
@@ -48,7 +48,10 @@
   ]
 
   if (!is_ios) {
-    input_files += [ "chrome_logo_dark.svg" ]
+    input_files += [
+      "chrome_logo_dark.svg",
+      "icon_edit.svg",
+    ]
   }
 
   if (!is_android && !is_ios) {
@@ -67,7 +70,6 @@
       "icon_clear.svg",
       "icon_clock.svg",
       "icon_delete_gray.svg",
-      "icon_edit.svg",
       "icon_filetype_generic.svg",
       "icon_folder_open.svg",
       "icon_history.svg",
diff --git a/ui/webui/resources/mojo/BUILD.gn b/ui/webui/resources/mojo/BUILD.gn
index 9312931..c049ae67 100644
--- a/ui/webui/resources/mojo/BUILD.gn
+++ b/ui/webui/resources/mojo/BUILD.gn
@@ -14,6 +14,8 @@
   "mojo/public/mojom/base/absl_status.mojom-webui.ts",
   "mojo/public/mojom/base/big_string.mojom-webui.ts",
   "mojo/public/mojom/base/big_buffer.mojom-webui.ts",
+  "mojo/public/mojom/base/empty.mojom-webui.ts",
+  "mojo/public/mojom/base/error.mojom-webui.ts",
   "mojo/public/mojom/base/file.mojom-webui.ts",
   "mojo/public/mojom/base/file_path.mojom-webui.ts",
   "mojo/public/mojom/base/int128.mojom-webui.ts",
diff --git a/url/third_party/mozilla/url_parse.h b/url/third_party/mozilla/url_parse.h
index 4109f0bf5..7a2277e 100644
--- a/url/third_party/mozilla/url_parse.h
+++ b/url/third_party/mozilla/url_parse.h
@@ -6,6 +6,7 @@
 #define URL_THIRD_PARTY_MOZILLA_URL_PARSE_H_
 
 #include <iosfwd>
+#include <optional>
 #include <string_view>
 
 #include "base/check.h"
@@ -58,6 +59,17 @@
     return std::basic_string_view(&source[begin], len);
   }
 
+  // Returns a std::optional<string_view> using `source` as a backend.
+  // Returns std::nullopt if the component is invalid.
+  template <typename CharT>
+  std::optional<std::basic_string_view<CharT>> maybe_as_string_view_on(
+      const CharT* source) const {
+    if (!is_valid()) {
+      return std::nullopt;
+    }
+    return std::basic_string_view(&source[begin], len);
+  }
+
   int begin;  // Byte offset in the string of this component.
   int len;    // Will be -1 if the component is unspecified.
 };
diff --git a/url/url_canon.h b/url/url_canon.h
index 99e5ef4..9de4f64a 100644
--- a/url/url_canon.h
+++ b/url/url_canon.h
@@ -13,6 +13,7 @@
 #include <stdlib.h>
 #include <string.h>
 
+#include <optional>
 #include <string_view>
 
 #include "base/check_op.h"
@@ -726,13 +727,11 @@
 // This function will not fail. If the input is invalid UTF-8/UTF-16, we'll use
 // the "Unicode replacement character" for the confusing bits and copy the rest.
 COMPONENT_EXPORT(URL)
-void CanonicalizeRef(const char* spec,
-                     const Component& path,
+void CanonicalizeRef(std::optional<std::string_view> spec,
                      CanonOutput* output,
                      Component* out_path);
 COMPONENT_EXPORT(URL)
-void CanonicalizeRef(const char16_t* spec,
-                     const Component& path,
+void CanonicalizeRef(std::optional<std::u16string_view> spec,
                      CanonOutput* output,
                      Component* out_path);
 
diff --git a/url/url_canon_etc.cc b/url/url_canon_etc.cc
index def7b66..dcb5e3f 100644
--- a/url/url_canon_etc.cc
+++ b/url/url_canon_etc.cc
@@ -8,11 +8,13 @@
 #endif
 
 #include <array>
+#include <string_view>
 
 // Canonicalizers for random bits that aren't big enough for their own files.
 
 #include <string.h>
 
+#include "url/third_party/mozilla/url_parse.h"
 #include "url/url_canon.h"
 #include "url/url_canon_internal.h"
 
@@ -304,15 +306,15 @@
 // clang-format on
 
 template <typename CHAR, typename UCHAR>
-void DoCanonicalizeRef(const CHAR* spec,
-                       const Component& ref,
+void DoCanonicalizeRef(std::optional<std::basic_string_view<CHAR>> input,
                        CanonOutput* output,
                        Component* out_ref) {
-  if (!ref.is_valid()) {
+  if (!input.has_value()) {
     // Common case of no ref.
     *out_ref = Component();
     return;
   }
+  auto input_value = input.value();
 
   // Append the ref separator. Note that we need to do this even when the ref
   // is empty but present.
@@ -320,19 +322,18 @@
   out_ref->begin = output->length();
 
   // Now iterate through all the characters, converting to UTF-8 and validating.
-  size_t end = static_cast<size_t>(ref.end());
-  for (size_t i = static_cast<size_t>(ref.begin); i < end; i++) {
-    UCHAR current_char = static_cast<UCHAR>(spec[i]);
+  for (size_t i = 0; i < input_value.length(); ++i) {
+    UCHAR current_char = static_cast<UCHAR>(input.value()[i]);
     if (current_char < 0x80) {
       if (kShouldEscapeCharInFragment[current_char])
-        AppendEscapedChar(static_cast<unsigned char>(spec[i]), output);
+        AppendEscapedChar(static_cast<unsigned char>(input_value[i]), output);
       else
-        output->push_back(static_cast<char>(spec[i]));
+        output->push_back(static_cast<char>(input_value[i]));
     } else {
-      AppendUTF8EscapedChar(spec, &i, end, output);
+      AppendUTF8EscapedChar(input_value.data(), &i, input_value.length(),
+                            output);
     }
   }
-
   out_ref->len = output->length() - out_ref->begin;
 }
 
@@ -418,18 +419,16 @@
                                     out_port);
 }
 
-void CanonicalizeRef(const char* spec,
-                     const Component& ref,
+void CanonicalizeRef(std::optional<std::string_view> input,
                      CanonOutput* output,
                      Component* out_ref) {
-  DoCanonicalizeRef<char, unsigned char>(spec, ref, output, out_ref);
+  DoCanonicalizeRef<char, unsigned char>(input, output, out_ref);
 }
 
-void CanonicalizeRef(const char16_t* spec,
-                     const Component& ref,
+void CanonicalizeRef(std::optional<std::u16string_view> input,
                      CanonOutput* output,
                      Component* out_ref) {
-  DoCanonicalizeRef<char16_t, char16_t>(spec, ref, output, out_ref);
+  DoCanonicalizeRef<char16_t, char16_t>(input, output, out_ref);
 }
 
 }  // namespace url
diff --git a/url/url_canon_filesystemurl.cc b/url/url_canon_filesystemurl.cc
index 2510005..8f93c22 100644
--- a/url/url_canon_filesystemurl.cc
+++ b/url/url_canon_filesystemurl.cc
@@ -4,6 +4,8 @@
 
 // Functions for canonicalizing "filesystem:file:" URLs.
 
+#include <optional>
+
 #include "url/url_canon.h"
 #include "url/url_canon_internal.h"
 #include "url/url_file.h"
@@ -74,7 +76,8 @@
   // Ignore failures for query/ref since the URL can probably still be loaded.
   CanonicalizeQuery(source.query, parsed.query, charset_converter,
                     output, &new_parsed->query);
-  CanonicalizeRef(source.ref, parsed.ref, output, &new_parsed->ref);
+  CanonicalizeRef(parsed.ref.maybe_as_string_view_on(source.ref), output,
+                  &new_parsed->ref);
   if (success)
     new_parsed->set_inner_parsed(new_inner_parsed);
 
diff --git a/url/url_canon_fileurl.cc b/url/url_canon_fileurl.cc
index be36fa02..4d24d370 100644
--- a/url/url_canon_fileurl.cc
+++ b/url/url_canon_fileurl.cc
@@ -172,7 +172,8 @@
 
   CanonicalizeQuery(source.query, parsed.query, query_converter,
                     output, &new_parsed->query);
-  CanonicalizeRef(source.ref, parsed.ref, output, &new_parsed->ref);
+  CanonicalizeRef(parsed.ref.maybe_as_string_view_on(source.ref), output,
+                  &new_parsed->ref);
 
   return success;
 }
diff --git a/url/url_canon_non_special_url.cc b/url/url_canon_non_special_url.cc
index 008a755..8c55bac1 100644
--- a/url/url_canon_non_special_url.cc
+++ b/url/url_canon_non_special_url.cc
@@ -167,7 +167,8 @@
                     &new_parsed.query);
 
   // Ref: ignore failure for this, since the page can probably still be loaded.
-  CanonicalizeRef(source.ref, parsed.ref, &output, &new_parsed.ref);
+  CanonicalizeRef(parsed.ref.maybe_as_string_view_on(source.ref), &output,
+                  &new_parsed.ref);
 
   // Carry over the flag for potentially dangling markup:
   if (parsed.potentially_dangling_markup) {
diff --git a/url/url_canon_pathurl.cc b/url/url_canon_pathurl.cc
index 499384a..17269084 100644
--- a/url/url_canon_pathurl.cc
+++ b/url/url_canon_pathurl.cc
@@ -81,7 +81,8 @@
   CanonicalizeQuery(source.query, parsed.query, nullptr, output,
                     &new_parsed->query);
 
-  CanonicalizeRef(source.ref, parsed.ref, output, &new_parsed->ref);
+  CanonicalizeRef(parsed.ref.maybe_as_string_view_on(source.ref), output,
+                  &new_parsed->ref);
 
   return success;
 }
diff --git a/url/url_canon_relative.cc b/url/url_canon_relative.cc
index 3280f8a..bacc89e 100644
--- a/url/url_canon_relative.cc
+++ b/url/url_canon_relative.cc
@@ -445,7 +445,8 @@
     // Finish with the query and reference part (these can't fail).
     CanonicalizeQuery(relative_url, query, query_converter,
                       output, &out_parsed->query);
-    CanonicalizeRef(relative_url, ref, output, &out_parsed->ref);
+    CanonicalizeRef(ref.maybe_as_string_view_on(relative_url), output,
+                    &out_parsed->ref);
 
     // Fix the path beginning to add back the "C:" we may have written above.
     out_parsed->path = MakeRange(true_path_begin, out_parsed->path.end());
@@ -460,7 +461,8 @@
     // failures for refs)
     CanonicalizeQuery(relative_url, query, query_converter,
                       output, &out_parsed->query);
-    CanonicalizeRef(relative_url, ref, output, &out_parsed->ref);
+    CanonicalizeRef(ref.maybe_as_string_view_on(relative_url), output,
+                    &out_parsed->ref);
     return success;
   }
 
@@ -473,7 +475,8 @@
 
   if (ref.is_valid()) {
     // Just the reference specified: replace it (ignoring failures).
-    CanonicalizeRef(relative_url, ref, output, &out_parsed->ref);
+    CanonicalizeRef(ref.as_string_view_on(relative_url), output,
+                    &out_parsed->ref);
     return success;
   }
 
diff --git a/url/url_canon_stdurl.cc b/url/url_canon_stdurl.cc
index a4af795..659d0ee 100644
--- a/url/url_canon_stdurl.cc
+++ b/url/url_canon_stdurl.cc
@@ -108,7 +108,8 @@
                     output, &new_parsed->query);
 
   // Ref: ignore failure for this, since the page can probably still be loaded.
-  CanonicalizeRef(source.ref, parsed.ref, output, &new_parsed->ref);
+  CanonicalizeRef(parsed.ref.maybe_as_string_view_on(source.ref), output,
+                  &new_parsed->ref);
 
   // Carry over the flag for potentially dangling markup:
   if (parsed.potentially_dangling_markup)
diff --git a/url/url_canon_unittest.cc b/url/url_canon_unittest.cc
index 364fa6fc..b297c7c2 100644
--- a/url/url_canon_unittest.cc
+++ b/url/url_canon_unittest.cc
@@ -1710,7 +1710,8 @@
 
       std::string out_str;
       StdStringCanonOutput output(&out_str);
-      CanonicalizeRef(ref_case.input8, in_comp, &output, &out_comp);
+      CanonicalizeRef(in_comp.maybe_as_string_view_on(ref_case.input8), &output,
+                      &out_comp);
       output.Complete();
 
       EXPECT_EQ(ref_case.expected_component.begin, out_comp.begin);
@@ -1728,7 +1729,8 @@
 
       std::string out_str;
       StdStringCanonOutput output(&out_str);
-      CanonicalizeRef(input16.c_str(), in_comp, &output, &out_comp);
+      CanonicalizeRef(in_comp.maybe_as_string_view_on(input16.c_str()), &output,
+                      &out_comp);
       output.Complete();
 
       EXPECT_EQ(ref_case.expected_component.begin, out_comp.begin);
@@ -1744,7 +1746,8 @@
 
   std::string out_str;
   StdStringCanonOutput output(&out_str);
-  CanonicalizeRef(null_input, null_input_component, &output, &out_comp);
+  CanonicalizeRef(null_input_component.as_string_view_on(null_input), &output,
+                  &out_comp);
   output.Complete();
 
   EXPECT_EQ(1, out_comp.begin);