From b31b7e7e822c757d7984dc1c490d283c35a7e8e3 Mon Sep 17 00:00:00 2001
From: Nikhil Sonti <nikhilsv92@gmail.com>
Date: Tue, 22 Jul 2025 21:36:10 -0700
Subject: [PATCH 14/20] patch(M): embed-third-party-llm-in-side-panel

---
 chrome/app/chrome_command_ids.h               |   2 +
 chrome/app/generated_resources.grd            |   3 +
 .../browser/global_keyboard_shortcuts_mac.mm  |   2 +
 chrome/browser/ui/actions/chrome_action_id.h  |   3 +-
 chrome/browser/ui/browser_actions.cc          |  12 +
 .../browser/ui/browser_command_controller.cc  |  20 +
 .../browser/ui/toolbar/toolbar_pref_names.cc  |  11 +
 chrome/browser/ui/ui_features.cc              |   4 +
 chrome/browser/ui/ui_features.h               |   1 +
 chrome/browser/ui/views/accelerator_table.cc  |   2 +
 chrome/browser/ui/views/side_panel/BUILD.gn   |   4 +
 .../ui/views/side_panel/side_panel_entry_id.h |   1 +
 .../ui/views/side_panel/side_panel_prefs.cc   |   6 +
 .../ui/views/side_panel/side_panel_util.cc    |   7 +
 .../third_party_llm_panel_coordinator.cc      | 823 ++++++++++++++++++
 .../third_party_llm_panel_coordinator.h       | 188 ++++
 .../third_party_llm/third_party_llm_view.cc   |  11 +
 .../third_party_llm/third_party_llm_view.h    |  32 +
 .../customize_toolbar/customize_toolbar.mojom |   1 +
 .../customize_toolbar_handler.cc              |   6 +
 20 files changed, 1138 insertions(+), 1 deletion(-)
 create mode 100644 chrome/browser/ui/views/side_panel/third_party_llm/third_party_llm_panel_coordinator.cc
 create mode 100644 chrome/browser/ui/views/side_panel/third_party_llm/third_party_llm_panel_coordinator.h
 create mode 100644 chrome/browser/ui/views/side_panel/third_party_llm/third_party_llm_view.cc
 create mode 100644 chrome/browser/ui/views/side_panel/third_party_llm/third_party_llm_view.h

diff --git a/chrome/app/chrome_command_ids.h b/chrome/app/chrome_command_ids.h
index 7dbc937b376cc..91d055b22ce33 100644
--- a/chrome/app/chrome_command_ids.h
+++ b/chrome/app/chrome_command_ids.h
@@ -290,6 +290,8 @@
 #define IDC_SHOW_HISTORY_SIDE_PANEL     40293
 #define IDC_OPEN_GLIC                   40294
 #define IDC_FIND_EXTENSIONS  40295
+#define IDC_SHOW_THIRD_PARTY_LLM_SIDE_PANEL  40296
+#define IDC_CYCLE_THIRD_PARTY_LLM_PROVIDER  40297
 
 // Spell-check
 // Insert any additional suggestions before _LAST; these have to be consecutive.
diff --git a/chrome/app/generated_resources.grd b/chrome/app/generated_resources.grd
index a7e94095c4dc5..ea11ecc8060dd 100644
--- a/chrome/app/generated_resources.grd
+++ b/chrome/app/generated_resources.grd
@@ -8840,6 +8840,9 @@ Keep your key file in a safe place. You will need it to create new versions of y
           Reading list
         </message>
       </if>
+      <message name="IDS_THIRD_PARTY_LLM_TITLE" desc="Title for 3rd party LLM side panel">
+        LLM Chat
+      </message>
       <message name="IDS_READ_LATER_MENU_UNREAD_HEADER" desc="Header for section of unread Read later items.">
         Unread
       </message>
diff --git a/chrome/browser/global_keyboard_shortcuts_mac.mm b/chrome/browser/global_keyboard_shortcuts_mac.mm
index cbc0d472d9476..262d771e6b568 100644
--- a/chrome/browser/global_keyboard_shortcuts_mac.mm
+++ b/chrome/browser/global_keyboard_shortcuts_mac.mm
@@ -145,6 +145,8 @@ const std::vector<KeyboardShortcutData>& GetShortcutsNotPresentInMainMenu() {
 
       {true,  true,  false, false, kVK_ANSI_M,            IDC_SHOW_AVATAR_MENU},
       {true,  false, false, true,  kVK_ANSI_L,            IDC_SHOW_DOWNLOADS},
+      {true,  true,  false, false, kVK_ANSI_L,            IDC_SHOW_THIRD_PARTY_LLM_SIDE_PANEL},
+      {true,  true,  false, false, kVK_ANSI_Semicolon,   IDC_CYCLE_THIRD_PARTY_LLM_PROVIDER},
       {true,  true,  false, false, kVK_ANSI_C,            IDC_DEV_TOOLS_INSPECT},
       {true,  false, false, true,  kVK_ANSI_C,            IDC_DEV_TOOLS_INSPECT},
       {true,  false, false, true,  kVK_DownArrow,         IDC_FOCUS_NEXT_PANE},
diff --git a/chrome/browser/ui/actions/chrome_action_id.h b/chrome/browser/ui/actions/chrome_action_id.h
index b79f667f412a8..15585a7886bdb 100644
--- a/chrome/browser/ui/actions/chrome_action_id.h
+++ b/chrome/browser/ui/actions/chrome_action_id.h
@@ -539,7 +539,8 @@
   E(kActionSidePanelShowShoppingInsights) \
   E(kActionSidePanelShowSideSearch) \
   E(kActionSidePanelShowUserNote) \
-  E(kActionSidePanelShowMerchantTrust)
+  E(kActionSidePanelShowMerchantTrust) \
+  E(kActionSidePanelShowThirdPartyLlm)
 
 #define TOOLBAR_PINNABLE_ACTION_IDS \
   E(kActionHome, IDC_HOME) \
diff --git a/chrome/browser/ui/browser_actions.cc b/chrome/browser/ui/browser_actions.cc
index 38faf665d9d87..b08b1f61ce3c4 100644
--- a/chrome/browser/ui/browser_actions.cc
+++ b/chrome/browser/ui/browser_actions.cc
@@ -231,6 +231,18 @@ void BrowserActions::InitializeBrowserActions() {
             .Build());
   }
 
