commit 658e8ba3a437aaec4be1e9092f8a35ac25a31f2e
parent 3aba5f11a2edf9d1f1ad6a42d374ddd55995c5e7
Author: unifolia <jlewis@mozilla.com>
Date: Mon, 1 Dec 2025 21:12:53 +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:
13 files changed, 185 insertions(+), 6 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/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(