tor-browser

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

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:
Mbrowser/components/aiwindow/ui/components/ai-window/ai-window.mjs | 1+
Abrowser/components/aiwindow/ui/content/ai-window-content.css | 11+++++++++++
Mbrowser/components/aiwindow/ui/content/aiChatContent.html | 4++++
Mbrowser/components/aiwindow/ui/content/firstrun.css | 9+++------
Mbrowser/components/aiwindow/ui/content/firstrun.html | 4++++
Mbrowser/components/aiwindow/ui/jar.mn | 1+
Mbrowser/components/aiwindow/ui/modules/AIWindow.sys.mjs | 11+++++++++++
Mbrowser/components/aiwindow/ui/test/browser/browser.toml | 5+++++
Abrowser/components/aiwindow/ui/test/browser/browser_aiwindow_transparency.js | 232+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Abrowser/components/aiwindow/ui/test/browser/head.js | 27+++++++++++++++++++++++++++
Mbrowser/components/tabbrowser/NewTabPagePreloading.sys.mjs | 4++++
Mbrowser/components/tabbrowser/content/tabbrowser.js | 23++++++++++++++++++++++-
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;