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