feat: add Ask ChatGPT toolbar action
This commit is contained in:
@@ -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
|
||||
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.
|
||||
- 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
|
||||
`docs/` must never appear, not even as empty directory entries.
|
||||
|
||||
@@ -4,6 +4,8 @@ All notable changes to this project will be documented in this file. See [commit
|
||||
|
||||
## 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
|
||||
visibility preference. Advanced provides selection presets and link/page URL
|
||||
actions, with optional translation and a preferred language or language dialog.
|
||||
|
||||
@@ -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
|
||||
**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
|
||||
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
|
||||
@@ -32,7 +38,7 @@ Firefox’s shortcut editor when supported; otherwise follow the menu path above
|
||||
**Restore defaults** does not change your keyboard shortcut.
|
||||
|
||||
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.
|
||||
|
||||
## 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:
|
||||
|
||||
- **Context menu** — **Simple** (default) keeps the direct selection action;
|
||||
**Advanced** offers contextual presets; **Off** hides the menu. The keyboard
|
||||
shortcut works in every mode. Existing enabled/disabled preferences migrate
|
||||
**Advanced** offers contextual presets; **Off** hides the menu. The toolbar
|
||||
button and keyboard shortcut work in every mode. Existing enabled/disabled preferences migrate
|
||||
to Simple/Off. Changes apply without restarting Firefox.
|
||||
- **Show Translate in advanced menu** — off by default. When enabled, choose
|
||||
**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.
|
||||
|
||||
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
|
||||
**Manage Extension Shortcuts** in the Add-ons gear menu.
|
||||
|
||||
The `gpt <query>` address-bar search is unchanged and does not read these
|
||||
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
|
||||
`gpt <query>` address-bar search still works; only these newer features are
|
||||
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.
|
||||
**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
|
||||
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
|
||||
with **Add-ons** enabled, they sync end-to-end encrypted through your own
|
||||
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
|
||||
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.
|
||||
|
||||
+18
-10
@@ -6,11 +6,12 @@ import { initializeMenus } from "./menus.js";
|
||||
|
||||
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.
|
||||
//
|
||||
// A commands invocation confers activeTab (ext-commands.js grants it before
|
||||
// firing onCommand), so executeScript works with no host permission.
|
||||
// Toolbar clicks and command invocations confer activeTab, so executeScript
|
||||
// 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
|
||||
// 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
|
||||
// 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 "";
|
||||
async function readSelection(tabId) {
|
||||
if (tabId === undefined) {
|
||||
try {
|
||||
const tabs = await browser.tabs.query({ active: true, currentWindow: true });
|
||||
tabId = tabs?.[0]?.id;
|
||||
} 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 "";
|
||||
}
|
||||
@@ -82,6 +84,12 @@ async function runAction(request) {
|
||||
}
|
||||
|
||||
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) {
|
||||
browser.commands.onCommand.addListener(async command => {
|
||||
if (command !== COMMAND_ID) return;
|
||||
|
||||
@@ -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.
|
||||
@@ -39,6 +39,13 @@
|
||||
"open_in_tab": 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": {
|
||||
"ask-chatgpt": {
|
||||
"suggested_key": {
|
||||
|
||||
@@ -13,6 +13,12 @@
|
||||
ChatGPT.
|
||||
</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">
|
||||
<div class="field">
|
||||
<label for="contextMenuMode">Context menu</label>
|
||||
@@ -71,7 +77,7 @@
|
||||
<div class="field">
|
||||
<label for="promptTemplate">Prompt template</label>
|
||||
<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 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>.
|
||||
</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.
|
||||
Restore defaults only resets extension settings, including the prompt
|
||||
template; it does not change Firefox’s search engine or keyboard shortcut.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user