tor-browser

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

commit fbb87850f74eed36329a2b2a4dac99302b53094a
parent 185d41ebb5abbbc7d7d3ea4c78e1f6951d72a335
Author: unifolia <jlewis@mozilla.com>
Date:   Tue,  2 Dec 2025 07:52:59 +0000

Bug 2000877 - Open AI Window from hamburger menu, set state/AI attr on window r=fluent-reviewers,bolsson,ai-frontend-reviewers,ngrato,Gijs

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

Diffstat:
Mbrowser/base/content/appmenu-viewcache.inc.xhtml | 5+++++
Mbrowser/base/content/browser-init.js | 13+++++++------
Mbrowser/base/content/browser-sets.inc | 1+
Mbrowser/base/content/browser-sets.js | 3+++
Mbrowser/base/content/browser.js | 21+++++++++++++++++++++
Mbrowser/base/content/browser.xhtml | 1+
Mbrowser/base/content/test/static/browser_sentence_case_strings.js | 2+-
Mbrowser/components/aiwindow/ui/moz.build | 2++
Abrowser/components/aiwindow/ui/test/browser/browser.toml | 3+++
Abrowser/components/aiwindow/ui/test/browser/browser_open_aiwindow.js | 110+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mbrowser/components/customizableui/content/panelUI.js | 19+++++++++++++++++++
Abrowser/locales-preview/aiWindow.ftl | 8++++++++
Mbrowser/locales/jar.mn | 1+
Mbrowser/modules/BrowserWindowTracker.sys.mjs | 4++++
14 files changed, 186 insertions(+), 7 deletions(-)

