Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[AI Chat]: Add TabInformer so the front end can be aware of open tabs #27187

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion browser/brave_content_browser_client.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -624,7 +625,8 @@ void BraveContentBrowserClient::RegisterWebUIInterfaceBrokers(
if (ai_chat::features::IsAIChatEnabled()) {
registry.ForWebUI<AIChatUI>()
.Add<ai_chat::mojom::AIChatUIHandler>()
.Add<ai_chat::mojom::Service>();
.Add<ai_chat::mojom::Service>()
.Add<ai_chat::mojom::TabInformer>();
registry.ForWebUI<AIChatUntrustedConversationUI>()
.Add<ai_chat::mojom::UntrustedUIHandler>()
.Add<ai_chat::mojom::UntrustedConversationHandler>();
Expand Down
1 change: 1 addition & 0 deletions browser/resources/settings/sources.gni
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ brave_settings_ts_extra_deps =
brave_settings_mojo_files = [
"$root_gen_dir/brave/components/ai_chat/core/common/mojom/settings_helper.mojom-webui.ts",
"$root_gen_dir/brave/components/ai_chat/core/common/mojom/ai_chat.mojom-webui.ts",
"$root_gen_dir/brave/components/ai_chat/core/common/mojom/tab_informer.mojom-webui.ts",
"$root_gen_dir/brave/components/ai_chat/core/common/mojom/untrusted_frame.mojom-webui.ts",
]

Expand Down
2 changes: 2 additions & 0 deletions browser/ui/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,8 @@ source_set("ui") {

if (!is_android) {
sources += [
"ai_chat/tab_informer.cc",
"ai_chat/tab_informer.h",
"bookmark/bookmark_helper.cc",
"bookmark/bookmark_helper.h",
"bookmark/bookmark_prefs_service.cc",
Expand Down
23 changes: 23 additions & 0 deletions browser/ui/ai_chat/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,26 @@ source_set("unit_tests") {
"//testing/gtest:gtest",
]
}

source_set("browser_tests") {
testonly = true

sources = []

if (!is_android) {
sources += [ "tab_informer_browsertest.cc" ]
}

defines = [ "HAS_OUT_OF_PROC_TEST_RUNNER" ]

deps = [
"//base/test:test_support",
"//brave/components/ai_chat/content/browser",
"//brave/components/ai_chat/core/browser",
"//brave/components/ai_chat/core/common",
"//brave/components/ai_chat/core/common/mojom",
"//chrome/browser/ui",
"//chrome/test:test_support",
"//testing/gtest",
]
}
125 changes: 125 additions & 0 deletions browser/ui/ai_chat/tab_informer.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// 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 <algorithm>
#include <iterator>
#include <utility>

#include "base/strings/utf_string_conversions.h"
#include "brave/components/ai_chat/core/common/constants.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/navigation_entry.h"
#include "content/public/browser/web_contents.h"
#include "mojo/public/cpp/bindings/receiver.h"

namespace ai_chat {

TabInformer::TabInformer(mojo::PendingReceiver<mojom::TabInformer> receiver,
content::WebContents* owner_contents)
: owner_contents_(owner_contents), receiver_(this, std::move(receiver)) {
tracker_.Init();
}

TabInformer::~TabInformer() = default;

content::WebContents* TabInformer::GetFromTab(const 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<mojom::TabListener> listener) {
auto id = listeners_.Add(std::move(listener));

NotifyListener(listeners_.Get(id), GetState());
}

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;
}

if (!ai_chat::kAllowedContentSchemes.contains(
contents->GetLastCommittedURL().scheme())) {
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();
tab_state->content_id =
contents->GetController().GetVisibleEntry()->GetUniqueID();
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
68 changes: 68 additions & 0 deletions browser/ui/ai_chat/tab_informer.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// 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 <string_view>
#include <vector>

#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,
fallaciousreasoning marked this conversation as resolved.
Show resolved Hide resolved
public BrowserTabStripTrackerDelegate,
public TabStripModelObserver {
public:
using State = std::vector<mojom::WindowPtr>;
// Note: |TabInformer| should not outlive |owner_contents_|.
TabInformer(mojo::PendingReceiver<mojom::TabInformer> receiver,
content::WebContents* owner_contents);
~TabInformer() override;

static content::WebContents* GetFromTab(const mojom::TabPtr& tab);

// mojom::TabInformer
void AddListener(mojo::PendingRemote<mojom::TabListener> 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:
friend class TabInformerBrowserTest;

State GetState();
void NotifyListeners();
void NotifyListener(mojom::TabListener* listener, State state);

raw_ptr<content::WebContents> owner_contents_;

mojo::Receiver<mojom::TabInformer> receiver_;
mojo::RemoteSet<mojom::TabListener> listeners_;
BrowserTabStripTracker tracker_{this, this};
};

} // namespace ai_chat

#endif // BRAVE_BROWSER_UI_AI_CHAT_TAB_INFORMER_H_
Loading
Loading