diff --git a/browser/brave_content_browser_client.cc b/browser/brave_content_browser_client.cc index a855e1ee0e45..8eb9e19fdd80 100644 --- a/browser/brave_content_browser_client.cc +++ b/browser/brave_content_browser_client.cc @@ -51,6 +51,7 @@ #include "brave/components/ai_chat/core/common/mojom/ai_chat.mojom.h" #include "brave/components/ai_chat/core/common/mojom/page_content_extractor.mojom.h" #include "brave/components/ai_chat/core/common/mojom/settings_helper.mojom.h" +#include "brave/components/ai_chat/core/common/mojom/tab_informer.mojom.h" #include "brave/components/ai_chat/core/common/mojom/untrusted_frame.mojom.h" #include "brave/components/ai_rewriter/common/buildflags/buildflags.h" #include "brave/components/body_sniffer/body_sniffer_throttle.h" @@ -624,7 +625,8 @@ void BraveContentBrowserClient::RegisterWebUIInterfaceBrokers( if (ai_chat::features::IsAIChatEnabled()) { registry.ForWebUI() .Add() - .Add(); + .Add() + .Add(); registry.ForWebUI() .Add() .Add(); diff --git a/browser/ui/BUILD.gn b/browser/ui/BUILD.gn index e9ef106a20d8..05eadeb8d551 100644 --- a/browser/ui/BUILD.gn +++ b/browser/ui/BUILD.gn @@ -65,6 +65,8 @@ source_set("ui") { public_deps = [ ":ui_public_dependencies" ] sources = [ + "ai_chat/tab_informer.cc", + "ai_chat/tab_informer.h", "brave_ui_features.cc", "brave_ui_features.h", "webui/ai_chat/ai_chat_ui.cc", diff --git a/browser/ui/ai_chat/tab_informer.cc b/browser/ui/ai_chat/tab_informer.cc new file mode 100644 index 000000000000..764754f6d366 --- /dev/null +++ b/browser/ui/ai_chat/tab_informer.cc @@ -0,0 +1,120 @@ +// Copyright (c) 2025 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "brave/browser/ui/ai_chat/tab_informer.h" + +#include +#include +#include + +#include "base/strings/utf_string_conversions.h" +#include "brave/components/ai_chat/core/common/mojom/tab_informer.mojom.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/ui/browser.h" +#include "chrome/browser/ui/browser_finder.h" +#include "chrome/browser/ui/browser_list.h" +#include "chrome/browser/ui/tabs/tab_model.h" +#include "content/public/browser/web_contents.h" +#include "mojo/public/cpp/bindings/receiver.h" + +namespace ai_chat { + +TabInformer::TabInformer(mojo::PendingReceiver receiver, + content::WebContents* owner_contents) + : owner_contents_(owner_contents), receiver_(this, std::move(receiver)) { + tracker_.Init(); +} + +TabInformer::~TabInformer() = default; + +content::WebContents* TabInformer::GetFromTab(mojom::TabPtr mojom_tab) { + const tabs::TabHandle handle = tabs::TabHandle(mojom_tab->id); + tabs::TabInterface* const tab = handle.Get(); + if (!tab) { + return nullptr; + } + return tab->GetContents(); +} + +void TabInformer::AddListener( + mojo::PendingRemote listener) { + auto id = listeners_.Add(std::move(listener)); + + NotifyListener(listeners_.Get(id), GetState()); +} + +void TabInformer::RemoveListener( + mojo::PendingRemote listener) { + listener.reset(); +} + +void TabInformer::OnTabStripModelChanged( + TabStripModel* tab_strip_model, + const TabStripModelChange& change, + const TabStripSelectionChange& selection) { + NotifyListeners(); +} + +void TabInformer::TabChangedAt(content::WebContents* contents, + int index, + TabChangeType change_type) { + NotifyListeners(); +} + +bool TabInformer::ShouldTrackBrowser(Browser* browser) { + return browser->is_type_normal() && + browser->profile() == + Profile::FromBrowserContext(owner_contents_->GetBrowserContext()); +} + +TabInformer::State TabInformer::GetState() { + State state; + + // Used to determine whether a given window is active. + auto* active = chrome::FindLastActive(); + + for (Browser* browser : *BrowserList::GetInstance()) { + if (!ShouldTrackBrowser(browser)) { + continue; + } + auto window = mojom::Window::New(); + window->is_active = browser == active; + + auto* tab_strip_model = browser->tab_strip_model(); + for (int i = 0; i < tab_strip_model->count(); ++i) { + auto* contents = tab_strip_model->GetWebContentsAt(i); + if (!contents) { + continue; + } + auto tab_state = mojom::Tab::New(); + tab_state->id = tab_strip_model->GetTabHandleAt(i).raw_value(); + tab_state->title = base::UTF16ToUTF8(contents->GetTitle()); + tab_state->url = contents->GetVisibleURL(); + window->tabs.push_back(std::move(tab_state)); + } + + state.push_back(std::move(window)); + } + return state; +} + +void TabInformer::NotifyListeners() { + if (listeners_.empty()) { + return; + } + auto state = GetState(); + for (auto& listener : listeners_) { + State clone; + std::ranges::transform(state, std::back_inserter(clone), + [](auto& window) { return window.Clone(); }); + NotifyListener(listener.get(), std::move(clone)); + } +} + +void TabInformer::NotifyListener(mojom::TabListener* listener, State state) { + listener->TabsChanged(std::move(state)); +} + +} // namespace ai_chat diff --git a/browser/ui/ai_chat/tab_informer.h b/browser/ui/ai_chat/tab_informer.h new file mode 100644 index 000000000000..1973438599b3 --- /dev/null +++ b/browser/ui/ai_chat/tab_informer.h @@ -0,0 +1,67 @@ +// Copyright (c) 2025 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_BROWSER_UI_AI_CHAT_TAB_INFORMER_H_ +#define BRAVE_BROWSER_UI_AI_CHAT_TAB_INFORMER_H_ + +#include + +#include "base/memory/raw_ptr.h" +#include "brave/components/ai_chat/core/common/mojom/tab_informer.mojom-forward.h" +#include "brave/components/ai_chat/core/common/mojom/tab_informer.mojom.h" +#include "chrome/browser/ui/browser_tab_strip_tracker.h" +#include "chrome/browser/ui/browser_tab_strip_tracker_delegate.h" +#include "chrome/browser/ui/tabs/tab_strip_model_observer.h" +#include "content/public/browser/web_contents.h" +#include "mojo/public/cpp/bindings/pending_receiver.h" +#include "mojo/public/cpp/bindings/receiver.h" +#include "mojo/public/cpp/bindings/remote_set.h" + +namespace ai_chat { + +class TabInformer : public mojom::TabInformer, + public BrowserTabStripTrackerDelegate, + public TabStripModelObserver { + public: + using State = std::vector; + // Note: |TabInformer| should not outlive |owner_contents_|. + TabInformer(mojo::PendingReceiver receiver, + content::WebContents* owner_contents); + ~TabInformer() override; + + content::WebContents* GetFromTab(mojom::TabPtr tab); + + // mojom::TabInformer + void AddListener(mojo::PendingRemote listener) override; + void RemoveListener( + mojo::PendingRemote listener) override; + + // TabStripModelObserver: + void OnTabStripModelChanged( + TabStripModel* tab_strip_model, + const TabStripModelChange& change, + const TabStripSelectionChange& selection) override; + void TabChangedAt(content::WebContents* contents, + int index, + TabChangeType change_type) override; + + // BrowserTabStripTrackerDelegate: + bool ShouldTrackBrowser(Browser* browser) override; + + private: + State GetState(); + void NotifyListeners(); + void NotifyListener(mojom::TabListener* listener, State state); + + raw_ptr owner_contents_; + + mojo::Receiver receiver_; + mojo::RemoteSet listeners_; + BrowserTabStripTracker tracker_{this, this}; +}; + +} // namespace ai_chat + +#endif // BRAVE_BROWSER_UI_AI_CHAT_TAB_INFORMER_H_ diff --git a/browser/ui/webui/ai_chat/ai_chat_ui.cc b/browser/ui/webui/ai_chat/ai_chat_ui.cc index efc256cb001f..f25cd7783b37 100644 --- a/browser/ui/webui/ai_chat/ai_chat_ui.cc +++ b/browser/ui/webui/ai_chat/ai_chat_ui.cc @@ -5,6 +5,7 @@ #include "brave/browser/ui/webui/ai_chat/ai_chat_ui.h" +#include #include #include "brave/browser/ai_chat/ai_chat_service_factory.h" @@ -162,6 +163,12 @@ void AIChatUI::BindInterface( std::move(parent_ui_frame_receiver)); } +void AIChatUI::BindInterface( + mojo::PendingReceiver tab_informer_receiver) { + tab_informer_ = std::make_unique( + std::move(tab_informer_receiver), web_ui()->GetWebContents()); +} + bool AIChatUIConfig::IsWebUIEnabled(content::BrowserContext* browser_context) { return ai_chat::IsAIChatEnabled( user_prefs::UserPrefs::Get(browser_context)) && diff --git a/browser/ui/webui/ai_chat/ai_chat_ui.h b/browser/ui/webui/ai_chat/ai_chat_ui.h index 67c26ed7403a..23fc064b3586 100644 --- a/browser/ui/webui/ai_chat/ai_chat_ui.h +++ b/browser/ui/webui/ai_chat/ai_chat_ui.h @@ -9,8 +9,10 @@ #include #include +#include "brave/browser/ui/ai_chat/tab_informer.h" #include "brave/browser/ui/webui/ai_chat/ai_chat_ui_page_handler.h" #include "brave/components/ai_chat/core/common/mojom/ai_chat.mojom.h" +#include "brave/components/ai_chat/core/common/mojom/tab_informer.mojom.h" #include "chrome/browser/ui/webui/top_chrome/top_chrome_web_ui_controller.h" #include "content/public/browser/web_ui_controller.h" #include "ui/webui/mojo_web_ui_controller.h" @@ -40,6 +42,8 @@ class AIChatUI : public ui::MojoWebUIController { void BindInterface(mojo::PendingReceiver receiver); void BindInterface(mojo::PendingReceiver parent_ui_frame_receiver); + void BindInterface( + mojo::PendingReceiver tab_informer_receiver); // Set by WebUIContentsWrapperT. TopChromeWebUIController provides default // implementation for this but we don't use it. @@ -52,6 +56,7 @@ class AIChatUI : public ui::MojoWebUIController { private: std::unique_ptr page_handler_; + std::unique_ptr tab_informer_; base::WeakPtr embedder_; raw_ptr profile_ = nullptr; diff --git a/components/ai_chat/core/common/mojom/BUILD.gn b/components/ai_chat/core/common/mojom/BUILD.gn index 646ac80617bc..20bf2a052a7c 100644 --- a/components/ai_chat/core/common/mojom/BUILD.gn +++ b/components/ai_chat/core/common/mojom/BUILD.gn @@ -17,6 +17,7 @@ mojom_component("mojom") { "ai_chat.mojom", "page_content_extractor.mojom", "settings_helper.mojom", + "tab_informer.mojom", "untrusted_frame.mojom", ] diff --git a/components/ai_chat/core/common/mojom/tab_informer.mojom b/components/ai_chat/core/common/mojom/tab_informer.mojom new file mode 100644 index 000000000000..7fec189b4b98 --- /dev/null +++ b/components/ai_chat/core/common/mojom/tab_informer.mojom @@ -0,0 +1,30 @@ +// Copyright (c) 2025 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +module ai_chat.mojom; + +import "url/mojom/url.mojom"; + +struct Window { + array tabs; + string name; + bool is_active; +}; + +struct Tab { + int32 id; + string title; + url.mojom.Url url; +}; + +interface TabListener { + // If we find we need it, we can make this more granular + TabsChanged(array windows); +}; + +interface TabInformer { + AddListener(pending_remote listener); + RemoveListener(pending_remote listener); +}; diff --git a/components/ai_chat/resources/common/mojom.ts b/components/ai_chat/resources/common/mojom.ts index 5b70af960e3a..4cce9d047be2 100644 --- a/components/ai_chat/resources/common/mojom.ts +++ b/components/ai_chat/resources/common/mojom.ts @@ -5,3 +5,4 @@ export * from 'gen/brave/components/ai_chat/core/common/mojom/ai_chat.mojom.m.js' export * from 'gen/brave/components/ai_chat/core/common/mojom/untrusted_frame.mojom.m.js' +export * from 'gen/brave/components/ai_chat/core/common/mojom/tab_informer.mojom.m.js' diff --git a/components/ai_chat/resources/page/api/index.ts b/components/ai_chat/resources/page/api/index.ts index 6e03ccab4cc4..679faa5a5369 100644 --- a/components/ai_chat/resources/page/api/index.ts +++ b/components/ai_chat/resources/page/api/index.ts @@ -3,9 +3,9 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at https://mozilla.org/MPL/2.0/. */ - import { loadTimeData } from '$web-common/loadTimeData' - import API from '../../common/api' - import * as Mojom from '../../common/mojom' +import { loadTimeData } from '$web-common/loadTimeData' +import API from '../../common/api' +import * as Mojom from '../../common/mojom' // State that is owned by this class because it is global to the UI // (loadTimeData / Service / UIHandler). @@ -19,6 +19,7 @@ export type State = Mojom.ServiceState & { isMobile: boolean isHistoryFeatureEnabled: boolean allActions: Mojom.ActionGroup[] + windows: Mojom.Window[] } export const defaultUIState: State = { @@ -34,10 +35,11 @@ export const defaultUIState: State = { isMobile: loadTimeData.getBoolean('isMobile'), isHistoryFeatureEnabled: loadTimeData.getBoolean('isHistoryEnabled'), allActions: [], + windows: [] } // Owns connections to the browser via mojom as well as global state -class PageAPI extends API { +class PageAPI extends API implements Mojom.TabListenerInterface { public service: Mojom.ServiceRemote = Mojom.Service.getRemote() @@ -53,6 +55,11 @@ class PageAPI extends API { public conversationEntriesFrameObserver: Mojom.ParentUIFrameCallbackRouter = new Mojom.ParentUIFrameCallbackRouter() + public tabInformer: Mojom.TabInformerRemote + = Mojom.TabInformer.getRemote() + + private tabListenerReceiver = new Mojom.TabListenerReceiver(this) + constructor() { super(defaultUIState) this.initialize() @@ -86,6 +93,11 @@ class PageAPI extends API { allActions }) + // If we're in standalone mode, listen for tab changes so we can show a picker. + if (isStandalone) { + this.tabInformer.addListener(this.tabListenerReceiver.$.bindNewPipeAndPassRemote()) + } + this.observer.onStateChanged.addListener((state: Mojom.ServiceState) => { this.setPartialState(state) }) @@ -116,6 +128,12 @@ class PageAPI extends API { }) } + tabsChanged(windows: Mojom.Window[]) { + this.setPartialState({ + windows + }) + } + private async getCurrentPremiumStatus() { const { status } = await this.service.getPremiumStatus() return {