import { load } from "../lib/settings.js"; import { prepareAction } from "../lib/prepare-action.js"; import { openChat } from "./open-chat.js"; import { showActionDialog } from "./dialogs.js"; import { initializeMenus } from "./menus.js"; const COMMAND_ID = "ask-chatgpt"; // Reads the user's selection for the keyboard-shortcut path. The context menu // does not need this — it receives info.selectionText directly. // // A commands invocation confers activeTab (ext-commands.js grants it before // firing onCommand), so executeScript works with no host permission. // // Every frame keeps its own independent Selection, and a selection made in a // non-focused frame is not cleared when the user selects elsewhere. Without a // filter, a stale selection in an unfocused (possibly cross-origin) iframe // could win over — or be pulled alongside — the selection the user actually // made, with the winner among multiple non-empty results decided by an // executeScript ordering that isn't specified. The injected snippet is // therefore gated on document.hasFocus(), which is true for the focused // document and all of its ancestors: the top frame wins when the user // selected there, and an iframe's selection is only picked up when it is // itself the focused frame. Exclude ancestors whose active element is a frame. // // Failure has three measured shapes, all handled here: a thrown // "Missing host permission for the tab", a thrown variant naming frames, and a // SILENT resolution to [null] on parent-process about: pages. Never index into // a result without a null check. async function readSelection() { let tabs; try { tabs = await browser.tabs.query({ active: true, currentWindow: true }); } catch { return ""; } // Only tab.id may be read. Reading tab.url or tab.title would require the // "tabs" permission, which does add an install-prompt warning line. const tabId = tabs?.[0]?.id; if (typeof tabId !== "number") { return ""; } let results; try { results = await browser.tabs.executeScript(tabId, { code: "document.hasFocus() && !['IFRAME', 'FRAME'].includes(document.activeElement?.tagName) ? window.getSelection().toString() : ''", allFrames: true, matchAboutBlank: true, }); } catch { return ""; } if (!Array.isArray(results)) { return ""; } for (const result of results) { if (typeof result === "string" && result.trim() !== "") { return result; } } return ""; } async function runAction(request) { let preparation = prepareAction(request); if (preparation.kind === "ready") { try { await openChat(browser, preparation.url, request.settings.openIn); return; } catch { // Keep the ready payload so the dialog's Continue button retries it. } } try { await showActionDialog(browser, { request, preparation }); } catch { console.error("Could not open the action dialog. Please try the action again."); } } void initializeMenus({ browser, load, onAction: runAction }).catch(console.error); if (browser.commands) { browser.commands.onCommand.addListener(async command => { if (command !== COMMAND_ID) return; // Read the selection before another asynchronous API loses activeTab context. const source = await readSelection(); await runAction({ actionId: "ask", source, settings: await load() }); }); }