tor-browser

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

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:
Mbrowser/components/genai/GenAI.sys.mjs | 103++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Mbrowser/components/genai/GenAIChild.sys.mjs | 97++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Mbrowser/components/genai/tests/browser/browser.toml | 3+++
Mbrowser/components/genai/tests/browser/browser_chat_request.js | 46++++++++++++++++++++++++++++++++++++++++++++++
Abrowser/components/genai/tests/browser/file_chat-autosubmit.html | 27+++++++++++++++++++++++++++
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>