+  // Add third-party LLM panel if feature is enabled
+  if (base::FeatureList::IsEnabled(features::kThirdPartyLlmPanel)) {
+    root_action_item_->AddChild(
+        SidePanelAction(SidePanelEntryId::kThirdPartyLlm,
+                        IDS_THIRD_PARTY_LLM_TITLE,  // Using reading list name temporarily
+                        IDS_THIRD_PARTY_LLM_TITLE,
+                        vector_icons::kChatOrangeIcon,
+                        kActionSidePanelShowThirdPartyLlm, browser,
+                        true)
+            .Build());
+  }
+
   if (HistorySidePanelCoordinator::IsSupported()) {
     root_action_item_->AddChild(
         SidePanelAction(SidePanelEntryId::kHistory, IDS_HISTORY_TITLE,
diff --git a/chrome/browser/ui/browser_command_controller.cc b/chrome/browser/ui/browser_command_controller.cc
index 6f2feda3e7920..f0912a69e5c05 100644
--- a/chrome/browser/ui/browser_command_controller.cc
+++ b/chrome/browser/ui/browser_command_controller.cc
@@ -68,6 +68,7 @@
 #include "chrome/browser/ui/views/side_panel/side_panel_entry_id.h"
 #include "chrome/browser/ui/views/side_panel/side_panel_enums.h"
 #include "chrome/browser/ui/views/side_panel/side_panel_ui.h"
+#include "chrome/browser/ui/views/side_panel/third_party_llm/third_party_llm_panel_coordinator.h"
 #include "chrome/browser/ui/web_applications/app_browser_controller.h"
 #include "chrome/browser/ui/web_applications/web_app_dialog_utils.h"
 #include "chrome/browser/ui/web_applications/web_app_launch_utils.h"
@@ -912,6 +913,21 @@ bool BrowserCommandController::ExecuteCommandWithDisposition(
       browser_->GetFeatures().side_panel_ui()->Show(
           SidePanelEntryId::kBookmarks, SidePanelOpenTrigger::kAppMenu);
       break;
+    case IDC_SHOW_THIRD_PARTY_LLM_SIDE_PANEL:
+      if (base::FeatureList::IsEnabled(features::kThirdPartyLlmPanel)) {
+        browser_->GetFeatures().side_panel_ui()->Toggle(
+            SidePanelEntry::Key(SidePanelEntryId::kThirdPartyLlm),
+            SidePanelOpenTrigger::kAppMenu);
+      }
+      break;
+    case IDC_CYCLE_THIRD_PARTY_LLM_PROVIDER:
+      if (base::FeatureList::IsEnabled(features::kThirdPartyLlmPanel)) {
+        if (ThirdPartyLlmPanelCoordinator* coordinator = 
+            ThirdPartyLlmPanelCoordinator::FromBrowser(browser_)) {
+          coordinator->CycleProvider();
+        }
+      }
+      break;
     case IDC_SHOW_APP_MENU:
       base::RecordAction(base::UserMetricsAction("Accel_Show_App_Menu"));
       ShowAppMenu(browser_);
@@ -1550,6 +1566,10 @@ void BrowserCommandController::InitCommandState() {
   }
 
   command_updater_.UpdateCommandEnabled(IDC_SHOW_BOOKMARK_SIDE_PANEL, true);
+  command_updater_.UpdateCommandEnabled(IDC_SHOW_THIRD_PARTY_LLM_SIDE_PANEL,
+                                        base::FeatureList::IsEnabled(features::kThirdPartyLlmPanel));
+  command_updater_.UpdateCommandEnabled(IDC_CYCLE_THIRD_PARTY_LLM_PROVIDER,
+                                        base::FeatureList::IsEnabled(features::kThirdPartyLlmPanel));
 
   if (browser_->is_type_normal()) {
     // Reading list commands.
diff --git a/chrome/browser/ui/toolbar/toolbar_pref_names.cc b/chrome/browser/ui/toolbar/toolbar_pref_names.cc
index 5d3da32f57f81..7f09152198f12 100644
--- a/chrome/browser/ui/toolbar/toolbar_pref_names.cc
+++ b/chrome/browser/ui/toolbar/toolbar_pref_names.cc
@@ -4,7 +4,9 @@
 
 #include "chrome/browser/ui/toolbar/toolbar_pref_names.h"
 
+#include "base/feature_list.h"
 #include "chrome/browser/ui/actions/chrome_action_id.h"
+#include "chrome/browser/ui/ui_features.h"
 #include "chrome/common/chrome_features.h"
 #include "components/pref_registry/pref_registry_syncable.h"
 #include "components/prefs/pref_registry_simple.h"
@@ -24,6 +26,15 @@ void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry) {
     }
   }
 
+  // Add third-party LLM panel to default pinned actions
+  if (base::FeatureList::IsEnabled(features::kThirdPartyLlmPanel)) {
+    const std::optional<std::string>& third_party_llm_action =
+        actions::ActionIdMap::ActionIdToString(kActionSidePanelShowThirdPartyLlm);
+    if (third_party_llm_action.has_value()) {
+      default_pinned_actions.Append(third_party_llm_action.value());
+    }
+  }
+
   registry->RegisterListPref(prefs::kPinnedActions,
                              std::move(default_pinned_actions),
                              user_prefs::PrefRegistrySyncable::SYNCABLE_PREF);
diff --git a/chrome/browser/ui/ui_features.cc b/chrome/browser/ui/ui_features.cc
index 6b7f69f033c66..f9c6087f5388e 100644
--- a/chrome/browser/ui/ui_features.cc
+++ b/chrome/browser/ui/ui_features.cc
@@ -137,6 +137,10 @@ BASE_FEATURE(kSidePanelResizing,
              "SidePanelResizing",
              base::FEATURE_DISABLED_BY_DEFAULT);
 
+BASE_FEATURE(kThirdPartyLlmPanel,
+             "ThirdPartyLlmPanel",
+             base::FEATURE_ENABLED_BY_DEFAULT);
+
 BASE_FEATURE(kTabDuplicateMetrics,
              "TabDuplicateMetrics",
              base::FEATURE_ENABLED_BY_DEFAULT);
diff --git a/chrome/browser/ui/ui_features.h b/chrome/browser/ui/ui_features.h
index 6ebeab8624481..f4a829cc38c71 100644
--- a/chrome/browser/ui/ui_features.h
+++ b/chrome/browser/ui/ui_features.h
@@ -110,6 +110,7 @@ extern const char kTabScrollingButtonPositionParameterName[];
 
 BASE_DECLARE_FEATURE(kSidePanelResizing);
 BASE_DECLARE_FEATURE(kSidePanelSearchCompanion);
+BASE_DECLARE_FEATURE(kThirdPartyLlmPanel);
 
 BASE_DECLARE_FEATURE(kTabGroupsCollapseFreezing);
 
diff --git a/chrome/browser/ui/views/accelerator_table.cc b/chrome/browser/ui/views/accelerator_table.cc
index 6db32fe196921..c22b9bd1d77ab 100644
--- a/chrome/browser/ui/views/accelerator_table.cc
+++ b/chrome/browser/ui/views/accelerator_table.cc
@@ -151,6 +151,8 @@ const AcceleratorMapping kAcceleratorMap[] = {
     {ui::VKEY_F11, ui::EF_NONE, IDC_FULLSCREEN},
     {ui::VKEY_M, ui::EF_SHIFT_DOWN | ui::EF_PLATFORM_ACCELERATOR,
      IDC_SHOW_AVATAR_MENU},
+    {ui::VKEY_L, ui::EF_SHIFT_DOWN | ui::EF_PLATFORM_ACCELERATOR,
+     IDC_SHOW_THIRD_PARTY_LLM_SIDE_PANEL},
 
 // Platform-specific key maps.
 #if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS)
diff --git a/chrome/browser/ui/views/side_panel/BUILD.gn b/chrome/browser/ui/views/side_panel/BUILD.gn
index 2af616e71a52a..36605fd2eddbf 100644
--- a/chrome/browser/ui/views/side_panel/BUILD.gn
+++ b/chrome/browser/ui/views/side_panel/BUILD.gn
@@ -88,6 +88,10 @@ source_set("side_panel") {
     "side_panel_util.h",
     "side_panel_web_ui_view.cc",
     "side_panel_web_ui_view.h",
+    "third_party_llm/third_party_llm_panel_coordinator.cc",
+    "third_party_llm/third_party_llm_panel_coordinator.h",
+    "third_party_llm/third_party_llm_view.cc",
+    "third_party_llm/third_party_llm_view.h",
   ]
   public_deps = [
     "//base",
diff --git a/chrome/browser/ui/views/side_panel/side_panel_entry_id.h b/chrome/browser/ui/views/side_panel/side_panel_entry_id.h
index a9232eaa871f2..9e159f5ad7a6d 100644
--- a/chrome/browser/ui/views/side_panel/side_panel_entry_id.h
+++ b/chrome/browser/ui/views/side_panel/side_panel_entry_id.h
@@ -39,6 +39,7 @@
   V(kLensOverlayResults, kActionSidePanelShowLensOverlayResults,              \
     "LensOverlayResults")                                                     \
   V(kMerchantTrust, kActionSidePanelShowMerchantTrust, "MerchantTrust")       \
+  V(kThirdPartyLlm, kActionSidePanelShowThirdPartyLlm, "ThirdPartyLlm")      \
   /* Extensions (nothing more should be added below here) */                  \
   V(kExtension, std::nullopt, "Extension")
 
diff --git a/chrome/browser/ui/views/side_panel/side_panel_prefs.cc b/chrome/browser/ui/views/side_panel/side_panel_prefs.cc
index 2c6ba6c527498..f4e4296509222 100644
--- a/chrome/browser/ui/views/side_panel/side_panel_prefs.cc
+++ b/chrome/browser/ui/views/side_panel/side_panel_prefs.cc
@@ -7,6 +7,7 @@
 #include "base/feature_list.h"
 #include "base/i18n/rtl.h"
 #include "chrome/browser/ui/ui_features.h"
+#include "chrome/browser/ui/views/side_panel/third_party_llm/third_party_llm_panel_coordinator.h"
 #include "chrome/common/pref_names.h"
 #include "components/pref_registry/pref_registry_syncable.h"
 #include "components/prefs/pref_registry_simple.h"
@@ -22,6 +23,11 @@ void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry) {
                                 !base::i18n::IsRTL());
   registry->RegisterBooleanPref(prefs::kGoogleSearchSidePanelEnabled, true);
   registry->RegisterDictionaryPref(prefs::kSidePanelIdToWidth);
+  
+  // Register third-party LLM panel preferences
+  if (base::FeatureList::IsEnabled(features::kThirdPartyLlmPanel)) {
+    ThirdPartyLlmPanelCoordinator::RegisterProfilePrefs(registry);
+  }
 }
 
 }  // namespace side_panel_prefs
diff --git a/chrome/browser/ui/views/side_panel/side_panel_util.cc b/chrome/browser/ui/views/side_panel/side_panel_util.cc
index f93a373cd9e96..cc55483ad32d7 100644
--- a/chrome/browser/ui/views/side_panel/side_panel_util.cc
+++ b/chrome/browser/ui/views/side_panel/side_panel_util.cc
@@ -20,6 +20,7 @@
 #include "chrome/browser/ui/views/side_panel/history_clusters/history_clusters_side_panel_coordinator.h"
 #include "chrome/browser/ui/views/side_panel/reading_list/reading_list_side_panel_coordinator.h"
 #include "chrome/browser/ui/views/side_panel/side_panel_content_proxy.h"
+#include "chrome/browser/ui/views/side_panel/third_party_llm/third_party_llm_panel_coordinator.h"
 #include "chrome/browser/ui/views/side_panel/side_panel_coordinator.h"
 #include "chrome/browser/ui/views/side_panel/side_panel_registry.h"
 #include "chrome/browser/ui/views/side_panel/side_panel_ui.h"
@@ -54,6 +55,12 @@ void SidePanelUtil::PopulateGlobalEntries(Browser* browser,
         ->history_side_panel_coordinator()
         ->CreateAndRegisterEntry(window_registry);
   }
+
+  // Add third-party LLM panel.
+  if (base::FeatureList::IsEnabled(features::kThirdPartyLlmPanel)) {
+    ThirdPartyLlmPanelCoordinator::GetOrCreateForBrowser(browser)
+        ->CreateAndRegisterEntry(window_registry);
+  }
 }
 
 SidePanelContentProxy* SidePanelUtil::GetSidePanelContentProxy(
diff --git a/chrome/browser/ui/views/side_panel/third_party_llm/third_party_llm_panel_coordinator.cc b/chrome/browser/ui/views/side_panel/third_party_llm/third_party_llm_panel_coordinator.cc
new file mode 100644
index 0000000000000..21a1737b9bb83
--- /dev/null
+++ b/chrome/browser/ui/views/side_panel/third_party_llm/third_party_llm_panel_coordinator.cc
@@ -0,0 +1,823 @@
+// 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.
+
+#include "chrome/browser/ui/views/side_panel/third_party_llm/third_party_llm_panel_coordinator.h"
+
+#include <memory>
+#include <vector>
+
+#include "base/functional/callback.h"
+#include "chrome/browser/ui/views/side_panel/third_party_llm/third_party_llm_view.h"
+#include "base/strings/utf_string_conversions.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/ui/browser.h"
+#include "chrome/browser/ui/browser_list.h"
+#include "chrome/browser/ui/browser_window.h"
+#include "chrome/browser/ui/browser_window/public/browser_window_features.h"
+#include "chrome/browser/ui/views/chrome_layout_provider.h"
+#include "chrome/browser/ui/views/side_panel/side_panel_entry.h"
+#include "chrome/browser/ui/views/side_panel/side_panel_entry_id.h"
+#include "chrome/browser/ui/views/side_panel/side_panel_registry.h"
+#include "chrome/browser/ui/views/side_panel/side_panel_ui.h"
+#include "chrome/grit/generated_resources.h"
+#include "content/public/browser/web_contents.h"
+#include "ui/base/l10n/l10n_util.h"
+#include "ui/base/models/combobox_model.h"
+#include "ui/views/controls/button/image_button.h"
+#include "ui/views/controls/button/label_button.h"
+#include "ui/views/controls/combobox/combobox.h"
+#include "ui/views/controls/label.h"
+#include "ui/views/controls/separator.h"
+#include "ui/views/controls/webview/webview.h"
+#include "ui/views/layout/box_layout.h"
+#include "ui/views/layout/flex_layout.h"
+#include "ui/views/vector_icons.h"
+#include "components/vector_icons/vector_icons.h"
+#include "components/prefs/pref_service.h"
+#include "components/user_prefs/user_prefs.h"
+#include "components/pref_registry/pref_registry_syncable.h"
+#include "chrome/browser/ui/browser_navigator.h"
+#include "chrome/browser/ui/browser_navigator_params.h"
+#include "chrome/browser/ui/tabs/tab_strip_model.h"
+#include "content/public/browser/browser_accessibility_state.h"
+#include "ui/accessibility/ax_node.h"
+#include "ui/accessibility/ax_node_data.h"
+#include "ui/accessibility/ax_enums.mojom.h"
+#include "ui/accessibility/ax_tree_update.h"
+#include "ui/base/clipboard/clipboard.h"
+#include "ui/base/clipboard/scoped_clipboard_writer.h"
+#include "chrome/browser/ui/browser_commands.h"
+#include "chrome/browser/ui/browser_tabstrip.h"
+#include "base/timer/timer.h"
+#include "base/task/sequenced_task_runner.h"
+#include "components/input/native_web_keyboard_event.h"
+
+namespace {
+
+// Preference name for storing selected LLM provider
+const char kThirdPartyLlmProviderPref[] = "third_party_llm.selected_provider";
+
+// ComboboxModel for LLM provider selection
+class LlmProviderComboboxModel : public ui::ComboboxModel {
+ public:
+  LlmProviderComboboxModel() = default;
+  ~LlmProviderComboboxModel() override = default;
+
+  // ui::ComboboxModel:
+  size_t GetItemCount() const override { return 5; }
+
+  std::u16string GetItemAt(size_t index) const override {
+    switch (index) {
+      case 0:
+        return u"ChatGPT";
+      case 1:
+        return u"Claude";
+      case 2:
+        return u"Grok";
+      case 3:
+        return u"Gemini";
+      case 4:
+        return u"Perplexity";
+      default:
+        NOTREACHED();
+    }
+  }
+};
+
+}  // namespace
+
+ThirdPartyLlmPanelCoordinator::ThirdPartyLlmPanelCoordinator(Browser* browser)
+    : BrowserUserData<ThirdPartyLlmPanelCoordinator>(*browser),
+      feedback_timer_(std::make_unique<base::OneShotTimer>()) {
+  // Register for early cleanup notifications
+  browser_list_observation_.Observe(BrowserList::GetInstance());
+  profile_observation_.Observe(browser->profile());
+
+  // Load saved provider preference
+  PrefService* prefs = browser->profile()->GetPrefs();
+  if (prefs->HasPrefPath(kThirdPartyLlmProviderPref)) {
+    int provider_value = prefs->GetInteger(kThirdPartyLlmProviderPref);
+    if (provider_value >= 0 && provider_value <= 4) {
+      current_provider_ = static_cast<LlmProvider>(provider_value);
+    }
+  }
+}
+
+ThirdPartyLlmPanelCoordinator::~ThirdPartyLlmPanelCoordinator() {
+  // Destructor should be minimal - cleanup already done in observer methods
+  // The ScopedObservation objects will automatically unregister
+}
+
+void ThirdPartyLlmPanelCoordinator::CreateAndRegisterEntry(
+    SidePanelRegistry* global_registry) {
+  auto entry = std::make_unique<SidePanelEntry>(
+      SidePanelEntry::Id::kThirdPartyLlm,
+      base::BindRepeating(
+          &ThirdPartyLlmPanelCoordinator::CreateThirdPartyLlmWebView,
+          base::Unretained(this)));
+  
+  global_registry->Register(std::move(entry));
+}
+
+std::unique_ptr<views::View>
+ThirdPartyLlmPanelCoordinator::CreateThirdPartyLlmWebView(
+    SidePanelEntryScope& scope) {
+  // Cancel any pending timer callbacks before resetting UI pointers
+  if (feedback_timer_ && feedback_timer_->IsRunning()) {
+    feedback_timer_->Stop();
+  }
+
+  // Reset UI pointers when creating new view
+  web_view_ = nullptr;
+  provider_selector_ = nullptr;
+  copy_feedback_label_ = nullptr;
+
+  // Stop observing any previous views to prevent dangling observations.
+  view_observation_.RemoveAllObservations();
+
+  // Create the main container using our custom view that handles cleanup
+  auto container = std::make_unique<ThirdPartyLlmView>();
+  auto* container_layout = container->SetLayoutManager(std::make_unique<views::FlexLayout>());
+  container_layout->SetOrientation(views::LayoutOrientation::kVertical)
+      .SetMainAxisAlignment(views::LayoutAlignment::kStart)
+      .SetCrossAxisAlignment(views::LayoutAlignment::kStretch);
+  
+  // Create header container with vertical layout for dropdown and feedback
+  auto* header_container = container->AddChildView(std::make_unique<views::View>());
+  header_container->SetLayoutManager(std::make_unique<views::BoxLayout>(
+      views::BoxLayout::Orientation::kVertical, 
+      gfx::Insets::TLBR(8, 12, 4, 12), 
+      4));
+  
+  // Create header row with dropdown and buttons
+  auto* header = header_container->AddChildView(std::make_unique<views::View>());
+  header->SetLayoutManager(std::make_unique<views::BoxLayout>(
+      views::BoxLayout::Orientation::kHorizontal, 
+      gfx::Insets(), 
+      12));  // Increased spacing between elements
+  
+  // Add dropdown
+  auto provider_model = std::make_unique<LlmProviderComboboxModel>();
+  provider_selector_ = header->AddChildView(
+      std::make_unique<views::Combobox>(std::move(provider_model)));
+  provider_selector_->SetSelectedIndex(static_cast<size_t>(current_provider_));
+  provider_selector_->SetCallback(base::BindRepeating(
+      &ThirdPartyLlmPanelCoordinator::OnProviderChanged,
+      weak_factory_.GetWeakPtr()));
+  provider_selector_->SetAccessibleName(u"LLM Provider Selection");
+
+  // Add feedback label below dropdown (initially hidden)
+  copy_feedback_label_ = header_container->AddChildView(
+      std::make_unique<views::Label>(u""));
+  copy_feedback_label_->SetVisible(false);
+  copy_feedback_label_->SetEnabledColor(ui::kColorLabelForegroundSecondary);
+  copy_feedback_label_->SetFontList(
+      copy_feedback_label_->font_list().DeriveWithSizeDelta(-1));
+  
+  // Observe UI elements so we can reset pointers when they are destroyed.
+  view_observation_.AddObservation(copy_feedback_label_);
+  view_observation_.AddObservation(provider_selector_);
+  
+  // Add flexible spacer
+  views::BoxLayout* box_layout = static_cast<views::BoxLayout*>(header->GetLayoutManager());
+  box_layout->SetFlexForView(header->AddChildView(std::make_unique<views::View>()), 1);
+  
+  // Add copy content button
+  auto* copy_button = header->AddChildView(
+      std::make_unique<views::ImageButton>(base::BindRepeating(
+          &ThirdPartyLlmPanelCoordinator::OnCopyContent,
+          weak_factory_.GetWeakPtr())));
+  copy_button->SetImageModel(
+      views::Button::STATE_NORMAL,
+      ui::ImageModel::FromVectorIcon(vector_icons::kContentCopyIcon, ui::kColorIcon, 20));
+  copy_button->SetAccessibleName(u"Copy page content");
+  copy_button->SetTooltipText(u"Copy main page content to clipboard");
+  copy_button->SetPreferredSize(gfx::Size(32, 32));
+  copy_button->SetImageHorizontalAlignment(views::ImageButton::ALIGN_CENTER);
+  copy_button->SetImageVerticalAlignment(views::ImageButton::ALIGN_MIDDLE);
+  
+  // Add open in new tab button
+  auto* open_button = header->AddChildView(
+      std::make_unique<views::ImageButton>(base::BindRepeating(
+          &ThirdPartyLlmPanelCoordinator::OnOpenInNewTab,
+          weak_factory_.GetWeakPtr())));
+  open_button->SetImageModel(
+      views::Button::STATE_NORMAL,
+      ui::ImageModel::FromVectorIcon(vector_icons::kLaunchIcon, ui::kColorIcon, 20));
+  open_button->SetAccessibleName(u"Open in new tab");
+  open_button->SetTooltipText(u"Open in new tab");
+  open_button->SetPreferredSize(gfx::Size(32, 32));
+  open_button->SetImageHorizontalAlignment(views::ImageButton::ALIGN_CENTER);
+  open_button->SetImageVerticalAlignment(views::ImageButton::ALIGN_MIDDLE);
+  
+  // Add separator
+  container->AddChildView(std::make_unique<views::Separator>());
+  
+  // Create WebView
+  web_view_ = container->AddChildView(
+      std::make_unique<views::WebView>(GetBrowser().profile()));
+  web_view_->SetProperty(
+      views::kFlexBehaviorKey,
+      views::FlexSpecification(views::MinimumFlexSizeRule::kScaleToZero,
+                               views::MaximumFlexSizeRule::kUnbounded));
+  
+  // Observe UI elements so we can reset pointers when they are destroyed.
+  view_observation_.AddObservation(web_view_);
+  
+  // Create WebContents if we don't have one yet
+  if (!owned_web_contents_) {
+    content::WebContents::CreateParams params(GetBrowser().profile());
+    owned_web_contents_ = content::WebContents::Create(params);
+
+    // Set this as the delegate to handle keyboard events
+    owned_web_contents_->SetDelegate(this);
+  }
+
+  // Navigate to initial provider (use last URL if available)
+  GURL provider_url;
+  auto it = last_urls_.find(current_provider_);
+  if (it != last_urls_.end() && it->second.is_valid()) {
+    provider_url = it->second;
+  } else {
+    provider_url = GetProviderUrl(current_provider_);
+  }
+  owned_web_contents_->GetController().LoadURL(
+      provider_url, 
+      content::Referrer(),
+      ui::PAGE_TRANSITION_AUTO_TOPLEVEL,
+      std::string());
+
+  // Set the WebContents in the WebView (WebView does NOT take ownership)
+  // We pass the raw pointer but retain ownership via owned_web_contents_
+  web_view_->SetWebContents(owned_web_contents_.get());
+  web_view_->SetVisible(true);
+  
+  // Tell our custom container about the WebView for proper cleanup
+  container->SetWebView(web_view_);
+
+  // Observe the WebContents
+  Observe(owned_web_contents_.get());
+
+  // Enable focus for the WebView to handle keyboard events properly
+  web_view_->SetFocusBehavior(views::View::FocusBehavior::ALWAYS);
+
+  // Allow accelerators (keyboard shortcuts) to be processed
+  web_view_->set_allow_accelerators(true);
+
+  // Add separator before footer
+  container->AddChildView(std::make_unique<views::Separator>());
+
+  // Create footer with keyboard shortcuts
+  auto* footer = container->AddChildView(std::make_unique<views::View>());
+  footer->SetLayoutManager(std::make_unique<views::BoxLayout>(
+      views::BoxLayout::Orientation::kHorizontal,
+      gfx::Insets::TLBR(6, 12, 6, 12),
+      8));
+  
+  // Add keyboard icon
+  auto* keyboard_icon = footer->AddChildView(std::make_unique<views::Label>(u"⌨️"));
+  keyboard_icon->SetEnabledColor(ui::kColorLabelForegroundSecondary);
+  
+  // Add shortcut text
+  auto* shortcuts_label = footer->AddChildView(
+      std::make_unique<views::Label>(u"Toggle: ⌘⇧L  •  Switch: ⌘⇧;"));
+  shortcuts_label->SetEnabledColor(ui::kColorLabelForegroundSecondary);
+  shortcuts_label->SetFontList(
+      shortcuts_label->font_list().DeriveWithSizeDelta(-1));
+  
+  return container;
+}
+
+void ThirdPartyLlmPanelCoordinator::OnProviderChanged() {
+  if (!provider_selector_)
+    return;
+
+  auto selected_index = provider_selector_->GetSelectedIndex();
+  if (!selected_index || selected_index.value() > 4)
+    return;
+
+  DoProviderChange(static_cast<LlmProvider>(selected_index.value()));
+}
+
+void ThirdPartyLlmPanelCoordinator::DoProviderChange(LlmProvider new_provider) {
+  // Prevent re-entrancy and overlapping updates.
+  if (provider_change_in_progress_ || new_provider == current_provider_)
+    return;
+
+  provider_change_in_progress_ = true;
+
+  if (owned_web_contents_) {
+    GURL current_url = owned_web_contents_->GetURL();
+    if (current_url.is_valid()) {
+      last_urls_[current_provider_] = current_url;
+    }
+  }
+
+  current_provider_ = new_provider;
+
+  // Persist preference.
+  if (PrefService* prefs = GetBrowser().profile()->GetPrefs()) {
+    prefs->SetInteger(kThirdPartyLlmProviderPref, static_cast<int>(current_provider_));
+  }
+
+  // Determine URL to load.
+  GURL provider_url;
+  auto it = last_urls_.find(current_provider_);
+  provider_url = (it != last_urls_.end() && it->second.is_valid()) ? it->second
+                                                                    : GetProviderUrl(current_provider_);
+
+  if (owned_web_contents_) {
+    owned_web_contents_->GetController().LoadURL(
+        provider_url, content::Referrer(), ui::PAGE_TRANSITION_AUTO_TOPLEVEL, std::string());
+  }
+
+  provider_change_in_progress_ = false;
+}
+
+GURL ThirdPartyLlmPanelCoordinator::GetProviderUrl(LlmProvider provider) const {
+  switch (provider) {
+    case LlmProvider::kChatGPT:
+      return GURL("https://chatgpt.com");
+    case LlmProvider::kClaude:
+      return GURL("https://claude.ai");
+    case LlmProvider::kGrok:
+      return GURL("https://grok.com");
+    case LlmProvider::kGemini:
+      return GURL("https://gemini.google.com");
+    case LlmProvider::kPerplexity:
+      return GURL("https://www.perplexity.ai");
+  }
+}
+
+std::u16string ThirdPartyLlmPanelCoordinator::GetProviderName(LlmProvider provider) const {
+  switch (provider) {
+    case LlmProvider::kChatGPT:
+      return u"ChatGPT";
+    case LlmProvider::kClaude:
+      return u"Claude";
+    case LlmProvider::kGrok:
+      return u"Grok";
+    case LlmProvider::kGemini:
+      return u"Gemini";
+    case LlmProvider::kPerplexity:
+      return u"Perplexity";
+  }
+}
+
+void ThirdPartyLlmPanelCoordinator::OnOpenInNewTab() {
+  if (!owned_web_contents_) {
+    return;
+  }
+  
+  GURL current_url = owned_web_contents_->GetURL();
+  if (!current_url.is_valid()) {
+    return;
+  }
+  
+  // Open the current URL in a new tab
+  NavigateParams params(&GetBrowser(), current_url, ui::PAGE_TRANSITION_LINK);
+  params.disposition = WindowOpenDisposition::NEW_FOREGROUND_TAB;
+  Navigate(&params);
+}
+
+void ThirdPartyLlmPanelCoordinator::OnCopyContent() {
+  // Get the active tab's web contents
+  TabStripModel* tab_strip_model = GetBrowser().tab_strip_model();
+  if (!tab_strip_model) {
+    return;
+  }
+
+  content::WebContents* active_contents = tab_strip_model->GetActiveWebContents();
+  if (!active_contents) {
+    return;
+  }
+  
+  // Store the title and URL for later use
+  page_title_ = active_contents->GetTitle();
+  page_url_ = active_contents->GetVisibleURL();
+  
+  // Request accessibility tree snapshot
+  active_contents->RequestAXTreeSnapshot(
+      base::BindOnce(&ThirdPartyLlmPanelCoordinator::OnAccessibilityTreeReceived,
+                     weak_factory_.GetWeakPtr()),
+      ui::AXMode::kWebContents,  // Request web contents mode
+      0,  // max_nodes (0 = no limit)
+      base::Seconds(5),  // timeout
+      content::WebContents::AXTreeSnapshotPolicy::kSameOriginDirectDescendants);
+}
+
+void ThirdPartyLlmPanelCoordinator::OnAccessibilityTreeReceived(
+    ui::AXTreeUpdate& update) {
+  // Build a map of node IDs to node data for easy lookup
+  std::map<ui::AXNodeID, const ui::AXNodeData*> node_map;
+  for (const auto& node_data : update.nodes) {
+    node_map[node_data.id] = &node_data;
+  }
+  
+  // Find the root node
+  ui::AXNodeID root_id = update.root_id;
+  if (node_map.find(root_id) == node_map.end()) {
+    LOG(ERROR) << "Root node not found in tree update";
+    return;
+  }
+  
+  // Extract text from the accessibility tree recursively
+  std::u16string extracted_text;
+  ExtractTextFromNodeData(node_map[root_id], node_map, &extracted_text);
+  
+  // Clean up text - remove excessive whitespace
+  if (!extracted_text.empty()) {
+    // Simple cleanup of multiple spaces
+    size_t pos = 0;
+    while ((pos = extracted_text.find(u"  ", pos)) != std::u16string::npos) {
+      extracted_text.replace(pos, 2, u" ");
+    }
+    
+    // Format the final output
+    std::u16string formatted_output = u"----------- WEB PAGE -----------\n\n";
+    formatted_output += u"TITLE: " + page_title_ + u"\n\n";
+    formatted_output += u"URL: " + base::UTF8ToUTF16(page_url_.spec()) + u"\n\n";
+    formatted_output += u"CONTENT:\n\n" + extracted_text;
+    formatted_output += u" ------------------------------------\n\n";
+    formatted_output += u"USER PROMPT:\n\n";
+    
+    // Copy to clipboard
+    ui::ScopedClipboardWriter clipboard_writer(ui::ClipboardBuffer::kCopyPaste);
+    clipboard_writer.WriteText(formatted_output);
+    
+    // Show feedback message
+    if (copy_feedback_label_) {
+      copy_feedback_label_->SetText(u"Content copied to clipboard");
+      copy_feedback_label_->SetVisible(true);
+
+      // Cancel any existing timer
+      if (feedback_timer_->IsRunning()) {
+        feedback_timer_->Stop();
+      }
+
+      // Start timer to hide message after 2.5 seconds
+      feedback_timer_->Start(FROM_HERE, base::Seconds(2.5),
+          base::BindOnce(&ThirdPartyLlmPanelCoordinator::HideFeedbackLabel,
+                         weak_factory_.GetWeakPtr()));
+    }
+  }
+}
+
+void ThirdPartyLlmPanelCoordinator::HideFeedbackLabel() {
+  // The timer may fire after the UI element has been destroyed (e.g. the side
+  // panel was closed). Guard against use-after-free by checking that the raw
+  // pointer is still valid (we clear it in OnViewIsDeleting).
+  if (!copy_feedback_label_)
+    return;
+
+  if (copy_feedback_label_->GetWidget()) {
+    copy_feedback_label_->SetVisible(false);
+  }
+}
+
+void ThirdPartyLlmPanelCoordinator::OnViewIsDeleting(views::View* observed_view) {
+  // Stop any pending timer that could reference destroyed elements.
+  if (observed_view == copy_feedback_label_) {
+    if (feedback_timer_ && feedback_timer_->IsRunning()) {
+      feedback_timer_->Stop();
+    }
+    copy_feedback_label_ = nullptr;
+  }
+
+  if (observed_view == provider_selector_) {
+    provider_selector_ = nullptr;
+  }
+
+  if (observed_view == web_view_) {
+    // Just clear our pointer. DO NOT call methods on the view being destroyed!
+    web_view_ = nullptr;
+  }
+
+  // Remove observation for this view.
+  view_observation_.RemoveObservation(observed_view);
+}
+
+
+void ThirdPartyLlmPanelCoordinator::ExtractTextFromNodeData(
+    const ui::AXNodeData* node,
+    const std::map<ui::AXNodeID, const ui::AXNodeData*>& node_map,
+    std::u16string* output) {
+  if (!node || !output) {
+    return;
+  }
+  
+  // Skip UI elements and navigation
+  if (node->role == ax::mojom::Role::kButton ||
+      node->role == ax::mojom::Role::kNavigation ||
+      node->role == ax::mojom::Role::kBanner ||
+      node->role == ax::mojom::Role::kComplementary ||
+      node->role == ax::mojom::Role::kContentInfo ||
+      node->role == ax::mojom::Role::kForm ||
+      node->role == ax::mojom::Role::kSearch ||
+      node->role == ax::mojom::Role::kMenu ||
+      node->role == ax::mojom::Role::kMenuBar ||
+      node->role == ax::mojom::Role::kMenuItem ||
+      node->role == ax::mojom::Role::kToolbar) {
+    // For these elements, still traverse children but don't extract their text
+    for (ui::AXNodeID child_id : node->child_ids) {
+      auto it = node_map.find(child_id);
+      if (it != node_map.end()) {
+        ExtractTextFromNodeData(it->second, node_map, output);
+      }
+    }
+    return;
+  }
+  
+  // Check if this is a text-containing element
+  bool is_text_element = (node->role == ax::mojom::Role::kStaticText ||
+                         node->role == ax::mojom::Role::kInlineTextBox);
+  
+  // Extract text if this is a text element
+  if (is_text_element) {
+    std::u16string text;
+    if (node->HasStringAttribute(ax::mojom::StringAttribute::kName)) {
+      text = node->GetString16Attribute(ax::mojom::StringAttribute::kName);
+    }
+    
+    if (text.empty() && node->HasStringAttribute(ax::mojom::StringAttribute::kValue)) {
+      text = node->GetString16Attribute(ax::mojom::StringAttribute::kValue);
+    }
+    
+    if (!text.empty()) {
+      // Add appropriate spacing
+      if (!output->empty() && output->back() != ' ' && output->back() != '\n') {
+        *output += u" ";
+      }
+      *output += text;
+    }
+  }
+  
+  // Handle line breaks
+  if (node->role == ax::mojom::Role::kLineBreak) {
+    *output += u"\n";
+  }
+  
+  // Add paragraph breaks for block-level elements
+  bool needs_paragraph_break = (node->role == ax::mojom::Role::kParagraph ||
+                               node->role == ax::mojom::Role::kHeading ||
+                               node->role == ax::mojom::Role::kListItem ||
+                               node->role == ax::mojom::Role::kBlockquote ||
+                               node->role == ax::mojom::Role::kArticle ||
+                               node->role == ax::mojom::Role::kSection);
+  
+  if (needs_paragraph_break && !output->empty() && output->back() != '\n') {
+    *output += u"\n\n";
+  }
+  
+  // Recursively process children for all elements
+  for (ui::AXNodeID child_id : node->child_ids) {
+    auto it = node_map.find(child_id);
+    if (it != node_map.end()) {
+      ExtractTextFromNodeData(it->second, node_map, output);
+    }
+  }
+  
+  // Add paragraph break after block-level elements if they had content
+  if (needs_paragraph_break && !output->empty() && output->back() != '\n') {
+    *output += u"\n\n";
+  }
+}
+
+bool ThirdPartyLlmPanelCoordinator::HandleKeyboardEvent(
+    content::WebContents* source,
+    const input::NativeWebKeyboardEvent& event) {
+  // Get the focused view - should be our WebView
+  if (!web_view_ || !web_view_->GetWidget())
+    return false;
+    
+  // Use the unhandled keyboard event handler to process the event
+  return unhandled_keyboard_event_handler_.HandleKeyboardEvent(
+      event, web_view_->GetFocusManager());
+}
+
+content::WebContents* ThirdPartyLlmPanelCoordinator::AddNewContents(
+    content::WebContents* source,
+    std::unique_ptr<content::WebContents> new_contents,
+    const GURL& target_url,
+    WindowOpenDisposition disposition,
+    const blink::mojom::WindowFeatures& window_features,
+    bool user_gesture,
+    bool* was_blocked) {
+  // Handle popup windows from the webview
+  Browser* browser = &GetBrowser();
+  
+  // Only allow popups triggered by user gesture
+  if (!user_gesture) {
+    if (was_blocked) {
+      *was_blocked = true;
+    }
+    return nullptr;
+  }
+  
+  // For popup windows and new tabs, open them in the main browser
+  if (disposition == WindowOpenDisposition::NEW_POPUP ||
+      disposition == WindowOpenDisposition::NEW_FOREGROUND_TAB ||
+      disposition == WindowOpenDisposition::NEW_BACKGROUND_TAB ||
+      disposition == WindowOpenDisposition::NEW_WINDOW) {
+    chrome::AddWebContents(browser, source, std::move(new_contents), 
+                          target_url, disposition, window_features);
+  }
+  
+  return nullptr;
+}
+
+void ThirdPartyLlmPanelCoordinator::CycleProvider() {
+  // If a provider change is already in flight, ignore additional toggle
+  // requests to prevent state races that could desynchronize the combobox and
+  // WebView.
+  if (provider_change_in_progress_)
+    return;
+
+  // Check if the third-party LLM panel is open
+  auto* side_panel_ui = GetBrowser().GetFeatures().side_panel_ui();
+  if (!side_panel_ui || 
+      !side_panel_ui->IsSidePanelShowing() ||
+      side_panel_ui->GetCurrentEntryId() != SidePanelEntry::Id::kThirdPartyLlm) {
+    return;
+  }
+
+  // Calculate next provider (cycle through 0-4)
+  int next_provider = (static_cast<int>(current_provider_) + 1) % 5;
+  LlmProvider new_provider = static_cast<LlmProvider>(next_provider);
+  
+  // Update the provider selector if it exists
+  if (provider_selector_) {
+    // Combobox selection changes made programmatically do NOT invoke the
+    // `SetCallback` observer, so we must call `OnProviderChanged()` manually
+    // to keep the page in sync with the visible provider label.
+    provider_selector_->SetSelectedIndex(next_provider);
+    OnProviderChanged();
+    return;
+  } else {
+    // If the UI isn't created yet, update everything manually
+    current_provider_ = new_provider;
+
+    // Save preference
+    PrefService* prefs = GetBrowser().profile()->GetPrefs();
+    if (prefs) {
+      prefs->SetInteger(kThirdPartyLlmProviderPref, next_provider);
+    }
+
+    // Navigate to the new provider URL if we have WebContents
+    if (owned_web_contents_) {
+      GURL provider_url = GetProviderUrl(current_provider_);
+      owned_web_contents_->GetController().LoadURL(
+          provider_url,
+          content::Referrer(),
+          ui::PAGE_TRANSITION_AUTO_TOPLEVEL,
+          std::string());
+    }
+  }
+  
+  // Removed provider change notification to prevent crash
+}
+
+void ThirdPartyLlmPanelCoordinator::DidFinishLoad(
+    content::RenderFrameHost* render_frame_host,
+    const GURL& validated_url) {
+  // Focus the input field when the page finishes loading
+  // Use a delayed task to ensure the page is fully ready
+  if (render_frame_host && render_frame_host->IsInPrimaryMainFrame()) {
+    base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
+        FROM_HERE,
+        base::BindOnce(&ThirdPartyLlmPanelCoordinator::FocusInputField,
+                       weak_factory_.GetWeakPtr()),
+        base::Seconds(1));
+  }
+}
+
+void ThirdPartyLlmPanelCoordinator::FocusInputField() {
+  if (!owned_web_contents_) {
+    return;
+  }
+  
+  // Get the main frame
+  content::RenderFrameHost* main_frame = 
+      owned_web_contents_->GetPrimaryMainFrame();
+  if (!main_frame || !main_frame->IsRenderFrameLive()) {
+    return;
+  }
+  
+  // JavaScript to focus the input field for each provider
+  std::string focus_script;
+  switch (current_provider_) {
+    case LlmProvider::kChatGPT:
+      // ChatGPT uses a textarea with id "prompt-textarea"
+      focus_script = R"(
+        setTimeout(() => {
+          const input = document.querySelector('#prompt-textarea');
+          if (input) {
+            input.focus();
+            input.click();
+          }
+        }, 500);
+      )";
+      break;
+      
+    case LlmProvider::kClaude:
+      // Claude uses a div with contenteditable
+      focus_script = R"(
+        setTimeout(() => {
+          const input = document.querySelector('div[contenteditable="true"]');
+          if (input) {
+            input.focus();
+            input.click();
+          }
+        }, 500);
+      )";
+      break;
+      
+    case LlmProvider::kGrok:
+      // Grok uses a textarea or input field
+      focus_script = R"(
+        setTimeout(() => {
+          const input = document.querySelector('textarea, input[type="text"]');
+          if (input) {
+            input.focus();
+            input.click();
+          }
+        }, 500);
+      )";
+      break;
+      
+    case LlmProvider::kGemini:
+      // Gemini uses a rich text editor
+      focus_script = R"(
+        setTimeout(() => {
+          const input = document.querySelector('.ql-editor, textarea, input[type="text"]');
+          if (input) {
+            input.focus();
+            input.click();
+          }
+        }, 500);
+      )";
+      break;
+      
+    case LlmProvider::kPerplexity:
+      // Perplexity uses a textarea
+      focus_script = R"(
+        setTimeout(() => {
+          const input = document.querySelector('textarea');
+          if (input) {
+            input.focus();
+            input.click();
+          }
+        }, 500);
+      )";
+      break;
+  }
+  
+  // Execute the JavaScript
+  if (!focus_script.empty()) {
+    main_frame->ExecuteJavaScriptForTests(
+        base::UTF8ToUTF16(focus_script),
+        base::NullCallback(),
+        /* has_user_gesture= */ true);
+  }
+}
+
+void ThirdPartyLlmPanelCoordinator::CleanupWebContents() {
+  // Cancel any pending timer callbacks first
+  if (feedback_timer_ && feedback_timer_->IsRunning()) {
+    feedback_timer_->Stop();
+  }
+
+  // Clear the WebView's association with WebContents
+  if (web_view_ && web_view_->web_contents()) {
+    web_view_->SetWebContents(nullptr);
+  }
+
+  // Destroy the WebContents we own
+  owned_web_contents_.reset();
+
+  // Stop observing
+  Observe(nullptr);
+}
+
+void ThirdPartyLlmPanelCoordinator::OnBrowserRemoved(Browser* browser) {
+  if (browser == &GetBrowser()) {
+    // Browser is being removed - clean up WebContents early
+    CleanupWebContents();
+  }
+}
+
+void ThirdPartyLlmPanelCoordinator::OnProfileWillBeDestroyed(Profile* profile) {
+  if (profile == GetBrowser().profile()) {
+    // Profile is being destroyed - clean up WebContents if not already done
+    CleanupWebContents();
+  }
+}
+
+// static
+void ThirdPartyLlmPanelCoordinator::RegisterProfilePrefs(
+    user_prefs::PrefRegistrySyncable* registry) {
+  registry->RegisterIntegerPref(kThirdPartyLlmProviderPref, 0);  // Default to ChatGPT
+}
+
+BROWSER_USER_DATA_KEY_IMPL(ThirdPartyLlmPanelCoordinator);
\ No newline at end of file
diff --git a/chrome/browser/ui/views/side_panel/third_party_llm/third_party_llm_panel_coordinator.h b/chrome/browser/ui/views/side_panel/third_party_llm/third_party_llm_panel_coordinator.h
new file mode 100644
index 0000000000000..40a67af155c06
--- /dev/null
+++ b/chrome/browser/ui/views/side_panel/third_party_llm/third_party_llm_panel_coordinator.h
@@ -0,0 +1,188 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_VIEWS_SIDE_PANEL_THIRD_PARTY_LLM_THIRD_PARTY_LLM_PANEL_COORDINATOR_H_
+#define CHROME_BROWSER_UI_VIEWS_SIDE_PANEL_THIRD_PARTY_LLM_THIRD_PARTY_LLM_PANEL_COORDINATOR_H_
+
+#include <map>
+#include <string>
+
+#include "base/memory/weak_ptr.h"
+#include "base/supports_user_data.h"
+#include "base/scoped_multi_source_observation.h"
+#include "base/scoped_observation.h"
+#include "chrome/browser/ui/browser_list_observer.h"
+#include "chrome/browser/ui/browser_user_data.h"
+#include "chrome/browser/profiles/profile_observer.h"
+#include "components/prefs/pref_change_registrar.h"
+#include "content/public/browser/web_contents_delegate.h"
+#include "content/public/browser/web_contents_observer.h"
+#include "third_party/blink/public/mojom/window_features/window_features.mojom.h"
+#include "ui/accessibility/ax_node_id_forward.h"
+#include "ui/base/window_open_disposition.h"
+#include "ui/views/controls/webview/unhandled_keyboard_event_handler.h"
+#include "url/gurl.h"
+#include "ui/views/view_observer.h"
+
+class Browser;
+class BrowserList;
+class Profile;
+class SidePanelEntryScope;
+class SidePanelRegistry;
+
+namespace input {
+struct NativeWebKeyboardEvent;
+}  // namespace input
+
+namespace user_prefs {
+class PrefRegistrySyncable;
+}  // namespace user_prefs
+
+namespace content {
+class WebContents;
+}  // namespace content
+
+namespace ui {
+struct AXNodeData;
+struct AXTreeUpdate;
+}  // namespace ui
+
+namespace views {
+class Combobox;
+class Label;
+class View;
+class WebView;
+}  // namespace views
+
+// ThirdPartyLlmPanelCoordinator handles the creation and registration of the
+// third-party LLM SidePanelEntry.
+class ThirdPartyLlmPanelCoordinator
+    : public BrowserUserData<ThirdPartyLlmPanelCoordinator>,
+      public BrowserListObserver,
+      public ProfileObserver,
+      public content::WebContentsDelegate,
+      public content::WebContentsObserver,
+      public views::ViewObserver {
+ public:
+  explicit ThirdPartyLlmPanelCoordinator(Browser* browser);
+  ThirdPartyLlmPanelCoordinator(const ThirdPartyLlmPanelCoordinator&) = delete;
+  ThirdPartyLlmPanelCoordinator& operator=(const ThirdPartyLlmPanelCoordinator&) = delete;
+  ~ThirdPartyLlmPanelCoordinator() override;
+
+  void CreateAndRegisterEntry(SidePanelRegistry* global_registry);
+  
+  // Registers user preferences
+  static void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry);
+  
+  // Cycles to the next LLM provider
+  void CycleProvider();
+  
+  // content::WebContentsDelegate:
+  bool HandleKeyboardEvent(content::WebContents* source,
+                          const input::NativeWebKeyboardEvent& event) override;
+  content::WebContents* AddNewContents(
+      content::WebContents* source,
+      std::unique_ptr<content::WebContents> new_contents,
+      const GURL& target_url,
+      WindowOpenDisposition disposition,
+      const blink::mojom::WindowFeatures& window_features,
+      bool user_gesture,
+      bool* was_blocked) override;
+  
+  // content::WebContentsObserver:
+  void DidFinishLoad(content::RenderFrameHost* render_frame_host,
+                     const GURL& validated_url) override;
+
+  // views::ViewObserver:
+  void OnViewIsDeleting(views::View* observed_view) override;
+
+  // BrowserListObserver:
+  void OnBrowserRemoved(Browser* browser) override;
+
+  // ProfileObserver:
+  void OnProfileWillBeDestroyed(Profile* profile) override;
+
+ private:
+  friend class BrowserUserData<ThirdPartyLlmPanelCoordinator>;
+  
+  BROWSER_USER_DATA_KEY_DECL();
+
+  enum class LlmProvider {
+    kChatGPT = 0,
+    kClaude = 1,
+    kGrok = 2,
+    kGemini = 3,
+    kPerplexity = 4,
+  };
+
+  std::unique_ptr<views::View> CreateThirdPartyLlmWebView(
+      SidePanelEntryScope& scope);
+  
+  void OnProviderChanged();
+  void OnOpenInNewTab();
+  void OnCopyContent();
+  void OnAccessibilityTreeReceived(ui::AXTreeUpdate& update);
+  void ExtractTextFromNodeData(
+      const ui::AXNodeData* node,
+      const std::map<ui::AXNodeID, const ui::AXNodeData*>& node_map,
+      std::u16string* output);
+  GURL GetProviderUrl(LlmProvider provider) const;
+  std::u16string GetProviderName(LlmProvider provider) const;
+  void FocusInputField();
+  void HideFeedbackLabel();
+
+  // Executes the actual provider switch after all sanity checks. Should only
+  // be called on the UI thread.  Uses |provider_change_in_progress_| to avoid
+  // reentrancy.
+  void DoProviderChange(LlmProvider new_provider);
+
+  // Clean up WebContents early to avoid shutdown crashes.
+  void CleanupWebContents();
+
+  // Current provider selection
+  LlmProvider current_provider_ = LlmProvider::kChatGPT;
+  
+  // UI elements
+  raw_ptr<views::WebView> web_view_ = nullptr;
+  raw_ptr<views::Combobox> provider_selector_ = nullptr;
+  raw_ptr<views::Label> copy_feedback_label_ = nullptr;
+  
+  // We need to own the WebContents because WebView doesn't take ownership
+  // when we call SetWebContents with externally created WebContents
+  std::unique_ptr<content::WebContents> owned_web_contents_;
+
+  // Store the last URL for each provider to restore state
+  std::map<LlmProvider, GURL> last_urls_;
+  
+  // Timer for auto-hiding feedback messages
+  std::unique_ptr<base::OneShotTimer> feedback_timer_;
+  
+  // Temporary storage for page info during copy
+  std::u16string page_title_;
+  GURL page_url_;
+  
+  // Handler for unhandled keyboard events
+  views::UnhandledKeyboardEventHandler unhandled_keyboard_event_handler_;
+  
+  // Reentrancy guard to prevent nested/overlapping provider changes that can
+  // leave the combobox selection and WebView out of sync.
+  bool provider_change_in_progress_ = false;
+  
+  // Observe lifetime of UI views we hold raw pointers to so that we can
+  // null-check safely after they are destroyed (e.g. when the side panel is
+  // closed). This prevents dangling pointer dereference from delayed tasks.
+  base::ScopedMultiSourceObservation<views::View, views::ViewObserver>
+      view_observation_{this};
+
+  // Observer registrations for early cleanup notifications
+  base::ScopedObservation<BrowserList, BrowserListObserver>
+      browser_list_observation_{this};
+  base::ScopedObservation<Profile, ProfileObserver>
+      profile_observation_{this};
+
+  // Weak pointer factory for callbacks
+  base::WeakPtrFactory<ThirdPartyLlmPanelCoordinator> weak_factory_{this};
+};
+
+#endif  // CHROME_BROWSER_UI_VIEWS_SIDE_PANEL_THIRD_PARTY_LLM_THIRD_PARTY_LLM_PANEL_COORDINATOR_H_
\ No newline at end of file
diff --git a/chrome/browser/ui/views/side_panel/third_party_llm/third_party_llm_view.cc b/chrome/browser/ui/views/side_panel/third_party_llm/third_party_llm_view.cc
new file mode 100644
index 0000000000000..dde4319c75b1d
--- /dev/null
+++ b/chrome/browser/ui/views/side_panel/third_party_llm/third_party_llm_view.cc
@@ -0,0 +1,11 @@
+// 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.
+
+#include "chrome/browser/ui/views/side_panel/third_party_llm/third_party_llm_view.h"
+
+#include "ui/views/controls/webview/webview.h"
+
+ThirdPartyLlmView::ThirdPartyLlmView() = default;
+
+ThirdPartyLlmView::~ThirdPartyLlmView() = default;
\ No newline at end of file
diff --git a/chrome/browser/ui/views/side_panel/third_party_llm/third_party_llm_view.h b/chrome/browser/ui/views/side_panel/third_party_llm/third_party_llm_view.h
new file mode 100644
index 0000000000000..c6f5e2a95e806
--- /dev/null
+++ b/chrome/browser/ui/views/side_panel/third_party_llm/third_party_llm_view.h
@@ -0,0 +1,32 @@
+// 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.
+
+#ifndef CHROME_BROWSER_UI_VIEWS_SIDE_PANEL_THIRD_PARTY_LLM_THIRD_PARTY_LLM_VIEW_H_
+#define CHROME_BROWSER_UI_VIEWS_SIDE_PANEL_THIRD_PARTY_LLM_THIRD_PARTY_LLM_VIEW_H_
+
+#include "base/memory/raw_ptr.h"
+#include "ui/views/view.h"
+
+namespace views {
+class WebView;
+}  // namespace views
+
+// Custom view for the Third Party LLM panel that ensures proper cleanup
+// of WebContents during browser shutdown.
+class ThirdPartyLlmView : public views::View {
+ public:
+  ThirdPartyLlmView();
+  ThirdPartyLlmView(const ThirdPartyLlmView&) = delete;
+  ThirdPartyLlmView& operator=(const ThirdPartyLlmView&) = delete;
+  ~ThirdPartyLlmView() override;
+
+  void SetWebView(views::WebView* web_view) { web_view_ = web_view; }
+
+ private:
+  // The WebView that contains our WebContents. We need to track this
+  // to ensure proper cleanup during shutdown.
+  raw_ptr<views::WebView> web_view_ = nullptr;
+};
+
+#endif  // CHROME_BROWSER_UI_VIEWS_SIDE_PANEL_THIRD_PARTY_LLM_THIRD_PARTY_LLM_VIEW_H_
\ No newline at end of file
diff --git a/chrome/browser/ui/webui/side_panel/customize_chrome/customize_toolbar/customize_toolbar.mojom b/chrome/browser/ui/webui/side_panel/customize_chrome/customize_toolbar/customize_toolbar.mojom
index 8eec2ead88a8a..278acb75a3704 100644
--- a/chrome/browser/ui/webui/side_panel/customize_chrome/customize_toolbar/customize_toolbar.mojom
+++ b/chrome/browser/ui/webui/side_panel/customize_chrome/customize_toolbar/customize_toolbar.mojom
@@ -33,6 +33,7 @@ enum ActionId {
   kCopyLink,
   kTabSearch,
   kSplitTab,
+  kShowThirdPartyLlm,
 };
 
 // Unique identifiers for categories the actions can belong to.
