tor-browser

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

commit 99a822346241d9938df5016f6738f0976def21c3
parent 9b6a51bf85d42e04e2dd5dd1c5d224018e8fe5da
Author: Cosmin Sabou <csabou@mozilla.com>
Date:   Fri,  9 Jan 2026 23:24:04 +0200

Revert "Bug 2001504 - Chat Assistant markdown rendering r=Mardak,ai-frontend-reviewers,Gijs" for failures on browser_aiwindow_search_button

This reverts commit 6066f300044b1dabdb17c66f2396d709eb526233.

Diffstat:
Mbrowser/components/aiwindow/ui/components/ai-chat-message/ai-chat-message.mjs | 80+++++++++++++------------------------------------------------------------------
Mbrowser/components/aiwindow/ui/components/ai-chat-message/ai-chat-message.stories.mjs | 9+--------
Mbrowser/components/aiwindow/ui/test/browser/browser_aichat_message.js | 189+++++++++++++++++--------------------------------------------------------------
Mdom/security/DOMSecurityMonitor.cpp | 1-
4 files changed, 53 insertions(+), 226 deletions(-)

diff --git a/browser/components/aiwindow/ui/components/ai-chat-message/ai-chat-message.mjs b/browser/components/aiwindow/ui/components/ai-chat-message/ai-chat-message.mjs @@ -4,12 +4,6 @@ import { html } from "chrome://global/content/vendor/lit.all.mjs"; import { MozLitElement } from "chrome://global/content/lit-utils.mjs"; -import { - defaultMarkdownParser, - DOMSerializer, -} from "chrome://browser/content/multilineeditor/prosemirror.bundle.mjs"; - -const SERIALIZER = DOMSerializer.fromSchema(defaultMarkdownParser.schema); // eslint-disable-next-line import/no-unassigned-import import "chrome://browser/content/aiwindow/components/ai-chat-search-button.mjs"; @@ -18,11 +12,12 @@ import "chrome://browser/content/aiwindow/components/ai-chat-search-button.mjs"; * A custom element for managing AI Chat Content */ export class AIChatMessage extends MozLitElement { - #lastMessage = null; - #lastMessageElement = ""; + /** + * @member {object} message - {role:"user"|"assistant" , content: string} + */ static properties = { - role: { type: String }, // "user" | "assistant" + role: { type: String }, message: { type: String }, }; @@ -52,61 +47,6 @@ export class AIChatMessage extends MozLitElement { this.dispatchEvent(e); } - /** - * Parse markdown content to HTML using ProseMirror - * - * @param {string} markdown the Markdown to parse - * @param {Element} element the element in which to insert the parsed markdown. - */ - parseMarkdown(markdown, element) { - const node = defaultMarkdownParser.parse(markdown); - const fragment = SERIALIZER.serializeFragment(node.content); - - // Convert DocumentFragment to HTML string - const container = this.ownerDocument.createElement("div"); - container.appendChild(fragment); - const containerString = container.innerHTML; - - // Sanitize the HTML string by using "setHTML" - element.setHTML(containerString); - } - - /** - * Ensure our message element is up to date. This gets called from - * render and memoizes based on `this.message` to avoid re-renders. - * - * @returns {Element} HTML element containing the parsed markdown - */ - getAssistantMessage() { - if (this.message == this.#lastMessage) { - return this.#lastMessageElement; - } - let messageElement = this.ownerDocument.createElement("div"); - messageElement.className = "message-" + this.role; - if (!this.message) { - return messageElement; - } - - this.parseMarkdown(this.message, messageElement); - - this.#lastMessage = this.message; - this.#lastMessageElement = messageElement; - - return messageElement; - } - - getUserMessage() { - return html`<div class=${"message-" + this.role}> - <!-- TODO: Parse user prompt to add any mentions pills --> - ${this.message} - </div> - <!-- TODO: update props based on assistant response --> - <ai-chat-search-button - query="Ada Lovelace" - label="Ada Lovelace" - ></ai-chat-search-button>`; - } - render() { return html` <link @@ -115,9 +55,15 @@ export class AIChatMessage extends MozLitElement { /> <article> - ${this.role === "user" - ? this.getUserMessage() - : this.getAssistantMessage()} + <div class=${"message-" + this.role}> + <!-- TODO: Add markdown parsing here --> + ${this.message} + </div> + <!-- TODO: update props based on assistant response --> + <ai-chat-search-button + query="Ada Lovelace" + label="Ada Lovelace" + ></ai-chat-search-button> </article> `; } diff --git a/browser/components/aiwindow/ui/components/ai-chat-message/ai-chat-message.stories.mjs b/browser/components/aiwindow/ui/components/ai-chat-message/ai-chat-message.stories.mjs @@ -20,7 +20,7 @@ export default { }; const Template = ({ role, content }) => html` - <ai-chat-message .role=${role} .message=${content}></ai-chat-message> + <ai-chat-message .message=${{ role, content }}></ai-chat-message> `; export const UserMessage = Template.bind({}); @@ -35,10 +35,3 @@ AssistantMessage.args = { content: "Test: I don't have access to real-time weather data, but I can help you with other tasks!", }; - -export const AssistantMessageWithMarkdown = Template.bind({}); -AssistantMessageWithMarkdown.args = { - role: "assistant", - content: - "Here's some **bold text** and *italic text*:\n\n- Item 1\n- Item 2\n\n```javascript\nconsole.log('code block');\n```", -}; diff --git a/browser/components/aiwindow/ui/test/browser/browser_aichat_message.js b/browser/components/aiwindow/ui/test/browser/browser_aichat_message.js @@ -4,170 +4,59 @@ "use strict"; /** - * Basic rendering + markdown/sanitization test for <ai-chat-message>. - * - * Notes: - * - Uses a content-side readiness gate (readyState polling) instead of - * BrowserTestUtils.browserLoaded to avoid missing the load event. - * - Avoids Lit's updateComplete because MozLitElement variants may not expose it - * or it may never resolve in this harness. + * Test ai-chat-message custom element basic rendering */ add_task(async function test_ai_chat_message_rendering() { await SpecialPowers.pushPrefEnv({ set: [["browser.aiwindow.enabled", true]], }); - const tab = await BrowserTestUtils.openNewForegroundTab( - gBrowser, - "about:aichatcontent" - ); - const browser = tab.linkedBrowser; - - try { - // Wait for content to be fully loaded - await SpecialPowers.spawn(browser, [], async () => { - if (content.document.readyState !== "complete") { - await ContentTaskUtils.waitForEvent(content, "load"); - } - }); - + // Use about:aichatcontent which already loads the components properly + await BrowserTestUtils.withNewTab("about:aichatcontent", async browser => { await SpecialPowers.spawn(browser, [], async () => { - const doc = content.document; + await content.customElements.whenDefined("ai-chat-message"); - function sleep(ms) { - return new content.Promise(resolve => content.setTimeout(resolve, ms)); - } + // Create a test ai-chat-message element + const messageElement = content.document.createElement("ai-chat-message"); + content.document.body.appendChild(messageElement); - async function withTimeout(promise, ms, label) { - return content.Promise.race([ - promise, - new content.Promise((_, reject) => - content.setTimeout( - () => reject(new Error(`Timeout (${ms}ms): ${label}`)), - ms - ) - ), - ]); - } + Assert.ok(messageElement, "ai-chat-message element should be created"); - async function waitFor(fn, msg, maxTicks = 200) { - for (let i = 0; i < maxTicks; i++) { - try { - if (fn()) { - return; - } - } catch (_) { - // Keep looping; DOM may not be ready yet. - } - await sleep(0); - } - throw new Error(`Timed out waiting: ${msg}`); - } + // Test setting a user message + messageElement.message = { role: "user", content: "Test user message" }; + await messageElement.updateComplete; - function root(el) { - return el.shadowRoot ?? el; - } - - function setRoleAndMessage(el, role, message) { - // Set both property + attribute to avoid any reflection differences. - el.role = role; - el.setAttribute("role", role); - - el.message = message; - el.setAttribute("message", message); + const messageDiv = + messageElement.renderRoot?.querySelector(".message-user"); + if (messageDiv) { + Assert.ok(messageDiv, "User message div should be rendered"); + Assert.ok( + messageDiv.textContent.includes("Test user message"), + "User message content should be present" + ); } - // Ensure the custom element is registered. If the module failed to load, - // this will fail fast instead of hanging until harness teardown. - await withTimeout( - content.customElements.whenDefined("ai-chat-message"), - 5000, - "customElements.whenDefined('ai-chat-message')" - ); - - const el = doc.createElement("ai-chat-message"); - doc.body.appendChild(el); - - Assert.ok(el, "ai-chat-message element should be created"); - - // --- User message --- - setRoleAndMessage(el, "user", "Test user message"); - - await waitFor(() => { - const div = root(el).querySelector(".message-user"); - return div && div.textContent.includes("Test user message"); - }, "User message should render with expected text"); - - const userDiv = root(el).querySelector(".message-user"); - Assert.ok(userDiv, "User message div should exist"); - Assert.ok( - userDiv.textContent.includes("Test user message"), - `User message content should be present (got: "${userDiv.textContent}")` - ); - - // --- Assistant message --- - setRoleAndMessage(el, "assistant", "Test AI response"); - - await waitFor(() => { - const div = root(el).querySelector(".message-assistant"); - return div && div.textContent.includes("Test AI response"); - }, "Assistant message should render with expected text"); - - let assistantDiv = root(el).querySelector(".message-assistant"); - Assert.ok(assistantDiv, "Assistant message div should exist"); - Assert.ok( - assistantDiv.textContent.includes("Test AI response"), - `Assistant message content should be present (got: "${assistantDiv.textContent}")` - ); - - // --- Markdown parsing (positive) --- - // Verifies that markdown like "**Bold** and *italic*" becomes markup - // (<strong> and <em> elements) rather than literal asterisks. - setRoleAndMessage(el, "assistant", "**Bold** and *italic* text"); - - await waitFor(() => { - const div = root(el).querySelector(".message-assistant"); - return div && div.querySelector("strong") && div.querySelector("em"); - }, "Markdown should produce <strong> and <em>"); - - assistantDiv = root(el).querySelector(".message-assistant"); - Assert.ok( - assistantDiv.querySelector("strong"), - `Expected <strong> in: ${assistantDiv.innerHTML}` - ); - Assert.ok( - assistantDiv.querySelector("em"), - `Expected <em> in: ${assistantDiv.innerHTML}` - ); - - // --- Negative: raw HTML should not become markup --- - // Verifies sanitization / safe rendering: raw HTML should not be - // interpreted as elements, but should remain visible as text. - setRoleAndMessage(el, "assistant", "<b>not bolded</b>"); - - await waitFor(() => { - const div = root(el).querySelector(".message-assistant"); - return ( - div && - !div.querySelector("b") && - div.textContent.includes("not bolded") + // Test setting an assistant message + messageElement.message = { + role: "assistant", + content: "Test AI response", + }; + await messageElement.updateComplete; + + const assistantDiv = + messageElement.renderRoot?.querySelector(".message-assistant"); + if (assistantDiv) { + Assert.ok(assistantDiv, "Assistant message div should be rendered"); + Assert.ok( + assistantDiv.textContent.includes("Test AI response"), + "Assistant message content should be present" ); - }, "Raw HTML should not become a <b> element, but text should remain"); - - assistantDiv = root(el).querySelector(".message-assistant"); - Assert.ok( - !assistantDiv.querySelector("b"), - `Should not contain real <b>: ${assistantDiv.innerHTML}` - ); - Assert.ok( - assistantDiv.textContent.includes("not bolded"), - `Raw HTML content should still be visible as text (got: "${assistantDiv.textContent}")` - ); + } - el.remove(); + // Clean up + messageElement.remove(); }); - } finally { - BrowserTestUtils.removeTab(tab); - await SpecialPowers.popPrefEnv(); - } + }); + + await SpecialPowers.popPrefEnv(); }); diff --git a/dom/security/DOMSecurityMonitor.cpp b/dom/security/DOMSecurityMonitor.cpp @@ -70,7 +70,6 @@ void DOMSecurityMonitor::AuditParsingOfHTMLXMLFragments( "resource://devtools/client/shared/widgets/Spectrum.js"_ns, "resource://gre/modules/narrate/VoiceSelect.sys.mjs"_ns, "chrome://global/content/vendor/react-dom.js"_ns, - "chrome://browser/content/aiwindow/components/ai-chat-message.mjs"_ns, // ------------------------------------------------------------------ // test pages // ------------------------------------------------------------------