feat: add advanced context actions and optional translation

This commit is contained in:
golem
2026-10-05 15:38:16 -06:00
parent 33c4dad00f
commit 5308932bf1
34 changed files with 1579 additions and 159 deletions
+28 -56
View File
@@ -1,7 +1,9 @@
import { load } from "../lib/settings.js";
import { buildChatGptUrl } from "../lib/build-url.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 MENU_ID = "ask-chatgpt-selection";
const COMMAND_ID = "ask-chatgpt";
// Reads the user's selection for the keyboard-shortcut path. The context menu
@@ -19,7 +21,7 @@ const COMMAND_ID = "ask-chatgpt";
// 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.
// 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
@@ -43,7 +45,7 @@ async function readSelection() {
let results;
try {
results = await browser.tabs.executeScript(tabId, {
code: "document.hasFocus() ? window.getSelection().toString() : ''",
code: "document.hasFocus() && !['IFRAME', 'FRAME'].includes(document.activeElement?.tagName) ? window.getSelection().toString() : ''",
allFrames: true,
matchAboutBlank: true,
});
@@ -62,59 +64,29 @@ async function readSelection() {
return "";
}
async function openChatGpt(query) {
try {
const settings = await load();
const url = buildChatGptUrl(settings, query);
if (settings.openIn === "new-window") {
try {
await browser.windows.create({ url });
return;
} catch {
// Fall through to a tab rather than doing nothing.
}
}
await browser.tabs.create({ url, active: settings.openIn !== "background-tab" });
} catch {
// Nothing useful to say and nowhere useful to say it: the user expects a
// tab or nothing at all. Both call sites invoke this without awaiting, so
// without this catch a failure would log an unhandled rejection.
}
}
// removeAll() first, so a background event page waking up and re-running this
// module cannot throw on a duplicate menu id.
async function registerMenu() {
await browser.menus.removeAll();
browser.menus.create({
id: MENU_ID,
title: 'Ask ChatGPT about "%s"',
contexts: ["selection"],
});
}
browser.menus.onClicked.addListener((info) => {
if (info.menuItemId !== MENU_ID) {
return;
}
openChatGpt(info.selectionText ?? "");
});
// browser.commands does not exist on Firefox for Android. This extension is
// desktop-only, but guard rather than throwing at startup.
if (browser.commands) {
browser.commands.onCommand.addListener(async (command) => {
if (command !== COMMAND_ID) {
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.
}
openChatGpt(await readSelection());
});
}
try {
await showActionDialog(browser, { request, preparation });
} catch {
console.error("Could not open the action dialog. Please try the action again.");
}
}
registerMenu().catch(() => {
// menus.removeAll() is promise-based and can reject; a failed re-registration
// cannot be retried usefully from here. (menus.create() is callback-based and
// cannot reject, so it needs no guard.)
});
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() });
});
}