diff --git a/chrome/browser/ui/webui/side_panel/customize_chrome/customize_toolbar/customize_toolbar_handler.cc b/chrome/browser/ui/webui/side_panel/customize_chrome/customize_toolbar/customize_toolbar_handler.cc
index 52dddc0ddc518..a7915f812708a 100644
--- a/chrome/browser/ui/webui/side_panel/customize_chrome/customize_toolbar/customize_toolbar_handler.cc
+++ b/chrome/browser/ui/webui/side_panel/customize_chrome/customize_toolbar/customize_toolbar_handler.cc
@@ -85,6 +85,8 @@ MojoActionForChromeAction(actions::ActionId action_id) {
       return side_panel::customize_chrome::mojom::ActionId::kTabSearch;
     case kActionSplitTab:
       return side_panel::customize_chrome::mojom::ActionId::kSplitTab;
+    case kActionSidePanelShowThirdPartyLlm:
+      return side_panel::customize_chrome::mojom::ActionId::kShowThirdPartyLlm;
     default:
       return std::nullopt;
   }
@@ -143,6 +145,8 @@ std::optional<actions::ActionId> ChromeActionForMojoAction(
       return kActionTabSearch;
     case side_panel::customize_chrome::mojom::ActionId::kSplitTab:
       return kActionSplitTab;
+    case side_panel::customize_chrome::mojom::ActionId::kShowThirdPartyLlm:
+      return kActionSidePanelShowThirdPartyLlm;
     default:
       return std::nullopt;
   }
@@ -290,6 +294,8 @@ void CustomizeToolbarHandler::ListActions(ListActionsCallback callback) {
              side_panel::customize_chrome::mojom::CategoryId::kYourChrome);
   add_action(kActionSidePanelShowReadingList,
              side_panel::customize_chrome::mojom::CategoryId::kYourChrome);
+  add_action(kActionSidePanelShowThirdPartyLlm,
+             side_panel::customize_chrome::mojom::CategoryId::kYourChrome);
   add_action(kActionSidePanelShowHistoryCluster,
              side_panel::customize_chrome::mojom::CategoryId::kYourChrome);
   add_action(kActionShowDownloads,
-- 
2.49.0

