feat: add Ask ChatGPT toolbar action

This commit is contained in:
golem
2026-10-12 12:51:24 -06:00
parent 5bc670b937
commit 78e94640e1
8 changed files with 180 additions and 18 deletions
+5
View File
@@ -72,6 +72,11 @@ After `npm test`, use `npm run dev` and confirm:
into the page → a blank chat is expected. Firefox leaves focus in the into the page → a blank chat is expected. Firefox leaves focus in the
findbar, so no content frame reports focus. This is a deliberate trade: a findbar, so no content frame reports focus. This is a deliberate trade: a
stale cross-origin frame silently seeding the prompt was the worse failure. stale cross-origin frame silently seeding the prompt was the worse failure.
- Click the **Ask ChatGPT** toolbar button with no selection, a top-frame
selection, and an iframe selection after a stale top-frame selection.
Check restricted-page fallback, context menu Off, saved prompt and opening
preferences, and consent before shortening long selections. Test both the
pinned toolbar button and its entry in Firefox’s extensions menu.
Inspect ZIP contents whenever packaging rules or assets change; `test/` and Inspect ZIP contents whenever packaging rules or assets change; `test/` and
`docs/` must never appear, not even as empty directory entries. `docs/` must never appear, not even as empty directory entries.
+2
View File
@@ -4,6 +4,8 @@ All notable changes to this project will be documented in this file. See [commit
## Unreleased ## Unreleased
- Add an Ask ChatGPT toolbar button: use selected text or open a blank chat,
honoring saved prompt and tab/window preferences. No new permissions.
- Add Off / Simple / Advanced context-menu modes with migration from the old - Add Off / Simple / Advanced context-menu modes with migration from the old
visibility preference. Advanced provides selection presets and link/page URL visibility preference. Advanced provides selection presets and link/page URL
actions, with optional translation and a preferred language or language dialog. actions, with optional translation and a preferred language or language dialog.
+16 -6
View File
@@ -25,6 +25,12 @@ You can also press **Alt+Shift+G** to use the current selection. With nothing
selected, the shortcut opens a blank chat. Change the shortcut under selected, the shortcut opens a blank chat. Change the shortcut under
**Manage Extension Shortcuts** in the `about:addons` gear menu. **Manage Extension Shortcuts** in the `about:addons` gear menu.
Click the extension’s **Ask ChatGPT** toolbar button to use selected text or,
with nothing selected, open a blank chat. On pages Firefox does not allow
extensions to read, the button opens a blank chat. You can pin the button to
the toolbar from Firefox’s extensions menu. It uses your saved prompt and
tab/window preferences, and works even when the context menu is Off.
The options panel shows your currently configured shortcut and refreshes when The options panel shows your currently configured shortcut and refreshes when
it changes or you return to the panel. If you remove it, the panel shows it changes or you return to the panel. If you remove it, the panel shows
“No keyboard shortcut assigned”. Its **Change keyboard shortcut** button opens “No keyboard shortcut assigned”. Its **Change keyboard shortcut** button opens
@@ -32,7 +38,7 @@ Firefox’s shortcut editor when supported; otherwise follow the menu path above
**Restore defaults** does not change your keyboard shortcut. **Restore defaults** does not change your keyboard shortcut.
To customize the prompt, set **Prompt template** to something like To customize the prompt, set **Prompt template** to something like
`Explain simply: {query}` or `Translate into Spanish: {query}`. The Simple menu action, Ask normally, and the shortcut use your template. `Explain simply: {query}` or `Translate into Spanish: {query}`. The Simple menu action, Ask normally, toolbar button, and shortcut use your template.
Other advanced actions use their own dedicated prompts. Other advanced actions use their own dedicated prompts.
## Search keyword and default engine ## Search keyword and default engine
@@ -53,8 +59,8 @@ Open `about:addons`, select this extension, and choose the **Options** tab
(**Preferences** on macOS and Linux). Available settings: (**Preferences** on macOS and Linux). Available settings:
- **Context menu** — **Simple** (default) keeps the direct selection action; - **Context menu** — **Simple** (default) keeps the direct selection action;
**Advanced** offers contextual presets; **Off** hides the menu. The keyboard **Advanced** offers contextual presets; **Off** hides the menu. The toolbar
shortcut works in every mode. Existing enabled/disabled preferences migrate button and keyboard shortcut work in every mode. Existing enabled/disabled preferences migrate
to Simple/Off. Changes apply without restarting Firefox. to Simple/Off. Changes apply without restarting Firefox.
- **Show Translate in advanced menu** — off by default. When enabled, choose - **Show Translate in advanced menu** — off by default. When enabled, choose
**Ask each time** for a language dialog, or enter a **Preferred language** **Ask each time** for a language dialog, or enter a **Preferred language**
@@ -84,14 +90,14 @@ that cannot fit are rejected. The extension does not simulate submission; ChatGP
prompt is sent automatically or waits in the composer. prompt is sent automatically or waits in the composer.
These settings apply to selecting text and choosing These settings apply to selecting text and choosing
**Ask ChatGPT** from the right-click menu, or pressing **Ask ChatGPT** from the right-click menu, clicking the toolbar button, or pressing
<kbd>Alt</kbd>+<kbd>Shift</kbd>+<kbd>G</kbd>. Rebind the shortcut under <kbd>Alt</kbd>+<kbd>Shift</kbd>+<kbd>G</kbd>. Rebind the shortcut under
**Manage Extension Shortcuts** in the Add-ons gear menu. **Manage Extension Shortcuts** in the Add-ons gear menu.
The `gpt <query>` address-bar search is unchanged and does not read these The `gpt <query>` address-bar search is unchanged and does not read these
settings. settings.
The options panel, the right-click item, and the keyboard shortcut require The options panel, toolbar button, right-click item, and keyboard shortcut require
Firefox 112 or newer. On older versions the extension still installs and the Firefox 112 or newer. On older versions the extension still installs and the
`gpt <query>` address-bar search still works; only these newer features are `gpt <query>` address-bar search still works; only these newer features are
inactive. inactive.
@@ -102,6 +108,10 @@ The extension opens ChatGPT with the prompt in the URL. ChatGPT controls what
happens next: it may send the prompt automatically or leave it in the composer. happens next: it may send the prompt automatically or leave it in the composer.
**If the prompt is not sent automatically, press Enter in ChatGPT.** **If the prompt is not sent automatically, press Enter in ChatGPT.**
If ChatGPT shows a verification error, try signing in and opening the prompt
again. Signing in has helped in testing, but does not guarantee automatic
submission.
The extension does not click Send, simulate Enter, or call ChatGPT's internal The extension does not click Send, simulate Enter, or call ChatGPT's internal
conversation API. This behavior can vary with changes to ChatGPT itself. conversation API. This behavior can vary with changes to ChatGPT itself.
@@ -145,7 +155,7 @@ reported to Zavage Software.
- Your settings are stored in your own browser profile. If you use Firefox Sync - Your settings are stored in your own browser profile. If you use Firefox Sync
with **Add-ons** enabled, they sync end-to-end encrypted through your own with **Add-ons** enabled, they sync end-to-end encrypted through your own
Mozilla account. Mozilla account.
- Selection actions and the shortcut place your selected text in the ChatGPT - Selection actions, the toolbar button, and the shortcut place your selected text in the ChatGPT
URL. Advanced link/page actions instead place the chosen link or current URL. Advanced link/page actions instead place the chosen link or current
page URL in the prompt. URLs may include private query parameters or fragments; page URL in the prompt. URLs may include private query parameters or fragments;
use those actions only for URLs you want to share with ChatGPT. use those actions only for URLs you want to share with ChatGPT.
+15 -7
View File
@@ -6,11 +6,12 @@ import { initializeMenus } from "./menus.js";
const COMMAND_ID = "ask-chatgpt"; const COMMAND_ID = "ask-chatgpt";
// Reads the user's selection for the keyboard-shortcut path. The context menu // Reads the user's selection for the shortcut and toolbar. The context menu
// does not need this — it receives info.selectionText directly. // does not need this — it receives info.selectionText directly.
// //
// A commands invocation confers activeTab (ext-commands.js grants it before // Toolbar clicks and command invocations confer activeTab, so executeScript
// firing onCommand), so executeScript works with no host permission. // works with no host permission. Use the toolbar event's tab ID rather than
// querying again after the user might have switched tabs.
// //
// Every frame keeps its own independent Selection, and a selection made in a // 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 // non-focused frame is not cleared when the user selects elsewhere. Without a
@@ -27,17 +28,18 @@ const COMMAND_ID = "ask-chatgpt";
// "Missing host permission for the tab", a thrown variant naming frames, and a // "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 // SILENT resolution to [null] on parent-process about: pages. Never index into
// a result without a null check. // a result without a null check.
async function readSelection() { async function readSelection(tabId) {
let tabs; if (tabId === undefined) {
try { try {
tabs = await browser.tabs.query({ active: true, currentWindow: true }); const tabs = await browser.tabs.query({ active: true, currentWindow: true });
tabId = tabs?.[0]?.id;
} catch { } catch {
return ""; return "";
} }
}
// Only tab.id may be read. Reading tab.url or tab.title would require the // 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. // "tabs" permission, which does add an install-prompt warning line.
const tabId = tabs?.[0]?.id;
if (typeof tabId !== "number") { if (typeof tabId !== "number") {
return ""; return "";
} }
@@ -82,6 +84,12 @@ async function runAction(request) {
} }
void initializeMenus({ browser, load, onAction: runAction }).catch(console.error); void initializeMenus({ browser, load, onAction: runAction }).catch(console.error);
if (browser.browserAction) {
browser.browserAction.onClicked.addListener(async tab => {
const source = await readSelection(tab.id);
await runAction({ actionId: "ask", source, settings: await load() });
});
}
if (browser.commands) { if (browser.commands) {
browser.commands.onCommand.addListener(async command => { browser.commands.onCommand.addListener(async command => {
if (command !== COMMAND_ID) return; if (command !== COMMAND_ID) return;
+60
View File
@@ -0,0 +1,60 @@
# Toolbar action verification — 2026-10-12
The Ask ChatGPT toolbar button uses the existing Ask action: selected text is
wrapped in the saved prompt; an empty or unreadable selection opens a blank
chat. Saved opening preferences and shortening consent apply. No new
permissions, popup, or settings were added.
## Automated checks
- Fresh `npm test` before the PR: strict extension lint reports zero errors,
warnings, or notices; all 61 tests across 11 test files pass; ZIP build passes.
- New toolbar regression covers the clicked tab rather than a later active tab,
focused iframe selection with stale parent/sibling selections, empty/unfocused
selection, saved prompt/background-tab preferences, and restricted-page
fallback with the new-window preference.
- ZIP inspection excludes docs, tests, and local tool metadata.
- Independent read-only review found no concrete code defects; the requested
browser checks for focus handling and shortening consent were then completed.
## Firefox 157.0.1 checks
Launched with `npm run dev` in temporary headless profiles. Marionette clicked
Firefox's actual action button; it did not call the extension handler directly.
- Pinned toolbar: top-frame selection reaches the ChatGPT URL; no selection
opens `https://chatgpt.com/`.
- Same-origin and cross-origin iframe selections win over a stale top-frame
selection.
- `about:config` opens a blank chat.
- Unpinned action in the extensions menu preserves top-frame and cross-origin
iframe selections, including a stale parent selection.
- Changing the prompt through the options UI works with context-menu mode Off.
- A long selection opens the shortening-consent dialog. Cancel closes it
without opening ChatGPT.
- The pinned icon was visually confirmed in Firefox.
The selection reader retains the shortcut's focused-leaf-frame filter. Actual
mouse activation through Firefox's toolbar and extensions menu preserved the
necessary focus in these checks; no broader frame scan was introduced.
The maintainer subsequently confirmed the toolbar button works during desktop
use. Every editable field type and the full release regression matrix in
AGENTS.md were not rechecked; existing shortcut-specific manual release checks
remain outstanding.
## ChatGPT submission limitation
These checks establish that the extension opens the expected URL and preserves
the selection. They do not guarantee that ChatGPT accepts or automatically
submits the prompt.
During follow-up testing, URL-supplied prompts showed a ChatGPT verification
error while signed out on affected machines. Signing in allowed the prompt to
fill the composer, but it could still require Enter. The verification failure
also occurred when the URL was entered directly with this extension disabled.
The cause remains unconfirmed.
The pre-toolbar version and this version generate the same
`https://chatgpt.com/?q=hello` URL for a default Ask action; the address-bar search
definition is also unchanged. No forced submission or workaround was added.
+7
View File
@@ -39,6 +39,13 @@
"open_in_tab": false, "open_in_tab": false,
"browser_style": false "browser_style": false
}, },
"browser_action": {
"default_title": "Ask ChatGPT",
"default_icon": {
"16": "icons/powered-by-openai-icon-16x16.png",
"32": "icons/powered-by-openai-icon-32x32.png"
}
},
"commands": { "commands": {
"ask-chatgpt": { "ask-chatgpt": {
"suggested_key": { "suggested_key": {
+8 -2
View File
@@ -13,6 +13,12 @@
ChatGPT. ChatGPT.
</p> </p>
<p class="intro">
Click the <strong>Ask ChatGPT</strong> toolbar button to ask about selected
text, or open a blank chat when nothing is selected. The button works in
every context-menu mode and follows your settings below.
</p>
<form id="settings" autocomplete="off"> <form id="settings" autocomplete="off">
<div class="field"> <div class="field">
<label for="contextMenuMode">Context menu</label> <label for="contextMenuMode">Context menu</label>
@@ -71,7 +77,7 @@
<div class="field"> <div class="field">
<label for="promptTemplate">Prompt template</label> <label for="promptTemplate">Prompt template</label>
<input type="text" id="promptTemplate" spellcheck="false" /> <input type="text" id="promptTemplate" spellcheck="false" />
<p class="hint"><code>{query}</code> is replaced with your selected text. Used by Ask normally, Simple mode, and the keyboard shortcut. Other advanced actions use their own prompts.</p> <p class="hint"><code>{query}</code> is replaced with your selected text. Used by Ask normally, Simple mode, the toolbar button, and the keyboard shortcut. Other advanced actions use their own prompts.</p>
</div> </div>
<div class="actions"> <div class="actions">
@@ -92,7 +98,7 @@
<a href="https://support.mozilla.org/en-US/kb/change-your-default-search-settings-firefox" target="_blank" rel="noopener noreferrer">Firefox search settings guide</a>. <a href="https://support.mozilla.org/en-US/kb/change-your-default-search-settings-firefox" target="_blank" rel="noopener noreferrer">Firefox search settings guide</a>.
</p> </p>
<p> <p>
The settings above apply to the right-click menu and keyboard shortcut; The settings above apply to the right-click menu, toolbar button, and keyboard shortcut;
address-bar searches use Firefox’s search engine settings. address-bar searches use Firefox’s search engine settings.
Restore defaults only resets extension settings, including the prompt Restore defaults only resets extension settings, including the prompt
template; it does not change Firefox’s search engine or keyboard shortcut. template; it does not change Firefox’s search engine or keyboard shortcut.
+64
View File
@@ -0,0 +1,64 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import vm from 'node:vm';
test('toolbar asks about the clicked tab selection and opens blank when unavailable', async () => {
let click;
let frames = [];
let restricted = false;
let settings = { contextMenuMode: 'off' };
const opened = [];
const noop = { addListener() {}, removeListener() {} };
globalThis.browser = {
storage: { sync: { get: async () => settings }, onChanged: noop },
runtime: { onConnect: noop },
menus: { removeAll: async () => {}, onShown: noop, onHidden: noop, onClicked: noop },
commands: { onCommand: noop },
browserAction: { onClicked: { addListener: listener => { click = listener; } } },
tabs: {
// The clicked tab must be used even if another tab becomes active.
query: async () => [{ id: 99 }],
executeScript: async (id, { code }) => {
assert.equal(id, 7);
if (restricted) throw new Error('Restricted page');
return frames.map(frame => vm.runInNewContext(code, {
document: {
hasFocus: () => frame.focus,
activeElement: { tagName: frame.active ?? 'BODY' },
},
window: { getSelection: () => ({ toString: () => frame.text }) },
}));
},
create: async options => { opened.push(options); },
},
windows: { create: async options => { opened.push({ ...options, window: true }); } },
};
try {
await import('../background/background.js');
assert.equal(typeof click, 'function', 'toolbar click handler must be registered');
for (const [selection, expected] of [
[[{ focus: true, text: 'selected $& text' }], 'selected $& text'],
[[{ focus: true, active: 'IFRAME', text: 'stale parent' },
{ focus: false, text: 'stale sibling' },
{ focus: true, text: 'iframe selection' }], 'iframe selection'],
[[{ focus: true, text: '' }], null],
[[{ focus: false, text: 'stale selection' }], null],
]) {
frames = selection;
await click({ id: 7 });
assert.equal(new URL(opened.at(-1).url).searchParams.get('q'), expected);
}
settings = { contextMenuMode: 'off', promptTemplate: 'Explain: {query}', openIn: 'background-tab' };
frames = [{ focus: true, text: 'hello' }];
await click({ id: 7 });
assert.deepEqual(opened.at(-1), { url: 'https://chatgpt.com/?q=Explain%3A+hello', active: false });
settings = { openIn: 'new-window' };
restricted = true;
await click({ id: 7 });
assert.deepEqual(opened.at(-1), { url: 'https://chatgpt.com/', window: true });
} finally {
delete globalThis.browser;
}
});