commit fb84c9bccbfc34b91613ff9f3c955a887c7e7196
parent a46cfb4e6bad6756c9e9f0f1f88ac1f7d11caa11
Author: mailelucks <maile.lucks@gmail.com>
Date: Tue, 30 Dec 2025 16:42:55 +0000
Bug 2004464 - Content transparency for AI Window - r=Gijs,desktop-theme-reviewers,ai-frontend-reviewers,emilio,tabbrowser-reviewers,sthompson
Differential Revision: https://phabricator.services.mozilla.com/D276567
Diffstat:
12 files changed, 325 insertions(+), 7 deletions(-)
diff --git a/browser/components/aiwindow/ui/components/ai-window/ai-window.mjs b/browser/components/aiwindow/ui/components/ai-window/ai-window.mjs
@@ -49,6 +49,7 @@ export class AIWindow extends MozLitElement {
browser.setAttribute("maychangeremoteness", "true");
browser.setAttribute("disableglobalhistory", "true");
browser.setAttribute("src", "about:aichatcontent");
+ browser.setAttribute("transparent", true);
const container = this.renderRoot.querySelector("#browser-container");
container.appendChild(browser);
diff --git a/browser/components/aiwindow/ui/content/ai-window-content.css b/browser/components/aiwindow/ui/content/ai-window-content.css
@@ -0,0 +1,11 @@
+/* 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/. */
+
+:root {
+ /* override --background-color-canvas from tokens-shared.css */
+ background-color: transparent;
+
+ --gradient-button: linear-gradient(97deg, #8341ca -31.39%, #656fff 90.52%);
+ --gradient-selected-border: linear-gradient(117deg, #321bfd -17.87%, #cf30e2 52.93%, #f90 89.02%, #f5c451 109.44%);
+}
diff --git a/browser/components/aiwindow/ui/content/aiChatContent.html b/browser/components/aiwindow/ui/content/aiChatContent.html
@@ -15,6 +15,10 @@
<!-- TODO : Add localization preview -->
<title>AI Chat Content</title>
<link rel="stylesheet" href="chrome://global/skin/in-content/common.css" />
+ <link
+ rel="stylesheet"
+ href="chrome://browser/content/aiwindow/ai-window-content.css"
+ />
<script src="chrome://browser/content/contentTheme.js"></script>
<script
type="module"
diff --git a/browser/components/aiwindow/ui/content/firstrun.css b/browser/components/aiwindow/ui/content/firstrun.css
@@ -1,9 +1,7 @@
/* 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/. */
-:root {
- --background-color-canvas: transparent;
-}
+
@keyframes springUp {
from {
opacity: 0;
@@ -21,9 +19,6 @@
--tile-border-color: #f1e7f8;
--shadow-color: rgb(59 34 121);
- --gradient-button: linear-gradient(97deg, #8341ca -31.39%, #656fff 90.52%);
- --gradient-selected-border: linear-gradient(117deg, #321bfd -17.87%, #cf30e2 52.93%, #f90 89.02%, #f5c451 109.44%);
-
.main-content,
.section-main,
.dialog-initial,
@@ -74,6 +69,7 @@
margin: 0 auto;
border: var(--border-width) solid var(--border-color-transparent);
border-radius: var(--border-radius-medium);
+ /* stylelint-disable-next-line stylelint-plugin-mozilla/use-design-tokens -- gradient defined in ai-window-content.css */
background: var(--gradient-button);
color: var(--button-text-color-primary);
text-align: center;
@@ -142,6 +138,7 @@
&:has(.selected) {
border: calc(var(--border-width) * 2) solid var(--border-color-transparent);
+ /* stylelint-disable-next-line stylelint-plugin-mozilla/use-design-tokens -- gradient defined in ai-window-content.css */
background:
linear-gradient(#fff, #fff) padding-box,
var(--gradient-selected-border) border-box;
diff --git a/browser/components/aiwindow/ui/content/firstrun.html b/browser/components/aiwindow/ui/content/firstrun.html
@@ -24,6 +24,10 @@
/>
<link
rel="stylesheet"
+ href="chrome://browser/content/aiwindow/ai-window-content.css"
+ />
+ <link
+ rel="stylesheet"
href="chrome://browser/content/aiwindow/firstrun.css"
/>
<link rel="localization" href="preview/aiWindow.ftl" />
diff --git a/browser/components/aiwindow/ui/jar.mn b/browser/components/aiwindow/ui/jar.mn
@@ -12,6 +12,7 @@ browser.jar:
content/browser/aiwindow/components/ai-chat-message.css (components/ai-chat-message/ai-chat-message.css)
content/browser/aiwindow/components/ai-window.mjs (components/ai-window/ai-window.mjs)
content/browser/aiwindow/components/ai-window.css (components/ai-window/ai-window.css)
+ content/browser/aiwindow/ai-window-content.css (content/ai-window-content.css)
content/browser/aiwindow/components/input-cta.css (components/input-cta/input-cta.css)
content/browser/aiwindow/components/input-cta.mjs (components/input-cta/input-cta.mjs)
content/browser/aiwindow/assets/model-choice-1.svg (assets/model-choice-1.svg)
diff --git a/browser/components/aiwindow/ui/modules/AIWindow.sys.mjs b/browser/components/aiwindow/ui/modules/AIWindow.sys.mjs
@@ -6,6 +6,7 @@
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
const AIWINDOW_URL = "chrome://browser/content/aiwindow/aiWindow.html";
+const AIWINDOW_URI = Services.io.newURI(AIWINDOW_URL);
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
ChatStore:
@@ -121,6 +122,16 @@ export const AIWindow = {
},
/**
+ * Is AI Window content page active
+ *
+ * @param {nsIURI} uri current URI
+ * @returns {boolean} whether AI Window content page is active
+ */
+ isAIWindowContentPage(uri) {
+ return AIWINDOW_URI.equalsExceptRef(uri);
+ },
+
+ /**
* Adds the AI Window app menu options
*
* @param {Event} event - History menu click event
diff --git a/browser/components/aiwindow/ui/test/browser/browser.toml b/browser/components/aiwindow/ui/test/browser/browser.toml
@@ -1,4 +1,7 @@
[DEFAULT]
+support-files = [
+ "head.js",
+]
["browser_actor_user_prompt.js"]
@@ -10,6 +13,8 @@
["browser_aiwindow_integration.js"]
+["browser_aiwindow_transparency.js"]
+
["browser_open_aiwindow.js"]
["browser_sidebar_aiwindow.js"]
diff --git a/browser/components/aiwindow/ui/test/browser/browser_aiwindow_transparency.js b/browser/components/aiwindow/ui/test/browser/browser_aiwindow_transparency.js
@@ -0,0 +1,232 @@
+/* Any copyright is dedicated to the Public Domain.
+ https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Checks if browser has transparent attribute
+ *
+ * @param {XULBrowser} browser
+ * @returns {boolean}
+ */
+function isBrowserTransparent(browser) {
+ return browser.hasAttribute("transparent");
+}
+
+/**
+ * Creates a data URL for a page with transparent styling
+ *
+ * @returns {string}
+ */
+function getTransparentPageURL() {
+ return `data:text/html,<!DOCTYPE html>
+ <html style="background: transparent;">
+ <head><title>Transparent Test Page</title></head>
+ <body style="background: transparent; color: white;">
+ <h1>This page tries to be transparent</h1>
+ </body>
+ </html>`;
+}
+
+let gAIWindow;
+let gReusableTab;
+
+add_setup(async function () {
+ gAIWindow = await openAIWindow();
+ registerCleanupFunction(async () => {
+ await BrowserTestUtils.closeWindow(gAIWindow);
+ });
+});
+
+add_task(async function test_transparency_on_new_window() {
+ const browser = gAIWindow.gBrowser.selectedBrowser;
+
+ Assert.ok(
+ isBrowserTransparent(browser),
+ "Browser should be transparent on new AI window"
+ );
+});
+
+add_task(async function test_transparency_on_new_tab() {
+ const newTab = await BrowserTestUtils.openNewForegroundTab(
+ gAIWindow.gBrowser,
+ AIWINDOW_URL
+ );
+ const newBrowser = gAIWindow.gBrowser.getBrowserForTab(newTab);
+
+ Assert.ok(
+ isBrowserTransparent(newBrowser),
+ "Browser should be transparent on new AI window tab"
+ );
+
+ Assert.equal(
+ newBrowser.currentURI.spec,
+ AIWINDOW_URL,
+ "New tab should be on AI window URL"
+ );
+
+ gAIWindow.gBrowser.removeTab(newTab);
+});
+
+add_task(async function test_transparency_removed_on_navigation() {
+ gReusableTab = await BrowserTestUtils.openNewForegroundTab(
+ gAIWindow.gBrowser,
+ AIWINDOW_URL
+ );
+ const browser = gReusableTab.linkedBrowser;
+
+ Assert.ok(
+ isBrowserTransparent(browser),
+ "Browser should be transparent on new AI window tab"
+ );
+
+ const loaded = BrowserTestUtils.browserLoaded(
+ browser,
+ false,
+ "https://example.com/"
+ );
+ BrowserTestUtils.startLoadingURIString(browser, "https://example.com/");
+ await loaded;
+
+ Assert.equal(
+ browser.currentURI.spec,
+ "https://example.com/",
+ "Browser should be on example.com after navigation"
+ );
+
+ Assert.ok(
+ !isBrowserTransparent(browser),
+ "Browser should not have transparent attribute after navigation"
+ );
+});
+
+add_task(async function test_transparency_blocked_on_transparent_page() {
+ const browser = gReusableTab.linkedBrowser;
+
+ let loaded = BrowserTestUtils.browserLoaded(browser, false, AIWINDOW_URL);
+ BrowserTestUtils.startLoadingURIString(browser, AIWINDOW_URL);
+ await loaded;
+
+ Assert.ok(
+ isBrowserTransparent(browser),
+ "Browser should be transparent on new AI window tab"
+ );
+
+ const transparentPageURL = getTransparentPageURL();
+ loaded = BrowserTestUtils.browserLoaded(browser, false, url =>
+ url.startsWith("data:")
+ );
+ BrowserTestUtils.startLoadingURIString(browser, transparentPageURL);
+ await loaded;
+
+ await BrowserTestUtils.waitForMutationCondition(
+ browser,
+ { attributes: true },
+ () => !isBrowserTransparent(browser)
+ );
+
+ Assert.ok(
+ !isBrowserTransparent(browser),
+ "Browser does not have transparent attribute"
+ );
+
+ await SpecialPowers.spawn(browser, [], async () => {
+ const bodyStyle = content.window.getComputedStyle(content.document.body);
+ const htmlStyle = content.window.getComputedStyle(
+ content.document.documentElement
+ );
+
+ Assert.equal(
+ bodyStyle.backgroundColor,
+ "rgba(0, 0, 0, 0)",
+ "Body background should be transparent"
+ );
+ Assert.equal(
+ htmlStyle.backgroundColor,
+ "rgba(0, 0, 0, 0)",
+ "HTML background should be transparent"
+ );
+ });
+
+ const canvas = gAIWindow.document.createElementNS(
+ "http://www.w3.org/1999/xhtml",
+ "canvas"
+ );
+ const rect = new DOMRect(0, 0, 100, 100);
+ const snapshot =
+ await browser.browsingContext.currentWindowGlobal.drawSnapshot(
+ rect,
+ 1,
+ "rgb(255, 255, 255)"
+ );
+
+ canvas.width = rect.width;
+ canvas.height = rect.height;
+ const ctx = canvas.getContext("2d");
+ ctx.drawImage(snapshot, 0, 0);
+ snapshot.close();
+
+ const imageData = ctx.getImageData(50, 50, 1, 1);
+ const [r, g, b, a] = imageData.data;
+
+ Assert.equal(
+ a,
+ 255,
+ "Background should be fully opaque (not system transparent)"
+ );
+ Assert.equal(
+ r,
+ 255,
+ "Background red channel should be 255 (white background)"
+ );
+ Assert.equal(
+ g,
+ 255,
+ "Background green channel should be 255 (white background)"
+ );
+ Assert.equal(
+ b,
+ 255,
+ "Background blue channel should be 255 (white background)"
+ );
+});
+
+add_task(async function test_transparency_restored_on_navigation_back() {
+ const browser = gReusableTab.linkedBrowser;
+
+ let loaded = BrowserTestUtils.browserLoaded(
+ browser,
+ false,
+ "https://example.com/"
+ );
+ BrowserTestUtils.startLoadingURIString(browser, "https://example.com/");
+ await loaded;
+
+ Assert.equal(
+ browser.currentURI.spec,
+ "https://example.com/",
+ "Browser should be on example.com"
+ );
+
+ Assert.ok(
+ !isBrowserTransparent(browser),
+ "Browser should not be transparent on example.com"
+ );
+
+ loaded = BrowserTestUtils.browserLoaded(browser, false, AIWINDOW_URL);
+ BrowserTestUtils.startLoadingURIString(browser, AIWINDOW_URL);
+ await loaded;
+
+ Assert.ok(
+ isBrowserTransparent(browser),
+ "Browser should be transparent again after returning to AI window URL"
+ );
+
+ Assert.equal(
+ browser.currentURI.spec,
+ AIWINDOW_URL,
+ "Browser should be back on AI window URL"
+ );
+
+ gAIWindow.gBrowser.removeTab(gReusableTab);
+});
diff --git a/browser/components/aiwindow/ui/test/browser/head.js b/browser/components/aiwindow/ui/test/browser/head.js
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+ * https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const AIWINDOW_URL = "chrome://browser/content/aiwindow/aiWindow.html";
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.aiwindow.enabled", true]],
+ });
+});
+
+/**
+ * Opens a new AI Window
+ *
+ * @returns {Promise<Window>}
+ */
+async function openAIWindow() {
+ const win = await BrowserTestUtils.openNewBrowserWindow({ aiWindow: true });
+ await BrowserTestUtils.waitForMutationCondition(
+ win.document.documentElement,
+ { attributes: true },
+ () => win.document.documentElement.hasAttribute("ai-window")
+ );
+ return win;
+}
diff --git a/browser/components/tabbrowser/NewTabPagePreloading.sys.mjs b/browser/components/tabbrowser/NewTabPagePreloading.sys.mjs
@@ -13,6 +13,8 @@ const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
AboutNewTab: "resource:///modules/AboutNewTab.sys.mjs",
+ AIWindow:
+ "moz-src:///browser/components/aiwindow/ui/modules/AIWindow.sys.mjs",
BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
E10SUtils: "resource://gre/modules/E10SUtils.sys.mjs",
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
@@ -77,11 +79,13 @@ export let NewTabPagePreloading = {
*/
_adoptBrowserFromOtherWindow(window) {
let winPrivate = lazy.PrivateBrowsingUtils.isWindowPrivate(window);
+ let winAIWindow = lazy.AIWindow.isAIWindowActive(window);
// Grab the least-recently-focused window with a preloaded browser:
let oldWin = lazy.BrowserWindowTracker.orderedWindows
.filter(w => {
return (
winPrivate == lazy.PrivateBrowsingUtils.isWindowPrivate(w) &&
+ winAIWindow == lazy.AIWindow.isAIWindowActive(w) &&
w.gBrowser &&
w.gBrowser.preloadedBrowser
);
diff --git a/browser/components/tabbrowser/content/tabbrowser.js b/browser/components/tabbrowser/content/tabbrowser.js
@@ -626,6 +626,19 @@
browser
);
+ if (AIWindow.isAIWindowActive(window)) {
+ let uriToLoad = gBrowserInit.uriToLoadPromise;
+ let firstURI = Array.isArray(uriToLoad) ? uriToLoad[0] : uriToLoad;
+
+ if (!this._allowTransparentBrowser) {
+ browser.toggleAttribute(
+ "transparent",
+ !firstURI ||
+ AIWindow.isAIWindowContentPage(Services.io.newURI(firstURI))
+ );
+ }
+ }
+
let uniqueId = this._generateUniquePanelID();
let panel = this.getPanel(browser);
panel.id = uniqueId;
@@ -2437,7 +2450,7 @@
b.setAttribute("name", name);
}
- if (this._allowTransparentBrowser) {
+ if (AIWindow.isAIWindowActive(window) || this._allowTransparentBrowser) {
b.setAttribute("transparent", "true");
}
@@ -9062,6 +9075,14 @@
) {
this.mBrowser.originalURI = aRequest.originalURI;
}
+
+ if (!this._allowTransparentBrowser) {
+ this.mBrowser.toggleAttribute(
+ "transparent",
+ AIWindow.isAIWindowActive(window) &&
+ AIWindow.isAIWindowContentPage(aLocation)
+ );
+ }
}
let userContextId = this.mBrowser.getAttribute("usercontextid") || 0;