tor-browser

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

commit 5ffd4fafff85925846643dbb42b2a18957ee1e3a
parent 1e94478ce97254e87981a2b5e96b36d61179f91a
Author: unifolia <jlewis@mozilla.com>
Date:   Sat,  6 Dec 2025 01:04:52 +0000

Bug 2000985 - Add conditional AI Window entrypoint in File menu, ensure New Window opens same type r=Gijs,fluent-reviewers,ai-frontend-reviewers,bolsson

Differential Revision: https://phabricator.services.mozilla.com/D274992

Diffstat:
Mbrowser/base/content/appmenu-viewcache.inc.xhtml | 13++++++++-----
Mbrowser/base/content/browser-menubar.inc | 6++++++
Mbrowser/base/content/browser-sets.inc | 1+
Mbrowser/base/content/browser-sets.js | 3+++
Mbrowser/base/content/browser.js | 43+++++++++++++++++--------------------------
Mbrowser/base/content/browser.js.globals | 1+
Mbrowser/base/content/macWindow.inc.xhtml | 1+
Mbrowser/components/aiwindow/ui/modules/AIWindow.sys.mjs | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mbrowser/components/aiwindow/ui/test/browser/browser_open_aiwindow.js | 274++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
Mbrowser/components/customizableui/content/panelUI.js | 9++++++++-
Mbrowser/locales-preview/aiWindow.ftl | 9+++++++++
11 files changed, 319 insertions(+), 106 deletions(-)

