ectionInfo = this.getSelectionInfo(); const delay = event.timeStamp - this.downTimeStamp; // Only send a message if there's a new selection or a long press if ( (selectionInfo.selection && selectionInfo.selection !== this.downSelection) || delay > lazy.shortcutsDelay ) { this.sendAsyncMessage("GenAI:ShowShortcuts", { ...selectionInfo, contentType: "selection", delay, screenXDevPx: screenX * this.contentWindow.devicePixelRatio, screenYDevPx: screenY * this.contentWindow.devicePixelRatio, }); this.registerHideEvents(); } // Clear the timeout reference after execution this.mouseUpTimeout = null; }, this.debounceDelay); break; } case "pagehide": case "resize": case "scroll": case "selectionchange": // Hide if selection might have shifted away from shortcuts sendHide(); break; } } /** * Provide the selected text and input type. * * @returns {object} selection info */ getSelectionInfo() { // Handle regular selection outside of inputs const { activeElement } = this.document; const selection = this.contentWindow.getSelection()?.toString().trim(); if (selection) { return { inputType: activeElement.closest("[contenteditable]") ? "contenteditable" : "", selection, }; } // Selection within input elements const { selectionStart, value } = activeElement; if (selectionStart != null && value != null) { return { inputType: activeElement.localName, selection: value.slice(selectionStart, activeElement.selectionEnd), }; } return { inputType: "", selection: "" }; } /** * Handles incoming messages from the browser * * @param {object} message - The message object containing name * @param {string} message.name - The name of the message * @param {object} message.data - The data object of the message */ 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")) ) { const container = editable.parentElement; if (!container) { return; } const observer = new win.MutationObserver(() => { // Always refetch because ChatGPT replaces editable div const currentEditable = container.querySelector( '[contenteditable="true"]' ); if (!currentEditable) { return; } let hasText = currentEditable.textContent?.trim().length > 0; if (hasText) { currentEditable.textContent = ""; currentEditable.dispatchEvent( new win.InputEvent("input", { bubbles: true }) ); } }); observer.observe(container, { childList: true, subtree: true }); // Disconnect once things stabilize win.setTimeout(() => observer.disconnect(), 2000); } } /** * Get readable article text or whole innerText from the content side. * * @returns {string} text from the page */ async getContentText() { const win = this.browsingContext?.window; const doc = win?.document; const article = await lazy.ReaderMode.parseDocument(doc); return { readerMode: !!article?.textContent, selection: (article?.textContent || doc?.body?.innerText || "") .trim() // Replace duplicate whitespace with either a single newline or space .replace(/(\s*\n\s*)|\s{2,}/g, (_, newline) => (newline ? "\n" : " ")), }; } didDestroy() { this.#isDestroyed = true; } } PK