tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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:
Abrowser/components/aiwindow/ui/modules/AIWindowUI.sys.mjs | 145+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mbrowser/components/aiwindow/ui/moz.build | 1+
Mbrowser/components/aiwindow/ui/test/browser/browser.toml | 2++
Abrowser/components/aiwindow/ui/test/browser/browser_aiwindowui.js | 138+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mbrowser/components/genai/content/smart-assist.mjs | 103+++----------------------------------------------------------------------------
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() {