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
|
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.
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
+18
-10
@@ -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 });
|
||||||
} catch {
|
tabId = tabs?.[0]?.id;
|
||||||
return "";
|
} catch {
|
||||||
|
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;
|
||||||
|
|||||||
@@ -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,
|
"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": {
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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