commit 5458f648958f245a1886c168a85b7516ecc993fe
parent 9604eb03510b2d8669b96c122a956818bea1a792
Author: Nick Grato <ngrato@gmail.com>
Date: Mon, 5 Jan 2026 22:31:02 +0000
Bug 2007135 - Create Sidebar service to control sidebar state from other location in the window r=ai-frontend-reviewers,Gijs
We will need the flexibility to open the side bar from the tool bar, currently I am aware of one but I think there might be more and it would be nice to isolate this functionality to be re-used. The patch creates a service that can be called with the window object to open the new ai window sidebar.
Differential Revision: https://phabricator.services.mozilla.com/D277209
Diffstat:
5 files changed, 289 insertions(+), 100 deletions(-)
diff --git a/browser/components/aiwindow/ui/modules/AIWindowUI.sys.mjs b/browser/components/aiwindow/ui/modules/AIWindowUI.sys.mjs
@@ -0,0 +1,145 @@
+/**
+ * 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 http://mozilla.org/MPL/2.0/.
+ */
+
+const AIWINDOW_SIDEBAR_URL =
+ "chrome://browser/content/aiwindow/aiWindow.html#mode=sidebar";
+
+export const AIWindowUI = {
+ BOX_ID: "ai-window-box",
+ SPLITTER_ID: "ai-window-splitter",
+ BROWSER_ID: "ai-window-browser",
+ STACK_CLASS: "ai-window-browser-stack",
+
+ /**
+ * @param {Window} win
+ * @returns {{ chromeDoc: Document, box: Element, splitter: Element } | null}
+ */
+ _getSidebarElements(win) {
+ if (!win) {
+ return null;
+ }
+ const chromeDoc = win.document;
+ const box = chromeDoc.getElementById(this.BOX_ID);
+ const splitter = chromeDoc.getElementById(this.SPLITTER_ID);
+
+ if (!box || !splitter) {
+ return null;
+ }
+ return { chromeDoc, box, splitter };
+ },
+
+ /**
+ * Ensure the aiwindow <browser> exists under the sidebar box.
+ *
+ * @param {Document} chromeDoc
+ * @param {Element} box
+ * @returns {XULElement} browser
+ */
+ ensureBrowserIsAppended(chromeDoc, box) {
+ const existingBrowser = chromeDoc.getElementById(this.BROWSER_ID);
+ if (existingBrowser) {
+ // Already exists
+ return existingBrowser;
+ }
+
+ const stack = box.querySelector(`.${this.STACK_CLASS}`);
+
+ if (!stack.isConnected) {
+ stack.className = this.STACK_CLASS;
+ stack.setAttribute("flex", "1");
+ box.appendChild(stack);
+ }
+
+ const browser = chromeDoc.createXULElement("browser");
+ browser.id = this.BROWSER_ID;
+ browser.setAttribute("transparent", "true");
+ browser.setAttribute("flex", "1");
+ browser.setAttribute("disablehistory", "true");
+ browser.setAttribute("disablefullscreen", "true");
+ browser.setAttribute("tooltip", "aHTMLTooltip");
+ browser.setAttribute("src", AIWINDOW_SIDEBAR_URL);
+ stack.appendChild(browser);
+ return browser;
+ },
+
+ /**
+ * @param {Window} win
+ * @returns {boolean} whether the sidebar is open (visible)
+ */
+ isSidebarOpen(win) {
+ const nodes = this._getSidebarElements(win);
+ if (!nodes) {
+ return false;
+ }
+ // The sidebar is considered open if the box is visible
+ return !nodes.box.hidden;
+ },
+
+ /**
+ * Open the AI Window sidebar
+ *
+ * @param {Window} win
+ */
+ openSidebar(win) {
+ const nodes = this._getSidebarElements(win);
+
+ if (!nodes) {
+ return;
+ }
+
+ const { chromeDoc, box, splitter } = nodes;
+
+ this.ensureBrowserIsAppended(chromeDoc, box);
+
+ box.hidden = false;
+ splitter.hidden = false;
+ box.parentElement.hidden = false;
+ },
+
+ /**
+ * Close the AI Window sidebar.
+ *
+ * @param {Window} win
+ */
+ closeSidebar(win) {
+ const nodes = this._getSidebarElements(win);
+ if (!nodes) {
+ return;
+ }
+ const { box, splitter } = nodes;
+
+ box.hidden = true;
+ splitter.hidden = true;
+ },
+
+ /**
+ * Toggle the AI Window sidebar
+ *
+ * @param {Window} win
+ * @returns {boolean} true if now open, false if now closed
+ */
+ toggleSidebar(win) {
+ const nodes = this._getSidebarElements(win);
+ if (!nodes) {
+ return false;
+ }
+ const { chromeDoc, box, splitter } = nodes;
+
+ const opening = box.hidden;
+ if (opening) {
+ this.ensureBrowserIsAppended(chromeDoc, box);
+ }
+
+ box.hidden = !opening;
+ splitter.hidden = !opening;
+
+ if (opening && box.parentElement?.hidden) {
+ box.parentElement.hidden = false;
+ }
+
+ return opening;
+ },
+};
diff --git a/browser/components/aiwindow/ui/moz.build b/browser/components/aiwindow/ui/moz.build
@@ -14,6 +14,7 @@ MOZ_SRC_FILES += [
"modules/AIWindow.sys.mjs",
"modules/AIWindowAccountAuth.sys.mjs",
"modules/AIWindowMenu.sys.mjs",
+ "modules/AIWindowUI.sys.mjs",
"modules/ChatConstants.sys.mjs",
"modules/ChatConversation.sys.mjs",
"modules/ChatEnums.sys.mjs",
diff --git a/browser/components/aiwindow/ui/test/browser/browser.toml b/browser/components/aiwindow/ui/test/browser/browser.toml
@@ -15,6 +15,8 @@ support-files = [
["browser_aiwindow_transparency.js"]
+["browser_aiwindowui.js"]
+
["browser_open_aiwindow.js"]
["browser_sidebar_aiwindow.js"]
diff --git a/browser/components/aiwindow/ui/test/browser/browser_aiwindowui.js b/browser/components/aiwindow/ui/test/browser/browser_aiwindowui.js
@@ -0,0 +1,138 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { AIWindowUI } = ChromeUtils.importESModule(
+ "moz-src:///browser/components/aiwindow/ui/modules/AIWindowUI.sys.mjs"
+);
+
+add_task(async function test_aiwindowui_constants() {
+ is(AIWindowUI.BOX_ID, "ai-window-box", "BOX_ID constant is correct");
+ is(
+ AIWindowUI.SPLITTER_ID,
+ "ai-window-splitter",
+ "SPLITTER_ID constant is correct"
+ );
+ is(
+ AIWindowUI.BROWSER_ID,
+ "ai-window-browser",
+ "BROWSER_ID constant is correct"
+ );
+ is(
+ AIWindowUI.STACK_CLASS,
+ "ai-window-browser-stack",
+ "STACK_CLASS constant is correct"
+ );
+});
+
+add_task(async function test_aiwindowui_sidebar_operations() {
+ const box = document.getElementById(AIWindowUI.BOX_ID);
+ const splitter = document.getElementById(AIWindowUI.SPLITTER_ID);
+
+ if (!box || !splitter) {
+ todo(
+ false,
+ "AI Window elements not present in this window - skipping DOM tests"
+ );
+ return;
+ }
+
+ const initialBoxHidden = box.hidden;
+ const initialSplitterHidden = splitter.hidden;
+
+ try {
+ // Test opening
+ AIWindowUI.openSidebar(window);
+ is(box.hidden, false, "Box should be visible after opening");
+ is(splitter.hidden, false, "Splitter should be visible after opening");
+ is(
+ AIWindowUI.isSidebarOpen(window),
+ true,
+ "isSidebarOpen should return true after opening"
+ );
+
+ // Test closing
+ AIWindowUI.closeSidebar(window);
+ is(box.hidden, true, "Box should be hidden after closing");
+ is(splitter.hidden, true, "Splitter should be hidden after closing");
+ is(
+ AIWindowUI.isSidebarOpen(window),
+ false,
+ "isSidebarOpen should return false after closing"
+ );
+
+ // Test toggling from closed to open
+ const toggleResult1 = AIWindowUI.toggleSidebar(window);
+ is(toggleResult1, true, "Toggle should return true when opening");
+ is(box.hidden, false, "Box should be visible after toggling open");
+ is(
+ splitter.hidden,
+ false,
+ "Splitter should be visible after toggling open"
+ );
+ is(
+ AIWindowUI.isSidebarOpen(window),
+ true,
+ "isSidebarOpen should return true after toggling open"
+ );
+
+ // Test toggling from open to closed
+ const toggleResult2 = AIWindowUI.toggleSidebar(window);
+ is(toggleResult2, false, "Toggle should return false when closing");
+ is(box.hidden, true, "Box should be hidden after toggling closed");
+ is(
+ splitter.hidden,
+ true,
+ "Splitter should be hidden after toggling closed"
+ );
+ is(
+ AIWindowUI.isSidebarOpen(window),
+ false,
+ "isSidebarOpen should return false after toggling closed"
+ );
+ } finally {
+ // Restore initial state
+ box.hidden = initialBoxHidden;
+ splitter.hidden = initialSplitterHidden;
+ }
+});
+
+add_task(async function test_aiwindowui_ensureBrowserIsAppended() {
+ const box = document.getElementById(AIWindowUI.BOX_ID);
+
+ if (!box) {
+ todo(
+ false,
+ "AI Window box element not present - skipping browser creation test"
+ );
+ return;
+ }
+
+ // Remove any existing browser to start clean
+ let existingBrowser = document.getElementById(AIWindowUI.BROWSER_ID);
+ if (existingBrowser) {
+ existingBrowser.remove();
+ }
+
+ try {
+ const browser1 = AIWindowUI.ensureBrowserIsAppended(document, box);
+ ok(browser1, "Should create and return a browser element");
+ is(browser1.id, AIWindowUI.BROWSER_ID, "Browser should have correct ID");
+ ok(browser1.isConnected, "Browser should be connected to DOM");
+
+ // Call again - should return the same browser
+ const browser2 = AIWindowUI.ensureBrowserIsAppended(document, box);
+ is(
+ browser1,
+ browser2,
+ "Should return the same browser instance when called again"
+ );
+ } finally {
+ // Clean up the created browser
+ let createdBrowser = document.getElementById(AIWindowUI.BROWSER_ID);
+ if (createdBrowser) {
+ createdBrowser.remove();
+ }
+ }
+});
diff --git a/browser/components/genai/content/smart-assist.mjs b/browser/components/genai/content/smart-assist.mjs
@@ -16,6 +16,8 @@ ChromeUtils.defineESModuleGetters(lazy, {
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
SpecialMessageActions:
"resource://messaging-system/lib/SpecialMessageActions.sys.mjs",
+ AIWindowUI:
+ "moz-src:///browser/components/aiwindow/ui/modules/AIWindowUI.sys.mjs",
});
const FULL_PAGE_URL = "chrome://browser/content/genai/smartAssistPage.html";
@@ -247,107 +249,8 @@ export class SmartAssist extends MozLitElement {
);
}
- /**
- * Helper method to get the chrome document
- *
- * @returns {Document} The top-level chrome window's document
- */
-
- _getChromeDocument() {
- return window.browsingContext.topChromeWindow.document;
- }
-
- /**
- * Helper method to find an element in the chrome document
- *
- * @param {string} id - The element ID to find
- * @returns {Element|null} The found element or null
- */
-
- _getChromeElement(id) {
- return this._getChromeDocument().getElementById(id);
- }
-
- /**
- * Helper method to get or create the AI window browser element
- *
- * @param {Document} chromeDoc - The chrome document
- * @param {Element} box - The AI window box element
- * @returns {Element} The AI window browser element
- */
-
- _getOrCreateBrowser(chromeDoc, box) {
- let stack = box.querySelector(".ai-window-browser-stack");
- if (!stack) {
- stack = chromeDoc.createXULElement("stack");
- stack.className = "ai-window-browser-stack";
- stack.setAttribute("flex", "1");
- box.appendChild(stack);
- }
-
- let browser = stack.querySelector("#ai-window-browser");
- if (!browser) {
- browser = chromeDoc.createXULElement("browser");
- browser.setAttribute("id", "ai-window-browser");
- browser.setAttribute("flex", "1");
- browser.setAttribute("disablehistory", "true");
- browser.setAttribute("disablefullscreen", "true");
- browser.setAttribute("tooltip", "aHTMLTooltip");
-
- browser.setAttribute(
- "src",
- "chrome://browser/content/aiwindow/aiWindow.html"
- );
-
- stack.appendChild(browser);
- }
- return stack;
- }
-
- /**
- * Helper method to get or create the smartbar element
- *
- * @param {Document} chromeDoc - The chrome document
- * @param {Element} container - The container element
- */
- _getOrCreateSmartbar(chromeDoc, container) {
- // Find existing Smartbar, or create it the first time we open the sidebar.
- let smartbar = chromeDoc.getElementById("ai-window-smartbar");
-
- if (!smartbar) {
- smartbar = chromeDoc.createElement("moz-smartbar");
- smartbar.id = "ai-window-smartbar";
- smartbar.setAttribute("sap-name", "smartbar");
- smartbar.setAttribute("pageproxystate", "invalid");
- smartbar.setAttribute("popover", "manual");
- smartbar.classList.add("smartbar", "urlbar");
- container.append(smartbar);
- }
- return smartbar;
- }
-
_toggleAIWindowSidebar() {
- const chromeDoc = this._getChromeDocument();
- const box = chromeDoc.getElementById("ai-window-box");
- const splitter = chromeDoc.getElementById("ai-window-splitter");
-
- if (!box || !splitter) {
- return;
- }
-
- const stack = this._getOrCreateBrowser(chromeDoc, box);
- this._getOrCreateSmartbar(chromeDoc, stack);
-
- // Toggle visibility
- const opening = box.hidden;
-
- box.hidden = !opening;
- splitter.hidden = !opening;
-
- // Make sure parent container is also visible
- if (box.parentElement && box.parentElement.hidden) {
- box.parentElement.hidden = false;
- }
+ lazy.AIWindowUI.toggleSidebar(window.browsingContext.topChromeWindow);
}
render() {