Files

93 lines
3.3 KiB
JavaScript

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() });
});
}