Source code

Revision control

Copy as Markdown

Other Tools

/*
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/. */
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
AIWINDOW_URL:
"moz-src:///browser/components/aiwindow/ui/modules/AIWindow.sys.mjs",
AIWindowUI:
"moz-src:///browser/components/aiwindow/ui/modules/AIWindowUI.sys.mjs",
ChatStore:
"moz-src:///browser/components/aiwindow/ui/modules/ChatStore.sys.mjs",
});
/**
* @typedef {{
* input: string,
* mode: string,
* pageUrl: URL,
* conversationId: string,
* }} TabState
*/
/**
* Manages state changes of the tabs in AIWindow to keep both the
* fullwindow and sidebar chats in sync as tabs are created/selected.
*/
export class AIWindowTabStatesManager {
/**
* The browser window instance that this manager operates on
*/
#window;
/**
* The currently selected browser tab
*
* @type {MozTabbrowserTab}
*/
#selectedTab;
/**
* A map of tabs and their states
*
* @type {WeakMap<MozTabbrowserTab, TabState>}
*/
#tabStates;
/**
* Global progress listener for all tabs
*/
#tabsListener;
constructor(win) {
this.#init(win);
}
/**
* Adds event listeners needed to manage tab states
*
* @param {ChromeWindow} win
*
* @private
*/
#init(win) {
this.#window = win;
this.#tabStates = new WeakMap();
const tabContainer = this.#window.gBrowser.tabContainer;
tabContainer.addEventListener("TabOpen", this);
tabContainer.addEventListener("TabSelect", this);
tabContainer.addEventListener("TabClose", this);
this.#tabsListener = this.#getTabsListener();
this.#window.gBrowser.addTabsProgressListener(this.#tabsListener);
this.#setUpInitialTabs();
this.#addWindowEventListeners();
}
/**
* Add event listeners to the window for ai-window:* events
*/
#addWindowEventListeners() {
this.#window.addEventListener(
"ai-window:smartbar-input",
this.#onSmartbarInput
);
this.#window.addEventListener(
"ai-window:connected",
this.#onAIWindowConnected
);
this.#window.addEventListener(
"ai-window:opened-conversation",
this.#onConversationOpened
);
}
/**
* Adds event listeners for any tabs that are present when the window opens.
* The TabOpen event does not fire for the initial tab of a new window, for example.
*
* @private
*/
#setUpInitialTabs() {
this.#window.gBrowser.tabs.forEach(tab => {
if (this.#tabStates.has(tab)) {
return;
}
this.#addEventListeners(tab);
});
}
/**
* Handles tab events
*
* @param {Event} event
*
* @private
*/
handleEvent(event) {
switch (event.type) {
case "TabOpen":
this.#onTabOpen(event);
break;
case "TabSelect":
this.#onTabSelect(event);
break;
case "TabClose":
this.#onTabClose(event);
break;
}
}
/**
* Handles TabOpen events from a new browser tab to add
* event listeners to it.
*
* @param {Event} event
*
* @private
*/
#onTabOpen(event) {
this.#addEventListeners(event.target);
}
/**
* Handles TabSelect events from a new browser tab to
* update the state of the sidebar.
*
* @param {Event} event
*
* @private
*/
async #onTabSelect(event) {
this.#selectedTab = event.target;
const tabState = this.#getTabState(this.#selectedTab);
const convId = tabState?.state?.conversationId;
if (!convId) {
lazy.AIWindowUI.closeSidebar(this.#window);
return;
}
const tabUrl = this.#selectedTab.linkedBrowser.currentURI.spec;
const conversation = await lazy.ChatStore.findConversationById(convId);
const tabNeedsSidebar =
conversation &&
conversation.messages.length &&
tabUrl !== lazy.AIWINDOW_URL;
if (tabNeedsSidebar) {
lazy.AIWindowUI.openSidebar(this.#window, conversation);
} else {
lazy.AIWindowUI.closeSidebar(this.#window);
}
// TODO: Bug 2014936
// Update input
}
/**
* Handles TabClose events from a new browser tab to
* clean up after the tab is gone.
*
* @param {Event} event
*
* @private
*/
#onTabClose(event) {
this.#removeEventListeners(event.target);
}
/**
* Adds a tab to the state map.
*
* @param {MozTabbrowserTab} tab
*
* @private
*/
#addEventListeners(tab) {
this.#tabStates.set(tab, { state: null });
}
/**
* Removes necessary event listeners from a tab.
*
* @param {MozTabbrowserTab} tab
*
* @private
*/
#removeEventListeners(tab) {
this.#tabStates.delete(tab);
}
/**
* Listens for ai-window:connected events from ai-window.mjs instances
*
* @param {AIWindowStateEvent} event
*
* @private
*/
#onAIWindowConnected = async event => {
const tabState = this.#getTabState(event.detail.tab, event.detail);
const { mode, pageUrl, conversationId, input } = tabState;
const conversation = await lazy.ChatStore.findConversationById(
conversationId || event.detail.conversationId
);
const isAIWindow = pageUrl === lazy.AIWINDOW_URL;
const needsSidebar =
this.#selectedTab === event.detail.tab &&
mode === "fullpage" &&
!isAIWindow &&
input &&
conversation &&
conversation.messages.length;
// NOTE: Don't need to fire open/close sidebar from here, the location change
// event handler is taking care of that logic.
if (needsSidebar) {
// TODO: Bug 2014936
// Update smartbar input
}
};
/**
* Gets the state for the specified tab. Will update the state
* if a newState is passed in.
*
* @param {*} tab The browser tab to get state for
* @param {*} [newState=null] New state to update the tab with
*
* @returns {TabState}
*
* @private
*/
#getTabState(tab, newState = null) {
const tabState = this.#tabStates.get(tab) ?? {};
if (newState) {
const { input, mode, pageUrl, conversationId } = tabState.state ?? {};
delete newState.tab;
tabState.state = {
input,
mode,
pageUrl,
conversationId,
...newState,
};
this.#tabStates.set(tab, tabState);
}
return tabState;
}
/**
* Handles input events from the Smartbar, updates the state
* with the latest input
*
* @param {TabStateEvent} event
*/
#onSmartbarInput = event => {
this.#getTabState(event.detail.tab, event.detail);
};
/**
* Handles ai-window:opened-conversation events from the ai-window.mjs,
* updates the state with conversation info
*
* @param {TabStateEvent} event
*/
#onConversationOpened = event => {
const { mode, conversationId, tab } = event.detail;
this.#getTabState(tab, { mode, conversationId });
};
/**
* Gets a global progress listener for all tabs. The callbacks from
* addTabsProgressListener prepend a browser argument.
*/
#getTabsListener() {
return {
QueryInterface: ChromeUtils.generateQI([
"nsIWebProgressListener",
"nsISupportsWeakReference",
]),
onLocationChange: async (
_browser,
webProgress,
_request,
locationURI,
_flags
) => {
if (!webProgress.isTopLevel) {
return;
}
const browser = webProgress.browsingContext?.embedderElement;
const tab = this.#window.gBrowser.getTabForBrowser(browser);
const tabState = this.#tabStates.get(tab);
if (!tabState || !tabState?.state?.conversationId) {
return;
}
const isSidebarOpen = lazy.AIWindowUI.isSidebarOpen(this.#window);
const conversation = await lazy.ChatStore.findConversationById(
tabState.state.conversationId
);
const isAiWindowUrl = locationURI.spec === lazy.AIWINDOW_URL;
const needsSidebar =
!isAiWindowUrl &&
tabState.state.mode === "fullpage" &&
conversation?.messages?.length &&
!isSidebarOpen;
const needsCloseSidebar =
isAiWindowUrl &&
tabState.state.mode === "fullpage" &&
conversation?.messages.length &&
isSidebarOpen;
if (needsSidebar) {
lazy.AIWindowUI.openSidebar(this.#window, conversation);
} else if (needsCloseSidebar) {
lazy.AIWindowUI.closeSidebar(this.#window);
}
},
onStateChange() {},
onProgressChange() {},
onStatusChange() {},
onSecurityChange() {},
onContentBlockingEvent() {},
};
}
}