diff --git a/browser/base/content/appmenu-viewcache.inc.xhtml b/browser/base/content/appmenu-viewcache.inc.xhtml @@ -54,6 +54,11 @@ 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-init.js b/browser/base/content/browser-init.js @@ -107,19 +107,20 @@ var gBrowserInit = { toolbarMenubar.setAttribute("data-l10n-attrs", "toolbarname"); } } - // If opening a Taskbar Tab window, add an attribute to the top-level element + // If opening a Taskbar Tab or AI window, add an attribute to the top-level element // to inform window styling. - if (window.arguments && window.arguments[1]) { + if (window.arguments?.[1] instanceof Ci.nsIPropertyBag2) { let extraOptions = window.arguments[1]; - if ( - extraOptions instanceof Ci.nsIWritablePropertyBag2 && - extraOptions.hasKey("taskbartab") - ) { + if (extraOptions.hasKey("taskbartab")) { window.document.documentElement.setAttribute( "taskbartab", extraOptions.getPropertyAsAString("taskbartab") ); } + + if (extraOptions.hasKey("ai-window")) { + document.documentElement.setAttribute("ai-window", true); + } } // Run menubar initialization first, to avoid CustomTitlebar code picking diff --git a/browser/base/content/browser-sets.inc b/browser/base/content/browser-sets.inc @@ -97,6 +97,7 @@ <command id="Tools:Downloads" /> <command id="Tools:Addons" /> <command id="cmd_openUnifiedExtensionsPanel" /> + <command id="Tools:AIWindow" /> <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 @@ -218,6 +218,9 @@ document.addEventListener( case "cmd_openUnifiedExtensionsPanel": gUnifiedExtensions.openPanel(event); break; + case "Tools:AIWindow": + OpenBrowserWindow({ aiWindow: true }); + break; case "Tools:Sanitize": Sanitizer.showUI(window); break; diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js @@ -1687,6 +1687,27 @@ function toOpenWindowByType(inType, uri, features) { function OpenBrowserWindow(options = {}) { let timerId = Glean.browserTimings.newWindow.start(); + if (options?.aiWindow) { + 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"; + + 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, diff --git a/browser/base/content/browser.xhtml b/browser/base/content/browser.xhtml @@ -95,6 +95,7 @@ <link rel="localization" href="preview/smartTabGroups.ftl"/> <link rel="localization" href="preview/genai.ftl"/> <link rel="localization" href="preview/ipProtection.ftl"/> + <link rel="localization" href="preview/aiWindow.ftl" /> <title data-l10n-id="browser-main-window-default-title"></title> diff --git a/browser/base/content/test/static/browser_sentence_case_strings.js b/browser/base/content/test/static/browser_sentence_case_strings.js @@ -22,7 +22,7 @@ const { AppMenuNotifications } = ChromeUtils.importESModule( // These are brand names, proper names, or other things that we expect to // not abide exactly to sentence case. NAMES is for single words, and PHRASES // is for words in a specific order. -const NAMES = new Set(["Mozilla", "Nightly", "Firefox"]); +const NAMES = new Set(["Mozilla", "Nightly", "Firefox", "AI"]); const PHRASES = new Set(["Troubleshoot Mode…"]); let gCUITestUtils = new CustomizableUITestUtils(window); diff --git a/browser/components/aiwindow/ui/moz.build b/browser/components/aiwindow/ui/moz.build @@ -5,6 +5,8 @@ with Files("**"): BUG_COMPONENT = ("Core", "Machine Learning: Frontend") +BROWSER_CHROME_MANIFESTS += ["test/browser/browser.toml"] + MOZ_SRC_FILES += [ "modules/AIWindow.sys.mjs", ] diff --git a/browser/components/aiwindow/ui/test/browser/browser.toml b/browser/components/aiwindow/ui/test/browser/browser.toml @@ -0,0 +1,3 @@ +[DEFAULT] + +["browser_open_aiwindow.js"] diff --git a/browser/components/aiwindow/ui/test/browser/browser_open_aiwindow.js b/browser/components/aiwindow/ui/test/browser/browser_open_aiwindow.js @@ -0,0 +1,110 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"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. + */ +add_task(async function test_ai_menuitem_pref_connection() { + // Scenario 1: Pref is false: 'New AI window' menu item should be hidden + 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, + false, + "PanelUI.isAIWindowEnabled should be false when pref is false" + ); + + // 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; + + // Test scenario 2: Pref is true: 'New AI window' menu item should NOT be hidden, etc. + await SpecialPowers.pushPrefEnv({ + set: [["browser.aiwindow.enabled", true]], + }); + + viewShownPromise = BrowserTestUtils.waitForEvent(mainView, "ViewShown"); + menuButton.click(); + await viewShownPromise; + + Assert.equal( + PanelUI.isAIWindowEnabled, + true, + "PanelUI.isAIWindowEnabled should be true when pref is true" + ); + + Assert.ok( + !aiMenuItem.hidden, + "AI menu item should be visible when pref is true" + ); + + 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. + */ +add_task(async function test_ai_menuitem_opens_window_with_attribute() { + 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; + + // Set up listeners for the new AI window + const delayedStartupPromise = BrowserTestUtils.waitForNewWindow(); + + const aiMenuItem = document.getElementById("appMenu-new-ai-window-button"); + aiMenuItem.click(); + + // Wait for the new window to open + const newWin = await delayedStartupPromise; + + Assert.ok( + newWin.document.documentElement.hasAttribute("ai-window"), + "New window should have the ai-window attribute" + ); + + await BrowserTestUtils.closeWindow(newWin); + await SpecialPowers.popPrefEnv(); +}); diff --git a/browser/components/customizableui/content/panelUI.js b/browser/components/customizableui/content/panelUI.js @@ -83,6 +83,16 @@ const PanelUI = { autoHidePref => autoHidePref && Services.appinfo.OS !== "Darwin" ); + XPCOMUtils.defineLazyPreferenceGetter( + this, + "isAIWindowEnabled", + "browser.aiwindow.enabled", + false, + (_pref, _previousValue, _newValue) => { + this._showAIMenuItem(); + } + ); + if (this.autoHideToolbarInFullScreen) { window.addEventListener("fullscreen", this); } else { @@ -110,6 +120,7 @@ const PanelUI = { "refresh" ); + this._showAIMenuItem(); this._initialized = true; }, @@ -1052,6 +1063,14 @@ const PanelUI = { popupnotification.show(); }, + _showAIMenuItem() { + const aiMenuItem = PanelMultiView.getViewNode( + document, + "appMenu-new-ai-window-button" + ); + aiMenuItem.hidden = !this.isAIWindowEnabled; + }, + _showBadge(notification) { let badgeStatus = this._getBadgeStatus(notification); this.menuButton.setAttribute("badge-status", badgeStatus); diff --git a/browser/locales-preview/aiWindow.ftl b/browser/locales-preview/aiWindow.ftl @@ -0,0 +1,8 @@ +# 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/. + +## Chrome + +appmenuitem-new-ai-window = + .label = New AI window diff --git a/browser/locales/jar.mn b/browser/locales/jar.mn @@ -16,6 +16,7 @@ preview/translations.ftl (../locales-preview/translations.ftl) preview/credentialChooser.ftl (../../toolkit/components/credentialmanagement/credentialChooser.ftl) browser (%browser/**/*.ftl) + preview/aiWindow.ftl (../locales-preview/aiWindow.ftl) preview/smartTabGroups.ftl (../locales-preview/smartTabGroups.ftl) preview/ipProtection.ftl (../locales-preview/ipProtection.ftl) preview/privacyPreferences.ftl (../locales-preview/privacyPreferences.ftl) diff --git a/browser/modules/BrowserWindowTracker.sys.mjs b/browser/modules/BrowserWindowTracker.sys.mjs @@ -320,6 +320,7 @@ export const BrowserWindowTracker = { openWindow({ openerWindow = undefined, private: isPrivate = false, + aiWindow = false, features = undefined, all = true, args = null, @@ -344,6 +345,9 @@ export const BrowserWindowTracker = { } else { windowFeatures += ",non-private"; } + if (aiWindow) { + windowFeatures += ",ai-window"; + } if (!args) { loadURIString ??= lazy.BrowserHandler.defaultArgs; args = Cc["@mozilla.org/supports-string;1"].createInstance(