commit e21bc5fe7644a6fe739ec716da92e48470e950d2
parent a5505b77d4a9355b2009e40039d8e7817dd08c3f
Author: Yubin Jamora <yjamora@mozilla.com>
Date: Fri, 24 Oct 2025 01:00:22 +0000
Bug 1980456 - exploring to resolve chatgpt auto submit issue r=Mardak,firefox-ai-ml-reviewers
Differential Revision: https://phabricator.services.mozilla.com/D268582
Diffstat:
5 files changed, 267 insertions(+), 9 deletions(-)
diff --git a/browser/components/genai/GenAI.sys.mjs b/browser/components/genai/GenAI.sys.mjs
@@ -131,6 +131,7 @@ export const GenAI = {
linksId: "genai-settings-chat-claude-links",
maxLength: 14150,
name: "Anthropic Claude",
+ supportAutoSubmit: true,
tooltipId: "genai-onboarding-claude-tooltip",
},
],
@@ -144,6 +145,7 @@ export const GenAI = {
linksId: "genai-settings-chat-chatgpt-links",
maxLength: 9350,
name: "ChatGPT",
+ supportAutoSubmit: true,
tooltipId: "genai-onboarding-chatgpt-tooltip",
},
],
@@ -1024,6 +1026,91 @@ export const GenAI = {
},
/**
+ * Set up automatic prompt submission for ChatGPT and Claude
+ *
+ * @param {Browser} browser - current browser
+ * @param {string} prompt - prompt text
+ * @param {object} context of how the prompt should be handled
+ */
+ setupAutoSubmit(browser, prompt, context) {
+ const sendAutoSubmit = (br, promptText) => {
+ const wgp = br.browsingContext?.currentWindowGlobal;
+ const actor = wgp?.getActor("GenAI");
+ if (!actor) {
+ return;
+ }
+
+ try {
+ actor.sendAsyncMessage("AutoSubmit", {
+ promptText,
+ });
+ } catch (e) {
+ console.error("error message: ", e);
+ }
+ };
+
+ if (lazy.chatSidebar) {
+ const injector = {
+ async onStateChange(_wp, _req, flags) {
+ const stopDoc =
+ flags & Ci.nsIWebProgressListener.STATE_STOP &&
+ flags & Ci.nsIWebProgressListener.STATE_IS_DOCUMENT;
+ if (!stopDoc) {
+ return;
+ }
+
+ const wgp = browser.browsingContext?.currentWindowGlobal;
+ if (!wgp || wgp.isInitialDocument) {
+ return;
+ }
+
+ try {
+ browser.webProgress?.removeProgressListener(injector);
+ } catch {}
+ await sendAutoSubmit(browser, prompt);
+ },
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIWebProgressListener",
+ "nsISupportsWeakReference",
+ ]),
+ };
+
+ browser.webProgress?.addProgressListener(
+ injector,
+ Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT
+ );
+ } else {
+ // Tab mode:
+ const gBrowser = context.window.gBrowser;
+ const targetBrowser = browser;
+
+ const tabListener = {
+ async onLocationChange(br, _wp, _req, location) {
+ if (br !== targetBrowser) {
+ return;
+ }
+
+ const spec = location?.spec || "";
+ if (spec === "about:blank") {
+ return;
+ }
+
+ try {
+ gBrowser.removeTabsProgressListener(tabListener);
+ } catch {}
+ await sendAutoSubmit(browser, prompt);
+ },
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIwebProgressListener",
+ "nsISupportsWeakReference",
+ ]),
+ };
+
+ gBrowser.addTabsProgressListener(tabListener);
+ }
+ },
+
+ /**
* Handle selected prompt by opening tab or sidebar.
*
* @param {object} promptObj to convert to string
@@ -1077,8 +1164,11 @@ export const GenAI = {
const prompt = this.buildChatPrompt(promptObj, context);
// Pass the prompt via GET url ?q= param or request header
- const { header, queryParam = "q" } =
- this.chatProviders.get(lazy.chatProvider) ?? {};
+ const {
+ header,
+ queryParam = "q",
+ supportAutoSubmit,
+ } = this.chatProviders.get(lazy.chatProvider) ?? {};
const url = new URL(lazy.chatProvider);
const options = {
inBackground: false,
@@ -1120,8 +1210,15 @@ export const GenAI = {
} else {
browser = context.window.gBrowser.addTab("", options).linkedBrowser;
}
-
browser.fixupAndLoadURIString(url, options);
+
+ // Run autosubmit only for chatGPT, Claude, or mochitest
+ if (
+ supportAutoSubmit ||
+ lazy.chatProvider?.includes("file_chat-autosubmit.html")
+ ) {
+ this.setupAutoSubmit(browser, prompt, context);
+ }
},
};
diff --git a/browser/components/genai/GenAIChild.sys.mjs b/browser/components/genai/GenAIChild.sys.mjs
@@ -144,16 +144,101 @@ export class GenAIChild extends JSWindowActorChild {
}
/**
- * Handles incoming messages from the browser.
+ * Handles incoming messages from the browser
*
* @param {object} message - The message object containing name
- * @param {string} message.name - The name of the message.
+ * @param {string} message.name - The name of the message
+ * @param {object} message.data - The data object of the message
*/
- async receiveMessage({ name }) {
- if (name === "GetReadableText") {
- return await this.getContentText();
+ async receiveMessage({ name, data }) {
+ switch (name) {
+ case "GetReadableText":
+ return this.getContentText();
+ case "AutoSubmit":
+ return await this.autoSubmitClick(data);
+ default:
+ return null;
+ }
+ }
+
+ /**
+ * Find the prompt editable element within a timeout
+ * Return the element or null
+ *
+ * @param {Window} win - the target window
+ * @param {number} [tms=1000] - time in ms
+ */
+ async findTextareaEl(win, tms = 1000) {
+ const start = win.performance.now();
+ let el;
+ while (
+ !(el = win.document.querySelector(
+ '#prompt-textarea, [contenteditable], [role="textbox"]'
+ )) &&
+ win.performance.now() - start < tms
+ ) {
+ await new Promise(r => win.requestAnimationFrame(r));
+ }
+ return el;
+ }
+
+ /**
+ * Automatically submit the prompt
+ *
+ * @param {string} promptText - the prompt to send
+ */
+ async autoSubmitClick({ promptText = "" } = {}) {
+ const win = this.contentWindow;
+ if (!win || win._autosent) {
+ return;
+ }
+
+ // Ensure the DOM is ready before querying elements
+ if (win.document.readyState === "loading") {
+ await new Promise(r =>
+ win.addEventListener("DOMContentLoaded", r, { once: true })
+ );
+ }
+
+ const editable = await this.findTextareaEl(win);
+ if (!editable) {
+ return;
+ }
+
+ if (!editable.textContent) {
+ editable.textContent = promptText;
+ editable.dispatchEvent(new win.InputEvent("input", { bubbles: true }));
+ }
+
+ // Explicitly wait for the button is ready
+ await new Promise(r => win.requestAnimationFrame(r));
+
+ // Simulating click to avoid SPA router rewriting (?prompt-textarea=)
+ const submitBtn =
+ win.document.querySelector('button[data-testid="send-button"]') ||
+ win.document.querySelector('button[aria-label="Send prompt"]') ||
+ win.document.querySelector('button[aria-label="Send message"]');
+
+ if (submitBtn) {
+ submitBtn.click();
+ win._autosent = true;
+ }
+
+ // Ensure clean up textarea only for chatGPT and mochitest
+ if (
+ win._autosent &&
+ (/chatgpt\.com/i.test(win.location.host) ||
+ win.location.pathname.includes("file_chat-autosubmit.html"))
+ ) {
+ win.setTimeout(() => {
+ if (editable.textContent) {
+ editable.textContent = "";
+ editable.dispatchEvent(
+ new win.InputEvent("input", { bubbles: true })
+ );
+ }
+ }, 500);
}
- return null;
}
/**
diff --git a/browser/components/genai/tests/browser/browser.toml b/browser/components/genai/tests/browser/browser.toml
@@ -22,6 +22,9 @@ skip-if = [
]
["browser_chat_request.js"]
+support-files = [
+ "file_chat-autosubmit.html",
+]
["browser_chat_shortcuts.js"]
diff --git a/browser/components/genai/tests/browser/browser_chat_request.js b/browser/components/genai/tests/browser/browser_chat_request.js
@@ -104,3 +104,49 @@ add_task(async function test_chat_default_query() {
gBrowser.removeTab(gBrowser.selectedTab);
});
+
+/**
+ * Check that the prompt submitted automatically in the certain provider page
+ */
+add_task(async function test_chat_auto_submit() {
+ const ROOT = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ );
+ const TEST_URL = ROOT + "file_chat-autosubmit.html";
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.ml.chat.provider", TEST_URL],
+ ["browser.ml.chat.prompt.prefix", ""],
+ ["browser.ml.chat.sidebar", false],
+ ],
+ });
+
+ await GenAI.handleAskChat({ value: "hello world?" }, { window });
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => {
+ await ContentTaskUtils.waitForCondition(
+ () => content.wrappedJSObject.submitCount === 1,
+ "Prompt form submitted"
+ );
+ Assert.equal(
+ content.wrappedJSObject.submitCount,
+ 1,
+ "Form is triggered by AutoSubmitClick"
+ );
+
+ await ContentTaskUtils.waitForCondition(
+ () => content.document.getElementById("ta").textContent === "",
+ "Prompt was cleared"
+ );
+ Assert.equal(
+ content.document.getElementById("ta").textContent,
+ "",
+ "Prompt text is cleared after auto submission"
+ );
+ });
+
+ gBrowser.removeTab(gBrowser.selectedTab);
+});
diff --git a/browser/components/genai/tests/browser/file_chat-autosubmit.html b/browser/components/genai/tests/browser/file_chat-autosubmit.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html>
+<head><meta charset="UTF-8"></head>
+<body>
+<form id="f">
+ <div id="ta" contenteditable="true"></div>
+ <button id="send" data-testid="send-button" type="submit">Send</button>
+</form>
+<script>
+ const url = new URL(location.href);
+ const prompt = url.searchParams.get("q") || "";
+ const ta = document.getElementById("ta");
+ const form = document.getElementById("f");
+ window.submitCount = 0;
+
+ setTimeout(() => {
+ ta.textContent = prompt;
+ ta.dispatchEvent(new InputEvent("input", { bubbles: true }));
+ }, 50);
+
+ form.addEventListener("submit", e => {
+ e.preventDefault();
+ window.submitCount++;
+ })
+</script>
+</body>
+</html>