commit 62be651d7a1eb3b66cc7cef83ab627a0f8d8a7c9 parent ed7fc6f9cc112708919780dbd9915cfd29891b12 Author: Nick Grato <ngrato@gmail.com> Date: Wed, 10 Dec 2025 21:45:27 +0000 Bug 2004148 - Set up AI Window Actors r=ai-frontend-reviewers,Mardak Setting up the pipeline for parent child actors for AI Window. Also Adding parent lit component for AI window. This will be added to the aiWindow.html when sibling patch lands. Differential Revision: https://phabricator.services.mozilla.com/D275279 Diffstat:
13 files changed, 314 insertions(+), 7 deletions(-)
diff --git a/browser/components/DesktopActorRegistry.sys.mjs b/browser/components/DesktopActorRegistry.sys.mjs @@ -218,6 +218,20 @@ let JSWINDOWACTORS = { enablePreference: "browser.aboutwelcome.enabled", }, + AIChatContent: { + parent: { + esModuleURI: + "moz-src:///browser/components/aiwindow/ui/actors/AIChatContentParent.sys.mjs", + }, + child: { + esModuleURI: + "moz-src:///browser/components/aiwindow/ui/actors/AIChatContentChild.sys.mjs", + }, + allFrames: true, + matches: ["about:aichatcontent"], + enablePreference: "browser.aiwindow.enabled", + }, + BackupUI: { parent: { esModuleURI: "resource:///actors/BackupUIParent.sys.mjs", diff --git a/browser/components/aiwindow/ui/actors/AIChatContentChild.sys.mjs b/browser/components/aiwindow/ui/actors/AIChatContentChild.sys.mjs @@ -0,0 +1,34 @@ +/* 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/. */ + +const lazy = {}; +ChromeUtils.defineESModuleGetters(lazy, {}); + +/** + * Represents a child actor for getting page data from the browser. + */ +export class AIChatContentChild extends JSWindowActorChild { + async receiveMessage(message) { + switch (message.name) { + case "AIChatContent:DispatchAIResponse": + return this.dispatchAIResponseToChatContent(message.data.response); + } + return undefined; + } + + async dispatchAIResponseToChatContent(response) { + try { + const chatContent = this.document.querySelector("ai-chat-content"); + const event = new this.contentWindow.CustomEvent("ai-response", { + detail: response, + bubbles: true, + }); + chatContent.dispatchEvent(event); + return false; + } catch (error) { + console.error("Error dispatching AI response to chat content:", error); + return false; + } + } +} diff --git a/browser/components/aiwindow/ui/actors/AIChatContentParent.sys.mjs b/browser/components/aiwindow/ui/actors/AIChatContentParent.sys.mjs @@ -0,0 +1,14 @@ +/* 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/. */ + +/** + * JSWindowActor to pass data between AIChatContent singleton and content pages. + */ +export class AIChatContentParent extends JSWindowActorParent { + async dispatchAIResponse(response) { + return this.sendQuery("AIChatContent:DispatchAIResponse", { + response, + }); + } +} diff --git a/browser/components/aiwindow/ui/components/ai-chat-content/ai-chat-content.mjs b/browser/components/aiwindow/ui/components/ai-chat-content/ai-chat-content.mjs @@ -0,0 +1,70 @@ +/* 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/. */ + +import { html } from "chrome://global/content/vendor/lit.all.mjs"; +import { MozLitElement } from "chrome://global/content/lit-utils.mjs"; + +/** + * A custom element for managing AI Chat Content + */ +export class AIChatContent extends MozLitElement { + static properties = { + messages: { type: Array }, + }; + + constructor() { + super(); + this.messages = []; + } + + connectedCallback() { + super.connectedCallback(); + + // Listen for ai-response events as fallback + this.addEventListener("ai-response", this.handleAIResponseEvent.bind(this)); + } + + /** + * Add an AI response to the chat + * + * @param {string} response - The AI response text + */ + addAIResponse(response) { + this.messages = [...this.messages, { type: "ai", content: response }]; + this.requestUpdate(); + } + + /** + * Handle AI response events + * + * @param {CustomEvent} event - The custom event containing the response + */ + handleAIResponseEvent(event) { + console.warn("Received AI response event:", event); + // TODO - Use Markdown to render rich text responses + this.addAIResponse(event.detail); + } + + render() { + return html` + <div> + <div> + ${this.messages.map( + (message, index) => html` + <div key=${index}> + <strong>${message.type === "ai" ? "AI" : "User"}:</strong> + ${message.content} + </div> + ` + )} + ${this.messages.length === 0 + ? html`<div>Chat will appear here...</div>` + : ""} + </div> + </div> + `; + } +} + +customElements.define("ai-chat-content", AIChatContent); diff --git a/browser/components/aiwindow/ui/components/ai-window/ai-window.css b/browser/components/aiwindow/ui/components/ai-window/ai-window.css @@ -0,0 +1,15 @@ +/* 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/. */ + +:host { + display: flex; + flex-direction: column; +} + +#browser-container, +#aichat-browser { + display: flex; + flex: 1; + min-height: var(--size-item-xlarge); +} diff --git a/browser/components/aiwindow/ui/components/ai-window/ai-window.mjs b/browser/components/aiwindow/ui/components/ai-window/ai-window.mjs @@ -0,0 +1,99 @@ +/* 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/. */ + +import { html } from "chrome://global/content/vendor/lit.all.mjs"; +import { MozLitElement } from "chrome://global/content/lit-utils.mjs"; + +/** + * A custom element for managing AI Window + */ +export class AIWindow extends MozLitElement { + static properties = {}; + + constructor() { + super(); + this._browser = null; + } + + connectedCallback() { + super.connectedCallback(); + } + + firstUpdated() { + // Create a real XUL <browser> element from the chrome document + const doc = this.ownerDocument; // browser.xhtml + const browser = doc.createXULElement("browser"); + + browser.setAttribute("id", "aichat-browser"); + browser.setAttribute("type", "content"); + browser.setAttribute("maychangeremoteness", "true"); + browser.setAttribute("disableglobalhistory", "true"); + browser.setAttribute("src", "about:aichatcontent"); + + const container = this.renderRoot.querySelector("#browser-container"); + container.appendChild(browser); + + this._browser = browser; + } + + async _submitUserPrompt() { + const mockPrompt = "This is a test prompt, how are you?"; + + // Call AI service directly from this instance + const response = this._fetchAIResponse(mockPrompt); + + // Dispatch directly to our browser's actor + await this._dispatchAIResponseToBrowser(response); + } + + _fetchAIResponse(userPrompt) { + // TODO - Add actual call to LLM service here + const mockResponse = `this is a response to ${userPrompt}`; + return mockResponse; + } + + async _dispatchAIResponseToBrowser(response) { + if (!this._browser) { + console.warn("AI browser not set, cannot dispatch response"); + return null; + } + + const windowGlobal = this._browser.browsingContext?.currentWindowGlobal; + + if (!windowGlobal) { + console.warn("No window global found for AI browser"); + return null; + } + + try { + const actor = windowGlobal.getActor("AIChatContent"); + return await actor.dispatchAIResponse(response); + } catch (error) { + console.error("Failed to dispatch AI response:", error); + return null; + } + } + + _handleSubmit() { + this._submitUserPrompt(); + } + + render() { + return html` + <link + rel="stylesheet" + href="chrome://browser/content/aiwindow/components/ai-window.css" + /> + <div> + <div id="browser-container"></div> + <!-- TODO : Remove place holder submit button, prompt will come from ai-input --> + <moz-button type="primary" size="small" @click=${this._handleSubmit}> + Submit mock prompt + </moz-button> + </div> + `; + } +} + +customElements.define("ai-window", AIWindow); diff --git a/browser/components/aiwindow/ui/content/aiChatContent.html b/browser/components/aiwindow/ui/content/aiChatContent.html @@ -16,10 +16,13 @@ <title>AI Chat Content</title> <link rel="stylesheet" href="chrome://global/skin/in-content/common.css" /> <script src="chrome://browser/content/contentTheme.js"></script> - <!-- TODO: load appropriate component modules - --> + <script + type="module" + src="chrome://browser/content/aiwindow/components/ai-chat-content.mjs" + ></script> </head> <body id="ai-window-wrapper"> Placeholder for AI Chat Content + <ai-chat-content></ai-chat-content> </body> </html> diff --git a/browser/components/aiwindow/ui/content/aiWindow.html b/browser/components/aiwindow/ui/content/aiWindow.html @@ -13,11 +13,21 @@ <meta name="color-scheme" content="light dark" /> <!-- TODO : Add localization preview --> <title>AI Window</title> - <!-- TODO: load appropriate component modules - "chrome://browser/content/aiwindow/components/input-cta.mjs" - --> + <link rel="stylesheet" href="chrome://global/skin/in-content/common.css" /> + <link + rel="stylesheet" + href="chrome://browser/content/sidebar/sidebar.css" + /> + <script + type="module" + src="chrome://browser/content/aiwindow/components/ai-window.mjs" + ></script> + <script + type="module" + src="chrome://browser/content/aiwindow/components/input-cta.mjs" + ></script> </head> <body> - Placeholder for AI Window + <ai-window></ai-window> </body> </html> diff --git a/browser/components/aiwindow/ui/jar.mn b/browser/components/aiwindow/ui/jar.mn @@ -6,5 +6,8 @@ browser.jar: content/browser/aiwindow/aiChatContent.html (content/aiChatContent.html) content/browser/aiwindow/aiWindow.html (content/aiWindow.html) content/browser/aiwindow/assets/input-cta-arrow-icon.svg (assets/input-cta-arrow-icon.svg) + content/browser/aiwindow/components/ai-chat-content.mjs (components/ai-chat-content/ai-chat-content.mjs) + 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/components/input-cta.css (components/input-cta/input-cta.css) content/browser/aiwindow/components/input-cta.mjs (components/input-cta/input-cta.mjs) diff --git a/browser/components/aiwindow/ui/moz.build b/browser/components/aiwindow/ui/moz.build @@ -8,6 +8,8 @@ with Files("**"): BROWSER_CHROME_MANIFESTS += ["test/browser/browser.toml"] MOZ_SRC_FILES += [ + "actors/AIChatContentChild.sys.mjs", + "actors/AIChatContentParent.sys.mjs", "modules/AIWindow.sys.mjs", ] diff --git a/browser/components/aiwindow/ui/test/browser/browser.toml b/browser/components/aiwindow/ui/test/browser/browser.toml @@ -1,3 +1,5 @@ [DEFAULT] +["browser_aichat_content_actors.js"] + ["browser_open_aiwindow.js"] diff --git a/browser/components/aiwindow/ui/test/browser/browser_aichat_content_actors.js b/browser/components/aiwindow/ui/test/browser/browser_aichat_content_actors.js @@ -0,0 +1,41 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test that AIChatContent actor gets registered when preference is enabled + */ +add_task(async function test_aichat_actor_registration() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.aiwindow.enabled", true]], + }); + + await BrowserTestUtils.withNewTab("about:aichatcontent", async browser => { + const actor = + browser.browsingContext.currentWindowGlobal.getActor("AIChatContent"); + Assert.ok(actor, "AIChatContent actor should be registered"); + }); + + await SpecialPowers.popPrefEnv(); +}); + +/** + * Test that AIChatContent actor is not available when preference is disabled + */ +add_task(async function test_aichat_actor_disabled() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.aiwindow.enabled", false]], + }); + + await BrowserTestUtils.withNewTab("about:aichatcontent", async browser => { + Assert.throws( + () => + browser.browsingContext.currentWindowGlobal.getActor("AIChatContent"), + /NotFoundError/, + "AIChatContent actor should not be available when disabled" + ); + }); + + await SpecialPowers.popPrefEnv(); +}); diff --git a/browser/components/genai/content/smart-assist.mjs b/browser/components/genai/content/smart-assist.mjs @@ -298,7 +298,7 @@ export class SmartAssist extends MozLitElement { browser.setAttribute( "src", - "chrome://browser/content/genai/smartAssist.html" + "chrome://browser/content/aiwindow/aiWindow.html" ); stack.appendChild(browser);