mirror of
https://git.zavage.net/Zavage-Software/search-with-chatgpt-powered-by-openai-extension.git
synced 2026-08-10 21:50:29 -06:00
121 lines
3.9 KiB
JavaScript
121 lines
3.9 KiB
JavaScript
import { load } from "../lib/settings.js";
|
|
import { buildChatGptUrl } from "../lib/build-url.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
|
|
// 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.
|
|
//
|
|
// 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() ? 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 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) {
|
|
return;
|
|
}
|
|
openChatGpt(await readSelection());
|
|
});
|
|
}
|
|
|
|
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.)
|
|
});
|