diff --git a/browser/base/content/appmenu-viewcache.inc.xhtml b/browser/base/content/appmenu-viewcache.inc.xhtml @@ -49,16 +49,19 @@ data-l10n-id="appmenuitem-new-window" key="key_newNavigator" command="cmd_newNavigator"/> + <toolbarbutton id="appMenu-new-ai-window-button" + class="subviewbutton" + data-l10n-id="appmenuitem-new-ai-window" + command="Tools:AIWindow"/> + <toolbarbutton id="appMenu-new-classic-window-button" + class="subviewbutton" + data-l10n-id="appmenuitem-new-classic-window" + command="Tools:ClassicWindow"/> <toolbarbutton id="appMenu-new-private-window-button2" class="subviewbutton" data-l10n-id="appmenuitem-new-private-window" key="key_privatebrowsing" command="Tools:PrivateBrowsing"/> - <toolbarbutton id="appMenu-new-ai-window-button" - class="subviewbutton" - data-l10n-id="appmenuitem-new-ai-window" - command="Tools:AIWindow" - hidden="true" /> <toolbarseparator/> <toolbarbutton id="appMenu-bookmarks-button" class="subviewbutton subviewbutton-nav" diff --git a/browser/base/content/browser-menubar.inc b/browser/base/content/browser-menubar.inc @@ -17,6 +17,12 @@ <menuitem id="menu_newNavigator" key="key_newNavigator" command="cmd_newNavigator" data-l10n-id="menu-file-new-window"/> + <menuitem id="menu_newAIWindow" + command="Tools:AIWindow" + data-l10n-id="menu-file-new-ai-window"/> + <menuitem id="menu_newClassicWindow" + command="Tools:ClassicWindow" + data-l10n-id="menu-file-new-classic-window"/> <menuitem id="menu_newPrivateWindow" command="Tools:PrivateBrowsing" key="key_privatebrowsing" data-l10n-id="menu-file-new-private-window"/> diff --git a/browser/base/content/browser-sets.inc b/browser/base/content/browser-sets.inc @@ -98,6 +98,7 @@ <command id="Tools:Addons" /> <command id="cmd_openUnifiedExtensionsPanel" /> <command id="Tools:AIWindow" /> + <command id="Tools:ClassicWindow" /> <command id="Tools:Sanitize" /> <command id="Tools:PrivateBrowsing" /> <command id="Browser:Screenshot" /> diff --git a/browser/base/content/browser-sets.js b/browser/base/content/browser-sets.js @@ -224,6 +224,9 @@ document.addEventListener( case "cmd_openUnifiedExtensionsPanel": gUnifiedExtensions.openPanel(event); break; + case "Tools:ClassicWindow": + OpenBrowserWindow({ aiWindow: false }); + break; case "Tools:AIWindow": OpenBrowserWindow({ aiWindow: true }); break; diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js @@ -13,6 +13,8 @@ var { AppConstants } = ChromeUtils.importESModule( // lazy module getters ChromeUtils.defineESModuleGetters(this, { + AIWindow: + "moz-src:///browser/components/aiwindow/ui/modules/AIWindow.sys.mjs", AMTelemetry: "resource://gre/modules/AddonManager.sys.mjs", AboutNewTab: "resource:///modules/AboutNewTab.sys.mjs", AboutReaderParent: "resource:///actors/AboutReaderParent.sys.mjs", @@ -1677,41 +1679,20 @@ function toOpenWindowByType(inType, uri, features) { ); } } - /** * Open a new browser window. See `BrowserWindowTracker.openWindow` for * options. * * @return a reference to the new window. */ -function OpenBrowserWindow(options = {}) { +function OpenBrowserWindow(options) { let timerId = Glean.browserTimings.newWindow.start(); + options ??= {}; + options.openerWindow ??= window; - if (options?.aiWindow) { - options.args ??= Cc["@mozilla.org/array;1"].createInstance( - Ci.nsIMutableArray - ); + AIWindow.handleAIWindowOptions(window, options); - if (!options.args.length) { - const aiWindowURI = Cc["@mozilla.org/supports-string;1"].createInstance( - Ci.nsISupportsString - ); - aiWindowURI.data = "chrome://browser/content/genai/smartAssist.html"; - - const aiOption = Cc["@mozilla.org/hash-property-bag;1"].createInstance( - Ci.nsIWritablePropertyBag2 - ); - aiOption.setPropertyAsBool("ai-window", options.aiWindow); - - options.args.appendElement(aiWindowURI); - options.args.appendElement(aiOption); - } - } - - let win = BrowserWindowTracker.openWindow({ - openerWindow: window, - ...options, - }); + let win = BrowserWindowTracker.openWindow(options); win.addEventListener( "MozAfterPaint", @@ -1880,6 +1861,16 @@ let gFileMenu = { ); } PrintUtils.updatePrintSetupMenuHiddenState(); + + const aiWindowMenu = event.target.querySelector("#menu_newAIWindow"); + const classicWindowMenu = event.target.querySelector( + "#menu_newClassicWindow" + ); + + aiWindowMenu.hidden = + !AIWindow.isAIWindowEnabled() || AIWindow.isAIWindowActive(window); + classicWindowMenu.hidden = + !AIWindow.isAIWindowEnabled() || !AIWindow.isAIWindowActive(window); }, }; diff --git a/browser/base/content/browser.js.globals b/browser/base/content/browser.js.globals @@ -1,5 +1,6 @@ [ "XPCOMUtils", + "AIWindow", "AppConstants", "gBrowser", "gContextMenu", diff --git a/browser/base/content/macWindow.inc.xhtml b/browser/base/content/macWindow.inc.xhtml @@ -16,6 +16,7 @@ <html:link rel="localization" href="browser/menubar.ftl"/> <html:link rel="localization" href="browser/reportBrokenSite.ftl"/> <html:link rel="localization" href="browser/screenshots.ftl"/> + <html:link rel="localization" href="preview/aiWindow.ftl"/> <html:link rel="localization" href="toolkit/branding/brandings.ftl"/> <html:link rel="localization" href="toolkit/global/textActions.ftl"/> </linkset> diff --git a/browser/components/aiwindow/ui/modules/AIWindow.sys.mjs b/browser/components/aiwindow/ui/modules/AIWindow.sys.mjs @@ -3,6 +3,7 @@ * 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/. */ +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; /** * AI Window Service @@ -20,18 +21,78 @@ export const AIWindow = { if (this._initialized) { return; } + + XPCOMUtils.defineLazyPreferenceGetter( + this, + "AIWindowEnabled", + "browser.aiwindow.enabled", + false + ); + this._initialized = true; this._windowStates.set(win, {}); }, /** + * Sets options for new AI Window if new or inherited conditions are met + * + * @param {object} win opener window + * @param {object} options options to be passed into BrowserWindowTracker.openWindow + */ + handleAIWindowOptions(win, options = {}) { + const { openerWindow } = options; + + const canInheritAIWindow = + this.isAIWindowActive(win) && + !options.private && + !Object.hasOwn(options, "aiWindow"); + + const willOpenAIWindow = + openerWindow && + openerWindow.AIWindow?.isAIWindowEnabled && + (options.aiWindow || canInheritAIWindow); + + if (!willOpenAIWindow) { + return; + } + + options.args ??= Cc["@mozilla.org/array;1"].createInstance( + Ci.nsIMutableArray + ); + + if (!options.args.length) { + const aiWindowURI = Cc["@mozilla.org/supports-string;1"].createInstance( + Ci.nsISupportsString + ); + aiWindowURI.data = "chrome://browser/content/genai/smartAssist.html"; + options.args.appendElement(aiWindowURI); + + const aiOption = Cc["@mozilla.org/hash-property-bag;1"].createInstance( + Ci.nsIWritablePropertyBag2 + ); + aiOption.setPropertyAsBool("ai-window", options.aiWindow); + options.args.appendElement(aiOption); + } + }, + + /** + * Is current window an AI Window + * + * @param {object} win current Window + * @returns {boolean} whether current Window is an AI Window + */ + + isAIWindowActive(win) { + return win.document.documentElement.hasAttribute("ai-window"); + }, + + /** * Is AI Window enabled * * @returns {boolean} whether AI Window is enabled */ isAIWindowEnabled() { - // TODO - Placeholder for actual implementation - return true; + return this.AIWindowEnabled; }, }; diff --git a/browser/components/aiwindow/ui/test/browser/browser_open_aiwindow.js b/browser/components/aiwindow/ui/test/browser/browser_open_aiwindow.js @@ -4,107 +4,237 @@ "use strict"; /** - * Test that the 'New AI window' menu item visibility is controlled by the browser.aiwindow.enabled preference - * and matches the PanelUI.isAIWindowEnabled state. + * Test window type detection and menu item visibility based on aiwindow pref and window type. */ -add_task(async function test_ai_menuitem_pref_connection() { - // Scenario 1: Pref is false: 'New AI window' menu item should be hidden +add_task(async function test_window_type_and_menu_visibility() { + // AI Window disabled await SpecialPowers.pushPrefEnv({ set: [["browser.aiwindow.enabled", false]], }); - // Open the browser's hamburger menu - const menuButton = document.getElementById("PanelUI-menu-button"); - const mainViewID = "appMenu-mainView"; - const mainView = PanelMultiView.getViewNode(document, mainViewID); - let viewShownPromise = BrowserTestUtils.waitForEvent(mainView, "ViewShown"); - menuButton.click(); - await viewShownPromise; - - // Get the 'New AI window' item - const aiMenuItem = document.getElementById("appMenu-new-ai-window-button"); - - // Verify that PanelUI.isAIWindowEnabled is false - Assert.equal( - PanelUI.isAIWindowEnabled, + await openHamburgerMenu(); + checkMenuItemVisibility( false, - "PanelUI.isAIWindowEnabled should be false when pref is false" + document.getElementById("appMenu-new-ai-window-button"), + document.getElementById("appMenu-new-classic-window-button") ); + await closeHamburgerMenu(); + + let fileMenuPopup = document.getElementById("menu_FilePopup"); + if (fileMenuPopup) { + await openFileMenu(fileMenuPopup); + checkMenuItemVisibility( + false, + document.getElementById("menu_newAIWindow"), + document.getElementById("menu_newClassicWindow") + ); + await closeFileMenu(fileMenuPopup); + } - // Verify that the menu item is hidden - Assert.ok( - aiMenuItem.hidden, - "AI menu item should be hidden when pref is false" - ); - - // Close the menu - let panelHiddenPromise = BrowserTestUtils.waitForEvent( - document.getElementById("appMenu-popup"), - "popuphidden" - ); - PanelUI.hide(); - await panelHiddenPromise; + await SpecialPowers.popPrefEnv(); - // Test scenario 2: Pref is true: 'New AI window' menu item should NOT be hidden, etc. + // AI Window enabled await SpecialPowers.pushPrefEnv({ set: [["browser.aiwindow.enabled", true]], }); - viewShownPromise = BrowserTestUtils.waitForEvent(mainView, "ViewShown"); - menuButton.click(); - await viewShownPromise; - - Assert.equal( - PanelUI.isAIWindowEnabled, + await openHamburgerMenu(); + checkMenuItemVisibility( true, - "PanelUI.isAIWindowEnabled should be true when pref is true" - ); - - Assert.ok( - !aiMenuItem.hidden, - "AI menu item should be visible when pref is true" + document.getElementById("appMenu-new-ai-window-button"), + document.getElementById("appMenu-new-classic-window-button") ); + await closeHamburgerMenu(); + + if (fileMenuPopup) { + await openFileMenu(fileMenuPopup); + checkMenuItemVisibility( + true, + document.getElementById("menu_newAIWindow"), + document.getElementById("menu_newClassicWindow") + ); + await closeFileMenu(fileMenuPopup); + } - panelHiddenPromise = BrowserTestUtils.waitForEvent( - document.getElementById("appMenu-popup"), - "popuphidden" - ); - PanelUI.hide(); - await panelHiddenPromise; - - // Clean up - reset pref - await SpecialPowers.popPrefEnv(); await SpecialPowers.popPrefEnv(); }); /** - * Test that clicking the 'New AI window' menu item opens a new window with the ai-window attribute. + * Test that clicking AI window and classic window buttons opens the correct window type. */ -add_task(async function test_ai_menuitem_opens_window_with_attribute() { +add_task(async function test_button_actions() { await SpecialPowers.pushPrefEnv({ set: [["browser.aiwindow.enabled", true]], }); - const menuButton = document.getElementById("PanelUI-menu-button"); - const mainView = document.getElementById("appMenu-mainView"); - const viewShownPromise = BrowserTestUtils.waitForEvent(mainView, "ViewShown"); - menuButton.click(); - await viewShownPromise; + const currentWindowIsAIWindow = isAIWindow(); + + await openHamburgerMenu(); + + const buttonId = currentWindowIsAIWindow + ? "appMenu-new-classic-window-button" + : "appMenu-new-ai-window-button"; + const button = document.getElementById(buttonId); + + if (button && !button.hidden) { + let delayedStartupPromise = BrowserTestUtils.waitForNewWindow(); + button.click(); + + const newWin = await delayedStartupPromise; + const newWinIsAI = + newWin.document.documentElement.hasAttribute("ai-window"); + + Assert.equal( + newWinIsAI, + !currentWindowIsAIWindow, + `Clicking ${currentWindowIsAIWindow ? "classic" : "AI"} window button should open a ${currentWindowIsAIWindow ? "classic" : "AI"} window` + ); + + if (newWinIsAI) { + let fileMenuPopup = newWin.document.getElementById("menu_FilePopup"); + + await openHamburgerMenu(newWin); + checkMenuItemVisibility( + true, + newWin.document.getElementById("appMenu-new-ai-window-button"), + newWin.document.getElementById("appMenu-new-classic-window-button") + ); + await closeHamburgerMenu(newWin); + + if (fileMenuPopup) { + await openFileMenu(fileMenuPopup); + checkMenuItemVisibility( + true, + newWin.document.getElementById("menu_newAIWindow"), + newWin.document.getElementById("menu_newClassicWindow") + ); + await closeFileMenu(fileMenuPopup); + } + } + + await BrowserTestUtils.closeWindow(newWin); + } else { + await closeHamburgerMenu(); + } + + await openHamburgerMenu(); + + const appMenuNewWindow = document.getElementById( + "appMenu-new-window-button2" + ); + if (appMenuNewWindow && !appMenuNewWindow.hidden) { + let delayedStartupPromise = BrowserTestUtils.waitForNewWindow(); + appMenuNewWindow.click(); - // Set up listeners for the new AI window - const delayedStartupPromise = BrowserTestUtils.waitForNewWindow(); + const newWin = await delayedStartupPromise; + const newWinIsAI = + newWin.document.documentElement.hasAttribute("ai-window"); - const aiMenuItem = document.getElementById("appMenu-new-ai-window-button"); - aiMenuItem.click(); + Assert.equal( + newWinIsAI, + currentWindowIsAIWindow, + "New window button should open same type as current window" + ); - // Wait for the new window to open - const newWin = await delayedStartupPromise; + await BrowserTestUtils.closeWindow(newWin); + } - Assert.ok( - newWin.document.documentElement.hasAttribute("ai-window"), - "New window should have the ai-window attribute" - ); + const appMenuPopup = document.getElementById("appMenu-popup"); + if (appMenuPopup && appMenuPopup.state !== "closed") { + await closeHamburgerMenu(); + } - await BrowserTestUtils.closeWindow(newWin); await SpecialPowers.popPrefEnv(); }); + +function checkMenuItemVisibility( + aiWindowEnabled, + aiOpenerButton, + classicOpenerButton +) { + const doc = + aiOpenerButton?.ownerDocument || + classicOpenerButton?.ownerDocument || + document; + const currentWindowIsAIWindow = doc.documentElement.hasAttribute("ai-window"); + + if (!aiWindowEnabled) { + Assert.ok( + !aiOpenerButton || aiOpenerButton.hidden, + `AI Window button should not be visible when browser.aiwindow.enabled is false` + ); + Assert.ok( + !classicOpenerButton || classicOpenerButton.hidden, + `Classic Window button should not be visible when browser.aiwindow.enabled is false` + ); + } else if (currentWindowIsAIWindow) { + Assert.ok( + !aiOpenerButton || aiOpenerButton.hidden, + `AI Window button should be hidden in AI Window when browser.aiwindow.enabled is true` + ); + Assert.ok( + classicOpenerButton && !classicOpenerButton.hidden, + `Classic Window button should be visible in AI Window when browser.aiwindow.enabled is true` + ); + } else { + Assert.ok( + aiOpenerButton && !aiOpenerButton.hidden, + `AI Window button should be visible in Classic Window when browser.aiwindow.enabled is true` + ); + Assert.ok( + !classicOpenerButton || classicOpenerButton.hidden, + `Classic Window button should be hidden in Classic Window when browser.aiwindow.enabled is true` + ); + } +} + +function isAIWindow() { + return window.document.documentElement.hasAttribute("ai-window"); +} + +async function openHamburgerMenu(aiWindow = null) { + let menuButton = aiWindow + ? aiWindow.document.getElementById("PanelUI-menu-button") + : document.getElementById("PanelUI-menu-button"); + let mainView = aiWindow + ? PanelMultiView.getViewNode(aiWindow.document, "appMenu-mainView") + : PanelMultiView.getViewNode(document, "appMenu-mainView"); + + let viewShownPromise = BrowserTestUtils.waitForEvent(mainView, "ViewShown"); + menuButton.click(); + await viewShownPromise; +} + +async function closeHamburgerMenu(aiWindow = null) { + let appMenuPopup = aiWindow + ? aiWindow.document.getElementById("appMenu-popup") + : document.getElementById("appMenu-popup"); + + let panelHiddenPromise = BrowserTestUtils.waitForEvent( + appMenuPopup, + "popuphidden" + ); + + if (aiWindow) { + aiWindow.PanelUI.hide(); + } else { + PanelUI.hide(); + } + await panelHiddenPromise; +} + +async function openFileMenu(menu) { + return new Promise(resolve => { + menu.addEventListener("popupshown", resolve, { once: true }); + menu.dispatchEvent(new MouseEvent("popupshowing", { bubbles: true })); + menu.dispatchEvent(new MouseEvent("popupshown", { bubbles: true })); + }); +} + +async function closeFileMenu(menu) { + return new Promise(resolve => { + menu.addEventListener("popuphidden", resolve, { once: true }); + menu.dispatchEvent(new MouseEvent("popuphiding", { bubbles: true })); + menu.dispatchEvent(new MouseEvent("popuphidden", { bubbles: true })); + }); +} diff --git a/browser/components/customizableui/content/panelUI.js b/browser/components/customizableui/content/panelUI.js @@ -1064,11 +1064,18 @@ const PanelUI = { }, _showAIMenuItem() { + const isAIWindowActive = document.documentElement.hasAttribute("ai-window"); const aiMenuItem = PanelMultiView.getViewNode( document, "appMenu-new-ai-window-button" ); - aiMenuItem.hidden = !this.isAIWindowEnabled; + const classicWindowMenuItem = PanelMultiView.getViewNode( + document, + "appMenu-new-classic-window-button" + ); + + aiMenuItem.hidden = !this.isAIWindowEnabled || isAIWindowActive; + classicWindowMenuItem.hidden = !this.isAIWindowEnabled || !isAIWindowActive; }, _showBadge(notification) { diff --git a/browser/locales-preview/aiWindow.ftl b/browser/locales-preview/aiWindow.ftl @@ -6,3 +6,12 @@ appmenuitem-new-ai-window = .label = New AI window + +appmenuitem-new-classic-window = + .label = New classic window + +menu-file-new-ai-window = + .label = New AI Window + +menu-file-new-classic-window = + .label = New Classic Window