Skip to content

Commit

Permalink
[AI Chat]: Add TabInformer so the front end can be aware of open tabs
Browse files Browse the repository at this point in the history
  • Loading branch information
fallaciousreasoning committed Jan 13, 2025
1 parent 54ef40b commit f608a76
Show file tree
Hide file tree
Showing 10 changed files with 258 additions and 5 deletions.
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
2 changes: 2 additions & 0 deletions browser/ui/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
120 changes: 120 additions & 0 deletions browser/ui/ai_chat/tab_informer.cc
Original file line number Diff line number Diff line change
@@ -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 <algorithm>
#include <iterator>
#include <utility>

#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<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(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::RemoveListener(
mojo::PendingRemote<mojom::TabListener> 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
67 changes: 67 additions & 0 deletions browser/ui/ai_chat/tab_informer.h
Original file line number Diff line number Diff line change
@@ -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 <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,
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;

content::WebContents* GetFromTab(mojom::TabPtr tab);

// mojom::TabInformer
void AddListener(mojo::PendingRemote<mojom::TabListener> listener) override;
void RemoveListener(
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:
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_
7 changes: 7 additions & 0 deletions browser/ui/webui/ai_chat/ai_chat_ui.cc
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

#include "brave/browser/ui/webui/ai_chat/ai_chat_ui.h"

#include <memory>
#include <utility>

#include "brave/browser/ai_chat/ai_chat_service_factory.h"
Expand Down Expand Up @@ -162,6 +163,12 @@ void AIChatUI::BindInterface(
std::move(parent_ui_frame_receiver));
}

void AIChatUI::BindInterface(
mojo::PendingReceiver<ai_chat::mojom::TabInformer> tab_informer_receiver) {
tab_informer_ = std::make_unique<ai_chat::TabInformer>(
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)) &&
Expand Down
5 changes: 5 additions & 0 deletions browser/ui/webui/ai_chat/ai_chat_ui.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@
#include <memory>
#include <string>

#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"
Expand Down Expand Up @@ -40,6 +42,8 @@ class AIChatUI : public ui::MojoWebUIController {
void BindInterface(mojo::PendingReceiver<ai_chat::mojom::Service> receiver);
void BindInterface(mojo::PendingReceiver<ai_chat::mojom::ParentUIFrame>
parent_ui_frame_receiver);
void BindInterface(
mojo::PendingReceiver<ai_chat::mojom::TabInformer> tab_informer_receiver);

// Set by WebUIContentsWrapperT. TopChromeWebUIController provides default
// implementation for this but we don't use it.
Expand All @@ -52,6 +56,7 @@ class AIChatUI : public ui::MojoWebUIController {

private:
std::unique_ptr<ai_chat::AIChatUIPageHandler> page_handler_;
std::unique_ptr<ai_chat::TabInformer> tab_informer_;

base::WeakPtr<TopChromeWebUIController::Embedder> embedder_;
raw_ptr<Profile> profile_ = nullptr;
Expand Down
1 change: 1 addition & 0 deletions components/ai_chat/core/common/mojom/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ mojom_component("mojom") {
"ai_chat.mojom",
"page_content_extractor.mojom",
"settings_helper.mojom",
"tab_informer.mojom",
"untrusted_frame.mojom",
]

Expand Down
30 changes: 30 additions & 0 deletions components/ai_chat/core/common/mojom/tab_informer.mojom
Original file line number Diff line number Diff line change
@@ -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<Tab> 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<Window> windows);
};

interface TabInformer {
AddListener(pending_remote<TabListener> listener);
RemoveListener(pending_remote<TabListener> listener);
};
1 change: 1 addition & 0 deletions components/ai_chat/resources/common/mojom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
26 changes: 22 additions & 4 deletions components/ai_chat/resources/page/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand All @@ -19,6 +19,7 @@ export type State = Mojom.ServiceState & {
isMobile: boolean
isHistoryFeatureEnabled: boolean
allActions: Mojom.ActionGroup[]
windows: Mojom.Window[]
}

export const defaultUIState: State = {
Expand All @@ -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<State> {
class PageAPI extends API<State> implements Mojom.TabListenerInterface {
public service: Mojom.ServiceRemote
= Mojom.Service.getRemote()

Expand All @@ -53,6 +55,11 @@ class PageAPI extends API<State> {
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()
Expand Down Expand Up @@ -86,6 +93,11 @@ class PageAPI extends API<State> {
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)
})
Expand Down Expand Up @@ -116,6 +128,12 @@ class PageAPI extends API<State> {
})
}

tabsChanged(windows: Mojom.Window[]) {
this.setPartialState({
windows
})
}

private async getCurrentPremiumStatus() {
const { status } = await this.service.getPremiumStatus()
return {
Expand Down

0 comments on commit f608a76

Please sign in to comment.