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:
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