mirror of
https://git.zavage.net/Zavage-Software/search-with-chatgpt-powered-by-openai-extension.git
synced 2026-08-10 13:40:29 -06:00
doc: add extension options panel design spec
Design for an options panel reachable from about:addons, configuring a context-menu entry on selected text and an Alt+Shift+G shortcut. Adds storage and menus permissions only, both in Firefox's no-prompt class, so the install dialog gains no lines and data_collection_permissions stays "none". The gpt <query> search provider is deliberately untouched. Technical claims were verified rather than assumed: MV2 background type:"module" works from Firefox 112 (tested in 153.0.1), so no background HTML shim is needed; package.json needs "type":"module" or node --test fails on Node 22.0-22.6; the ChatGPT model/hints/temporary-chat params are undocumented and unreliable, so every label is written at hint strength. q= no longer auto-submits, only prefills. Also excludes docs/ and test/ from the packaged ZIP. Verified: without this the spec doc itself shipped inside the extension, and a bare glob alone leaves empty directory entries behind. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
5edb288681
commit
f057c66155
604
docs/superpowers/specs/2026-07-29-extension-options-design.md
Normal file
604
docs/superpowers/specs/2026-07-29-extension-options-design.md
Normal file
@ -0,0 +1,604 @@
|
|||||||
|
# Extension Options Panel — Design
|
||||||
|
|
||||||
|
- **Date:** 2026-07-29
|
||||||
|
- **Status:** Approved for planning
|
||||||
|
- **Branch:** `feat/extension-options-panel` (cut from `develop` @ `5edb288`)
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Add an options panel to the extension, reachable from `about:addons` → the
|
||||||
|
extension's detail card → the **Options** tab (Windows) / **Preferences** tab
|
||||||
|
(macOS, Linux). The panel configures two new entry points that this change also
|
||||||
|
introduces: a context-menu item on selected text, and a keyboard shortcut that
|
||||||
|
opens a fresh ChatGPT chat.
|
||||||
|
|
||||||
|
The existing `gpt <query>` address-bar search provider is **not** modified and
|
||||||
|
does not read the settings. See [Why the search provider is untouched](#why-the-search-provider-is-untouched).
|
||||||
|
|
||||||
|
New permissions: `storage` and `menus`. Both are in Firefox's no-prompt class,
|
||||||
|
so the install dialog gains no new lines and `data_collection_permissions:
|
||||||
|
["none"]` remains truthful.
|
||||||
|
|
||||||
|
## Goals
|
||||||
|
|
||||||
|
1. Ship a working options panel embedded in `about:addons`.
|
||||||
|
2. Ship two new entry points whose behaviour the panel controls.
|
||||||
|
3. Add no permission-warning lines to the install prompt.
|
||||||
|
4. Keep `data_collection_permissions: ["none"]` accurate.
|
||||||
|
5. Introduce unit testing for the pure logic, without new runtime dependencies.
|
||||||
|
6. Never promise behaviour that ChatGPT does not reliably deliver.
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
|
||||||
|
- Making the `gpt <query>` search provider configurable. Out of scope by
|
||||||
|
decision; it requires either host permissions on `chatgpt.com` or a
|
||||||
|
navigation-interception mechanism, both of which conflict with goals 3 and 4.
|
||||||
|
- A toolbar button.
|
||||||
|
- Named/multiple prompt templates with add-remove-reorder UI. One template only.
|
||||||
|
- Reading the page selection from the keyboard shortcut. That requires
|
||||||
|
`activeTab` + `tabs.executeScript`; deliberately excluded (see
|
||||||
|
[Rejected alternatives](#rejected-alternatives)).
|
||||||
|
- Migrating to Manifest V3.
|
||||||
|
- Chrome or other-browser packaging (`AGENTS.md` browser scope).
|
||||||
|
- User-facing error notifications. The `notifications` permission *does*
|
||||||
|
generate an install warning, so every failure path degrades silently instead.
|
||||||
|
|
||||||
|
## Background and constraints
|
||||||
|
|
||||||
|
The extension is currently manifest-only: no JavaScript exists, and the
|
||||||
|
manifest declares no `background`, `content_scripts`, or
|
||||||
|
`web_accessible_resources` key. Its single feature is
|
||||||
|
`chrome_settings_overrides.search_provider` with
|
||||||
|
`search_url: "https://chatgpt.com?q={searchTerms}"`.
|
||||||
|
|
||||||
|
### Why the search provider is untouched
|
||||||
|
|
||||||
|
`chrome_settings_overrides.search_provider` is declarative and fixed at install
|
||||||
|
time. No WebExtension API can rewrite an installed engine's URL. Making
|
||||||
|
`gpt <query>` honour the settings would require intercepting the navigation it
|
||||||
|
starts, which needs host access to `chatgpt.com` plus `webRequest` or `tabs` —
|
||||||
|
all of which add install-prompt lines. Rejected in favour of goals 3 and 4.
|
||||||
|
|
||||||
|
`is_default` stays `false`. Setting it to `true` triggers a second, separate
|
||||||
|
consent doorhanger (`webext-default-search-description`) outside any permission
|
||||||
|
list, plus an allowlist requirement.
|
||||||
|
|
||||||
|
### ChatGPT URL parameters are undocumented
|
||||||
|
|
||||||
|
None of `q`, `model`, `hints`, or `temporary-chat` is documented by OpenAI.
|
||||||
|
There is no contract and no support channel. The design is therefore resilient
|
||||||
|
by construction: the URL is built from a template with no runtime dependency on
|
||||||
|
any parameter being honoured, each parameter is independently removable so a
|
||||||
|
user can bisect when ChatGPT ignores one, and all user-facing copy is written at
|
||||||
|
hint strength. See [Appendix A](#appendix-a-verification-findings) for the
|
||||||
|
evidence behind each parameter's confidence rating.
|
||||||
|
|
||||||
|
The one parameter with quasi-official standing is `q`, because OpenAI's own
|
||||||
|
"ChatGPT search" browser extension uses `https://chatgpt.com/?q=`. Build on it;
|
||||||
|
treat everything else as best-effort.
|
||||||
|
|
||||||
|
**`q` prefills but does not submit.** Auto-submit was real historically but has
|
||||||
|
been prefill-only since approximately November 2025, after OpenAI gated it on
|
||||||
|
the `Sec-Fetch-Site` header as a prompt-injection mitigation. No copy in the
|
||||||
|
options panel, README, or AMO listing may say the query is *searched*, *sent*,
|
||||||
|
or *asked* — only that ChatGPT opens **with the query filled in**.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
Manifest stays **V2**. Mozilla has not announced any MV2 deprecation and
|
||||||
|
commits to at least 12 months' notice. The repo is MV2 today and has no Chrome
|
||||||
|
target, so migration is an orthogonal change. The code is nonetheless kept
|
||||||
|
MV3-portable: ES-module background, `persistent: false`, no `browser_style: true`.
|
||||||
|
|
||||||
|
ES modules work in an MV2 background script via
|
||||||
|
`"background": {"scripts": [...], "type": "module"}`, supported since Firefox
|
||||||
|
112 and verified empirically in Firefox 153.0.1. No background HTML shim is
|
||||||
|
needed. The options page is a normal extension page and can use
|
||||||
|
`<script type="module">` in both the embedded and standalone surfaces.
|
||||||
|
|
||||||
|
`strict_min_version` becomes `"112.0"` — the binding floor, above
|
||||||
|
`storage.sync` quota enforcement (79), `menus` (55), `options_ui` (55), and
|
||||||
|
`commands` (48).
|
||||||
|
|
||||||
|
### File layout
|
||||||
|
|
||||||
|
```
|
||||||
|
manifest.json
|
||||||
|
background/
|
||||||
|
background.js # registers menu + command, handles clicks. Imports lib/.
|
||||||
|
lib/
|
||||||
|
defaults.js # DEFAULT_SETTINGS — single source of truth
|
||||||
|
build-url.js # PURE: (settings, rawQuery) -> url string
|
||||||
|
settings.js # mergeSettings() pure; load()/save() wrap browser.storage
|
||||||
|
# load() -> storage.sync.get() piped through mergeSettings()
|
||||||
|
# save() -> storage.sync.set(), debounced by the caller
|
||||||
|
options/
|
||||||
|
options.html
|
||||||
|
options.css
|
||||||
|
options.js
|
||||||
|
test/
|
||||||
|
build-url.test.js
|
||||||
|
settings.test.js
|
||||||
|
docs/superpowers/specs/
|
||||||
|
2026-07-29-extension-options-design.md
|
||||||
|
```
|
||||||
|
|
||||||
|
`lib/build-url.js` and `lib/defaults.js` have zero browser dependencies, so
|
||||||
|
`node:test` imports them directly with no stubbing. `lib/settings.js` separates
|
||||||
|
the pure `mergeSettings()` from the `browser.storage` I/O for the same reason;
|
||||||
|
only `mergeSettings()` is unit-tested.
|
||||||
|
|
||||||
|
`lib/` ships inside the ZIP because the background script imports it at runtime.
|
||||||
|
`test/` and `docs/` must not ship — see [Packaging](#packaging).
|
||||||
|
|
||||||
|
### Manifest additions
|
||||||
|
|
||||||
|
```json
|
||||||
|
"permissions": ["storage", "menus"],
|
||||||
|
"background": {
|
||||||
|
"scripts": ["background/background.js"],
|
||||||
|
"type": "module",
|
||||||
|
"persistent": false
|
||||||
|
},
|
||||||
|
"options_ui": {
|
||||||
|
"page": "options/options.html",
|
||||||
|
"open_in_tab": false,
|
||||||
|
"browser_style": false
|
||||||
|
},
|
||||||
|
"commands": {
|
||||||
|
"new-chat": {
|
||||||
|
"suggested_key": { "default": "Alt+Shift+G" },
|
||||||
|
"description": "Open a new ChatGPT chat"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`browser_specific_settings.gecko` gains `"strict_min_version": "112.0"`. The
|
||||||
|
existing `id` and `data_collection_permissions` are unchanged.
|
||||||
|
|
||||||
|
`browser_style: false` is set **explicitly**. Omitting it is not equivalent — it
|
||||||
|
defaults to `true` in MV2. Setting it false means the panel renders identically
|
||||||
|
if the extension ever moves to MV3, where `browser_style: true` is unsupported.
|
||||||
|
|
||||||
|
`menus` is the canonical Firefox permission name; `contextMenus` is a
|
||||||
|
cross-browser alias. Use `menus` and access the API as `browser.menus`.
|
||||||
|
|
||||||
|
`Alt+Shift+G` is deliberate. `Ctrl+Shift+G` is Firefox's Find Previous — it
|
||||||
|
would validate, display in `about:addons`, and never fire.
|
||||||
|
|
||||||
|
## Settings
|
||||||
|
|
||||||
|
Stored as individual keys in `browser.storage.sync`.
|
||||||
|
|
||||||
|
| Key | Type | Default | Maps to |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `model` | string | `""` | `model=<value>`, omitted when empty |
|
||||||
|
| `temporaryChat` | boolean | `false` | `temporary-chat=true` when set |
|
||||||
|
| `webSearch` | boolean | `false` | `hints=search` when set |
|
||||||
|
| `openIn` | `"new-tab"` \| `"background-tab"` \| `"new-window"` | `"new-tab"` | tab/window creation |
|
||||||
|
| `promptTemplate` | string | `"{query}"` | wraps the selection |
|
||||||
|
|
||||||
|
Defaults reproduce today's behaviour exactly: an empty `model` and both
|
||||||
|
toggles off mean the only parameter sent is `q`.
|
||||||
|
|
||||||
|
### Model
|
||||||
|
|
||||||
|
A **free-text field**, not a dropdown. No authoritative slug list exists and
|
||||||
|
every published one has rotted (`gpt-4` → `o1-preview` → `gpt-4o` →
|
||||||
|
`gpt-5-thinking` → `auto`/`instant`/`thinking`). A curated dropdown would be
|
||||||
|
wrong within a quarter, and each stale entry fails *silently* to the account
|
||||||
|
default.
|
||||||
|
|
||||||
|
- Label: **Model (experimental)**
|
||||||
|
- Placeholder: `auto`
|
||||||
|
- Help text: "ChatGPT may ignore this and use your account default. Leave blank
|
||||||
|
to always use your account default."
|
||||||
|
- Default is empty, not `auto`, so the default configuration sends no `model`
|
||||||
|
parameter and matches current behaviour.
|
||||||
|
|
||||||
|
### Temporary chat
|
||||||
|
|
||||||
|
- Label: **Request a temporary chat**
|
||||||
|
- Help text: "ChatGPT may not honour this. Check ChatGPT's own indicator before
|
||||||
|
sending anything sensitive."
|
||||||
|
- Default **off**.
|
||||||
|
- The words *private*, *incognito*, and *secure* must not appear near this
|
||||||
|
setting. Reports indicate chats can revert to permanent after the first
|
||||||
|
message, and OpenAI is actively reworking the feature's semantics.
|
||||||
|
|
||||||
|
### Web search
|
||||||
|
|
||||||
|
- Label: **Ask ChatGPT to use web search (hint only)**
|
||||||
|
- Help text: "A hint to ChatGPT's interface, not a guarantee."
|
||||||
|
- Default **off**. The value is hard-coded to `search`; no mode dropdown.
|
||||||
|
|
||||||
|
### Open in
|
||||||
|
|
||||||
|
- Label: **Open in**
|
||||||
|
- Options: `A new tab` / `A new background tab` / `A new window`
|
||||||
|
- Default `A new tab`.
|
||||||
|
|
||||||
|
### Prompt template
|
||||||
|
|
||||||
|
- Label: **Prompt template**
|
||||||
|
- Default `{query}`
|
||||||
|
- Help text: "`{query}` is replaced with your selected text."
|
||||||
|
|
||||||
|
### Panel behaviour
|
||||||
|
|
||||||
|
Auto-save on change with a transient "Saved" indicator; no Save button. Writes
|
||||||
|
are debounced at 400 ms so typing in a text field cannot approach any
|
||||||
|
write-rate quota.
|
||||||
|
|
||||||
|
A **Restore defaults** button resets every key to `DEFAULT_SETTINGS`.
|
||||||
|
|
||||||
|
The panel subscribes to `browser.storage.sync.onChanged` and updates its
|
||||||
|
controls live, so a remote sync landing while the panel is open does not get
|
||||||
|
clobbered by stale field values.
|
||||||
|
|
||||||
|
The embedded surface inside `about:addons` is narrow and inherits that page's
|
||||||
|
layout. The panel must therefore be responsive down to roughly 400 px, must not
|
||||||
|
depend on window sizing, and must remain usable as a standalone page — the
|
||||||
|
`extensions.htmlaboutaddons.inline-options.enabled` pref can degrade it to a
|
||||||
|
tab. Because `browser_style` is off, the panel is hand-styled and **must
|
||||||
|
support dark theme via `prefers-color-scheme`**; `about:addons` follows the
|
||||||
|
system theme.
|
||||||
|
|
||||||
|
## URL construction
|
||||||
|
|
||||||
|
`buildChatGptUrl(settings, rawQuery) -> string` in `lib/build-url.js`. Pure.
|
||||||
|
|
||||||
|
```
|
||||||
|
BASE = "https://chatgpt.com/"
|
||||||
|
MAX_URL_CHARS = 8000
|
||||||
|
```
|
||||||
|
|
||||||
|
The root path is mandatory. `chatgpt.com/search?q=` began returning 404 in
|
||||||
|
mid-2025; the root form is what OpenAI's own extension uses.
|
||||||
|
|
||||||
|
### Algorithm
|
||||||
|
|
||||||
|
1. **Build the prompt.** Let `q = String(rawQuery ?? "").trim()`.
|
||||||
|
- If `q === ""` → `prompt = ""` (the template is ignored entirely).
|
||||||
|
- Else if the template contains `{query}` → replace **every** occurrence
|
||||||
|
(`replaceAll`).
|
||||||
|
- Else if the trimmed template is empty → `prompt = q`.
|
||||||
|
- Else → `prompt = trimmedTemplate + " " + q` (template treated as a prefix;
|
||||||
|
the selection is never silently discarded).
|
||||||
|
|
||||||
|
2. **Measure the non-`q` budget.** Build a `URL` from `BASE`, append `model`,
|
||||||
|
`temporary-chat`, and `hints` per the rules below, and record
|
||||||
|
`baseLength = url.href.length`.
|
||||||
|
|
||||||
|
3. **Fit the prompt.** If `prompt !== ""`, find the largest number of *code
|
||||||
|
points* of `prompt` (via binary search over `Array.from(prompt)`, so
|
||||||
|
surrogate pairs are never split) whose resulting full URL length is
|
||||||
|
`<= MAX_URL_CHARS`. Truncation operates on the decoded string and encoding
|
||||||
|
happens afterwards, so a percent-escape can never be severed. If the kept
|
||||||
|
text contains whitespace within its final 10%, cut at the last such
|
||||||
|
whitespace instead, for a cleaner break.
|
||||||
|
|
||||||
|
4. **Assemble in final order.** Build a fresh `URL` and set parameters in this
|
||||||
|
order, matching the most-cited working examples: `q` (if non-empty), then
|
||||||
|
`model`, `temporary-chat`, `hints`.
|
||||||
|
|
||||||
|
Measuring in step 2 without `q` and assembling with `q` first is safe: total
|
||||||
|
serialized length is independent of parameter order, since reordering only
|
||||||
|
swaps which separator is `?` and which are `&`, never how many there are.
|
||||||
|
|
||||||
|
5. Return `url.href`.
|
||||||
|
|
||||||
|
### Parameter rules
|
||||||
|
|
||||||
|
- `q` — set only when the fitted prompt is non-empty. Never emit `q=` with an
|
||||||
|
empty value.
|
||||||
|
- `model` — set to `settings.model.trim()` only when that is non-empty.
|
||||||
|
- `temporary-chat` — set to the string `"true"` only when `temporaryChat` is true.
|
||||||
|
- `hints` — set to the string `"search"` only when `webSearch` is true.
|
||||||
|
|
||||||
|
`q` is the parameter to send. ChatGPT canonicalises it to `prompt=` internally;
|
||||||
|
`prompt=` is the first fallback to try should `q` ever stop working. Never send
|
||||||
|
both.
|
||||||
|
|
||||||
|
## Data flow
|
||||||
|
|
||||||
|
1. **Trigger.** `browser.menus.onClicked` supplies `info.selectionText`, or
|
||||||
|
`browser.commands.onCommand` supplies no query.
|
||||||
|
2. `settings = await load()` — `load()` already pipes the raw
|
||||||
|
`storage.sync.get()` result through `mergeSettings()`, so callers always
|
||||||
|
receive a complete, validated settings object and never need to merge again.
|
||||||
|
3. `url = buildChatGptUrl(settings, query)`.
|
||||||
|
4. Open per `settings.openIn`:
|
||||||
|
- `new-tab` → `browser.tabs.create({ url, active: true })`
|
||||||
|
- `background-tab` → `browser.tabs.create({ url, active: false })`
|
||||||
|
- `new-window` → `browser.windows.create({ url })`
|
||||||
|
|
||||||
|
`tabs.create` and `windows.create` require no permissions. The created `Tab`
|
||||||
|
object's `url`/`title` must not be read — that *would* require the `tabs`
|
||||||
|
permission, which carries an install warning.
|
||||||
|
|
||||||
|
The menu item is registered with `contexts: ["selection"]`, so Firefox shows it
|
||||||
|
only when text is selected.
|
||||||
|
|
||||||
|
## Error handling
|
||||||
|
|
||||||
|
Every path resolves to something useful. No path shows an error to the user.
|
||||||
|
|
||||||
|
| Case | Behaviour |
|
||||||
|
|---|---|
|
||||||
|
| Selection empty or whitespace | Omit `q` entirely; open a blank chat with the other settings applied. |
|
||||||
|
| Prompt exceeds the URL budget | Truncate per step 3 above. Never sever a percent-escape or split a surrogate pair. |
|
||||||
|
| `storage.sync` rejects (quota, or a sanitized backend error) | `try`/`catch` → fall back to `DEFAULT_SETTINGS`. Never block opening ChatGPT. Do not branch on error type: Firefox sanitizes all non-quota backend errors to `"An unexpected error occurred"`. |
|
||||||
|
| Fresh or signed-out profile returns `{}` | `mergeSettings` layers stored values over `DEFAULT_SETTINGS`, so `{}` yields defaults. |
|
||||||
|
| Template lacks `{query}` | Treat as a prefix and append. |
|
||||||
|
| Template has several `{query}` | Replace all occurrences. |
|
||||||
|
| `model` blank or whitespace | Omit the parameter. |
|
||||||
|
| Stored settings corrupt or partial | `mergeSettings` drops unknown keys and falls back to default on wrong type or out-of-range enum. |
|
||||||
|
| Background event page wakes and re-registers | `menus.removeAll()` before `menus.create()`, so re-registration is idempotent. |
|
||||||
|
| `windows.create` fails | Fall back to `tabs.create`. |
|
||||||
|
| `browser.commands` undefined | Guard before subscribing. The API does not exist on Firefox for Android. |
|
||||||
|
|
||||||
|
`storage.sync` needs no capability probe: it works with no Mozilla account,
|
||||||
|
silently behaving as local storage, and syncs later if the user signs in and has
|
||||||
|
Add-ons sync enabled. The `gecko.id` (already present) is required and **must
|
||||||
|
never change** — synced data is keyed by extension ID.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### Unit — `node --test`
|
||||||
|
|
||||||
|
`test/build-url.test.js`:
|
||||||
|
- Defaults + a simple query → exactly `https://chatgpt.com/?q=hello`
|
||||||
|
- Each parameter appears only when its setting is enabled
|
||||||
|
- All four URL-affecting settings together (`model`, `temporaryChat`,
|
||||||
|
`webSearch`, `promptTemplate`), with parameters in the specified order.
|
||||||
|
`openIn` never influences the URL and must not appear in these assertions.
|
||||||
|
- Encoding of `&`, `#`, `?`, `=`, `+`, spaces, newlines, emoji, CJK
|
||||||
|
- `{query}` substitution: present, absent (prefix fallback), multiple occurrences,
|
||||||
|
empty template
|
||||||
|
- Empty / whitespace-only query → no `q` parameter at all
|
||||||
|
- Truncation: result `<= MAX_URL_CHARS`; no severed percent-escape; no split
|
||||||
|
surrogate pair (assert with an emoji at the boundary); whitespace-boundary
|
||||||
|
preference
|
||||||
|
- `model` empty and whitespace-only → no `model` key
|
||||||
|
|
||||||
|
`test/settings.test.js` — `mergeSettings` with: `{}`, `null`, `undefined`,
|
||||||
|
missing keys, unknown keys, wrong types, out-of-range `openIn` value.
|
||||||
|
|
||||||
|
### Manual
|
||||||
|
|
||||||
|
Retain the three existing regression checks from `AGENTS.md`: `gpt <query>`
|
||||||
|
opens ChatGPT, the provider stays non-default, and it remains configurable in
|
||||||
|
Firefox search settings.
|
||||||
|
|
||||||
|
Add:
|
||||||
|
- The Options/Preferences tab appears on the extension's detail card in
|
||||||
|
`about:addons`, and all five settings controls plus **Restore defaults**
|
||||||
|
render correctly — **in both light and dark theme**, and at a narrow width
|
||||||
|
- The page is also usable standalone via `browser.runtime.openOptionsPage()`
|
||||||
|
- Settings persist across a browser restart
|
||||||
|
- The context-menu item appears only when text is selected, and is absent otherwise
|
||||||
|
- `Alt+Shift+G` opens a new chat, appears under `about:addons` → gear → Manage
|
||||||
|
Extension Shortcuts, and can be rebound
|
||||||
|
- Each of the three `openIn` values behaves correctly
|
||||||
|
- **A packaged build's install prompt gains no new lines.** This cannot be
|
||||||
|
checked with `npm run dev` — install prompts are suppressed for temporary
|
||||||
|
installs. Use a packaged build.
|
||||||
|
- `unzip -l web-ext-artifacts/*.zip` shows no `test/` or `docs/` entries, not
|
||||||
|
even empty directory entries
|
||||||
|
|
||||||
|
### Parameter reality check
|
||||||
|
|
||||||
|
One pass in a real, logged-in Firefox profile to record which parameters are
|
||||||
|
actually honoured on this extension's navigation path: does `q` prefill only or
|
||||||
|
also submit; is `model` respected alongside `q`; does `temporary-chat` produce a
|
||||||
|
temporary chat and does it survive the first message; does `hints=search` engage
|
||||||
|
web search.
|
||||||
|
|
||||||
|
No further desk research substitutes for this. **The outcome adjusts the help
|
||||||
|
text, not the architecture** — every label is already written at hint strength,
|
||||||
|
so a negative result requires no redesign.
|
||||||
|
|
||||||
|
## Packaging
|
||||||
|
|
||||||
|
`web-ext-config.mjs` — add to `ignoreFiles`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
"docs", "docs/**", "test", "test/**",
|
||||||
|
```
|
||||||
|
|
||||||
|
Both forms per directory are required. A glob alone removes the files but leaves
|
||||||
|
an **empty directory entry** in the ZIP; web-ext's own code pairs the bare name
|
||||||
|
with the glob for exactly this reason.
|
||||||
|
|
||||||
|
`.gitea/` and `.agents/` need **no** entries — web-ext ignores all dotfiles and
|
||||||
|
dot-directories by default. `web-ext-artifacts/**` is likewise already redundant;
|
||||||
|
it stays as-is to keep this diff focused.
|
||||||
|
|
||||||
|
`web-ext lint` scans `lib/` and `test/` with the **same** file filter as
|
||||||
|
`build`. With `lint.warningsAsErrors: true`, one addons-linter warning in a test
|
||||||
|
file is a hard CI failure. Adding `test/` to `ignoreFiles` fixes linting and
|
||||||
|
packaging together — it is one lever, not two.
|
||||||
|
|
||||||
|
`web-ext-config.mjs` must keep its `.mjs` extension. web-ext rejects a `.js`
|
||||||
|
config file outright.
|
||||||
|
|
||||||
|
## Tooling
|
||||||
|
|
||||||
|
`package.json`:
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
"type": "module", // NEW — required, see below
|
||||||
|
"scripts": {
|
||||||
|
"test:unit": "node --test", // NEW
|
||||||
|
"test": "npm run lint && npm run test:unit && npm run build"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`"type": "module"` is **required**, not stylistic. Without it, `node --test`
|
||||||
|
importing an ES-module `lib/*.js` relies on Node's module-syntax detection,
|
||||||
|
which:
|
||||||
|
|
||||||
|
- emits a permanent `MODULE_TYPELESS_PACKAGE_JSON` warning on every run, and
|
||||||
|
- **fails outright on Node 22.0–22.6**, since detection only became default in
|
||||||
|
22.7. `engines.node` says `>=22` and CI pins the bare major `"22"`, leaving
|
||||||
|
the patch to runner resolution — so without this field the project declares
|
||||||
|
support for a range in which its own suite does not run.
|
||||||
|
|
||||||
|
Using `.mjs` for test files only does **not** fix this; the typeless
|
||||||
|
`lib/*.js` is the ambiguous file. Adding `"type": "module"` was verified not to
|
||||||
|
break `web-ext lint` or `web-ext build` on web-ext 10.5.0, which never consults
|
||||||
|
the consumer package's `type` field.
|
||||||
|
|
||||||
|
Ordering is lint → unit → build, so unit failures prevent a ZIP from being
|
||||||
|
produced. The existing `.gitea` workflow calls `npm test` and needs no edit.
|
||||||
|
|
||||||
|
`node --test` **exits 0 when it discovers zero test files.** CI would go green
|
||||||
|
on a misconfigured path, so acceptance must assert a non-zero test count, not
|
||||||
|
merely a zero exit code.
|
||||||
|
|
||||||
|
Note for implementers: local Node here is v26.5.0 while CI pins 22. Local green
|
||||||
|
does not imply CI green.
|
||||||
|
|
||||||
|
## Documentation changes
|
||||||
|
|
||||||
|
- **README** — an Options section listing the five settings and how to reach the
|
||||||
|
panel (referring to "Options (Windows) / Preferences (macOS, Linux)", since
|
||||||
|
Firefox's label is platform-dependent). Privacy section amended: settings are
|
||||||
|
stored in the user's own browser profile and, only if that user has Firefox
|
||||||
|
Sync with Add-ons enabled, synced end-to-end encrypted through their own
|
||||||
|
Mozilla account. Do **not** write "never leaves your device" unconditionally.
|
||||||
|
Nothing reaches Zavage Software, so the zero-data-collection claim stands.
|
||||||
|
- **AGENTS.md** — update Project Structure for the new directories, Build/Test
|
||||||
|
commands for `test:unit`, and the Testing Guidelines manual checklist.
|
||||||
|
- **CHANGELOG.md** — entry for the release.
|
||||||
|
- **Version** — `manifest.json` `1.0` → `1.1`; `package.json` `1.0.0` → `1.1.0`.
|
||||||
|
|
||||||
|
## Rejected alternatives
|
||||||
|
|
||||||
|
| Alternative | Why rejected |
|
||||||
|
|---|---|
|
||||||
|
| Rewrite the `gpt` keyword's navigation to honour settings | Needs host access to `chatgpt.com` plus `webRequest`/`tabs`; adds install-prompt lines. |
|
||||||
|
| Point `search_url` at a bundled extension page that redirects | Firefox requires an http(s) search URL; also a visible redirect flash. Not pursued. |
|
||||||
|
| Keyboard shortcut reads the page selection | Requires `activeTab` + `tabs.executeScript`. No lighter route exists — a declared `<all_urls>` content script is strictly worse. Excluded by decision; revisitable as its own change. |
|
||||||
|
| `background.page` HTML shim for ES modules | Works, but unnecessary once `background.type: "module"` was confirmed for MV2. |
|
||||||
|
| `background.scripts` without `type` | Classic scripts; an `import` statement fails with a compile-time `SyntaxError` and silently kills the script. |
|
||||||
|
| Curated model dropdown | Slug lists rot within a quarter and every stale entry fails silently to the account default. |
|
||||||
|
| Migrate to MV3 now | Orthogonal, larger diff. Mozilla has announced no MV2 sunset and promises ≥12 months' notice. |
|
||||||
|
| Toast/notification on failure | The `notifications` permission adds an install warning. All failure paths degrade silently instead. |
|
||||||
|
|
||||||
|
## Risks
|
||||||
|
|
||||||
|
1. **Any ChatGPT parameter may stop working without notice.** Mitigated by
|
||||||
|
hint-strength copy everywhere, independently removable parameters, and no
|
||||||
|
architectural dependence on any of them. The `/search` path's silent move to
|
||||||
|
404 in mid-2025 is the precedent.
|
||||||
|
2. **`model` may never work alongside `q`.** An unresolved report says `q`
|
||||||
|
forces the account default; no 2026 source positively confirms otherwise. The
|
||||||
|
setting is labelled experimental and defaults to empty, so the default
|
||||||
|
configuration is unaffected either way.
|
||||||
|
3. **`Alt+Shift+G` may collide with an OS or window-manager hotkey.** Verified
|
||||||
|
clear of Firefox's own keyset and DevTools only; no registry of other add-ons'
|
||||||
|
shortcuts exists. Mitigated by the context menu remaining a full-featured
|
||||||
|
path and by documenting Manage Extension Shortcuts.
|
||||||
|
4. **MV2's long-term future is not guaranteed.** The strongest Mozilla
|
||||||
|
commitment is dated March 2024 and nothing newer restates it. Mitigated by
|
||||||
|
keeping the code MV3-portable.
|
||||||
|
|
||||||
|
## Appendix A: Verification findings
|
||||||
|
|
||||||
|
Established 2026-07-29 by parallel research with adversarial verification;
|
||||||
|
64 findings, one overturned by its skeptic. Browser claims were verified by
|
||||||
|
loading real test extensions into **Firefox 153.0.1**. Repo claims were verified
|
||||||
|
by running commands against copies under `/tmp`; the repo itself was unmodified.
|
||||||
|
|
||||||
|
### Confirmed empirically in Firefox 153.0.1
|
||||||
|
|
||||||
|
- MV2 `background: {scripts, type: "module"}` loads real ES modules. Supported
|
||||||
|
since Firefox 112 (`bug 1811443`); Firefox's manifest schema puts no
|
||||||
|
`min_manifest_version` on `type`.
|
||||||
|
- MV2 `background: {scripts}` **without** `type` fails on `import` with
|
||||||
|
`"SyntaxError: import declarations may only appear at top level of a module"`.
|
||||||
|
- `options_ui.open_in_tab: false` renders inside `about:addons`
|
||||||
|
(`inline-options-browser`), gated on
|
||||||
|
`extensions.htmlaboutaddons.inline-options.enabled`, which ships `true`.
|
||||||
|
- The tab label is platform-dependent:
|
||||||
|
`preferences-addon-button = { PLATFORM() -> [windows] Options *[other] Preferences }`.
|
||||||
|
- Options pages support `<script type="module">` in both surfaces.
|
||||||
|
|
||||||
|
### Confirmed from Firefox source
|
||||||
|
|
||||||
|
- `PERMISSIONS_WITH_MESSAGE` (26 entries) is the authoritative list of API
|
||||||
|
permissions that add a warning line. `storage`, `menus`, and `activeTab` are
|
||||||
|
all absent; `tabs` is present. **Caveat established by the overturned finding:**
|
||||||
|
it is one of five independent install-prompt inputs (API permissions, host
|
||||||
|
permissions, `DATA_COLLECTION_PERMISSIONS`, site permissions, unsigned
|
||||||
|
warning), so it is not a complete account of install-time warnings.
|
||||||
|
- A fresh install **always** shows a prompt; the no-prompt early-return is gated
|
||||||
|
on `type == "update"`. This extension's prompt today is "Add …" + the
|
||||||
|
data-collection line + the private-windows checkbox — unchanged by this work.
|
||||||
|
- Install prompts are suppressed for temporary/unpacked installs.
|
||||||
|
- `storage.sync` throws only for a *temporary* add-on ID
|
||||||
|
(`enforceNoTemporaryAddon`); the explicit `gecko.id` satisfies it. No account
|
||||||
|
check exists anywhere in the path — the backing store is a plain profile-local
|
||||||
|
`storage-sync-v2.sqlite`. Quotas: 102400 total / 8192 per item / 512 items.
|
||||||
|
Non-quota backend errors are sanitized to `"An unexpected error occurred"`.
|
||||||
|
- Sync transport is per-extension encrypted BSOs over the user's own Firefox
|
||||||
|
Account; extension IDs are hidden behind random GUIDs.
|
||||||
|
- `Ctrl+G` is `key_findAgain` and `Ctrl+Shift+G` is `key_findPrevious`.
|
||||||
|
`Alt+Shift+G` is unused by `browser-sets.inc` and DevTools.
|
||||||
|
- `tabs.create` / `windows.create` declare no permissions; only
|
||||||
|
`Tab.url`/`title`/`favIconUrl` require `tabs`.
|
||||||
|
- `commands` is `version_added: false` on Firefox for Android.
|
||||||
|
- `options_ui.browser_style` is deprecated in **MV3 only** (default flipped in
|
||||||
|
Firefox 115, unsupported in 118). Legal in MV2, where it defaults to `true`.
|
||||||
|
|
||||||
|
### Confirmed empirically against the repo
|
||||||
|
|
||||||
|
- `npx web-ext lint` → 0 errors / 0 warnings / 0 notices, exit 0. Green baseline.
|
||||||
|
- lint and build share one `FileFilter`; `eval()` planted in `lib/` produced
|
||||||
|
`DANGEROUS_EVAL` and exit 1.
|
||||||
|
- web-ext's default ignore list includes `'**/.*'` and `'**/.*/**/*'`, so
|
||||||
|
dotfiles never ship. Confirmed: `.gitea/` and a planted `.agents/note.md` were
|
||||||
|
both absent from a build with no dotfile entries in `ignoreFiles`.
|
||||||
|
- `test/` and `docs/` **do** ship by default. With only `"test/**"`, empty
|
||||||
|
`test/` and `docs/` directory entries remained in the ZIP; adding the bare
|
||||||
|
names removed them.
|
||||||
|
- The current shipped ZIP contains only `manifest.json`, `LICENSE.txt`, and
|
||||||
|
`icons/` — packaging hygiene is a property to preserve, not a bug to fix.
|
||||||
|
- `node --test` with no `"type"` field: passes on Node 26.5.0 but warns
|
||||||
|
`MODULE_TYPELESS_PACKAGE_JSON`; with `--no-experimental-detect-module` (pre-22.7
|
||||||
|
behaviour) it **fails** with `"Cannot use import statement outside a module"`.
|
||||||
|
- `.mjs` for tests only moves the warning to the typeless `lib/*.js`.
|
||||||
|
- Adding `"type": "module"`: `node --test` clean, `web-ext lint` 0/0/0,
|
||||||
|
`web-ext build` succeeded.
|
||||||
|
- `node --test` with zero discovered tests: `tests 0`, exit **0**.
|
||||||
|
- Local Node v26.5.0, npm 12.0.1, web-ext 10.5.0. CI pins node-version `"22"`.
|
||||||
|
|
||||||
|
### ChatGPT parameters — confidence ratings
|
||||||
|
|
||||||
|
| Parameter | Rating | Basis |
|
||||||
|
|---|---|---|
|
||||||
|
| `q` prefills | **Solid** | Dated 2026 sources; maintained tooling; OpenAI's own extension uses it; a live probe returned HTTP 200 with all parameters preserved. |
|
||||||
|
| `q` auto-submits | **Refuted** | Prefill-only in every dated report since 2025-11-11. Tenable TRA-2025-22 documents OpenAI gating auto-submit on `Sec-Fetch-Site`. |
|
||||||
|
| `model` honoured alongside `q` | **Uncertain** | An unresolved 2024-12-28 report says `q` forces GPT-4o regardless; counter-evidence exists from 2024–2025; no 2026 source positively confirms. Maintained tooling grades it experimental. |
|
||||||
|
| Valid `model` slugs | **Uncertain** | No authoritative list; sightings rot (`gpt-4`, `o1-preview`, `gpt-4o`, `gpt-5-thinking`, `auto`, `instant`, `thinking`). |
|
||||||
|
| `temporary-chat=true` | **Uncertain** | Community-confirmed originally; multiple reports of it not applying or reverting after the first message; OpenAI reworking the feature. |
|
||||||
|
| `hints=search` is a hint only | **Confirmed** | Multiple independent sources; "a frontend user interface hint, not a backend trigger". |
|
||||||
|
| `hints=search` still functions | **Uncertain** | Freshest positive source is provably recycling stale material; freshest negative is 2025-11-11. Costs nothing to append and degrades to a normal chat. |
|
||||||
|
| Stable set of `hints` values | **Refuted** | Published value sets contradict each other; only `search` has cross-source corroboration. |
|
||||||
|
| Officially documented by OpenAI | **Refuted** | No documentation for any parameter; primary community threads have zero staff replies. |
|
||||||
|
| `/search` path | **Refuted** | Began returning 404 mid-2025. |
|
||||||
|
| `q` canonicalises to `prompt=` | **Confirmed** | Dated 2026 reports; `prompt=` is the fallback if `q` dies. |
|
||||||
|
| Combining all four | **Structurally safe** | Nothing errors or gets stripped. But "the URL loads" is not "the parameter was honoured". |
|
||||||
|
|
||||||
|
### Unresolved
|
||||||
|
|
||||||
|
- Whether Firefox search-provider navigations land in the favoured
|
||||||
|
`Sec-Fetch-Site: none` bucket for auto-submit. Inference from the Fetch
|
||||||
|
Metadata spec, contradicted by a dated report of a default-search string
|
||||||
|
losing auto-submit. Settled only by the manual parameter reality check.
|
||||||
|
- Whether Firefox enforces Chrome-style `storage.sync` write-rate quotas. The
|
||||||
|
constants exist in the schema but no rate-limiting logic appears in the Rust
|
||||||
|
backend. Made moot by debouncing writes.
|
||||||
|
- Whether `Alt+Shift+G` collides with OS-level or other-add-on shortcuts.
|
||||||
@ -8,6 +8,12 @@ export default {
|
|||||||
"package.json",
|
"package.json",
|
||||||
"package-lock.json",
|
"package-lock.json",
|
||||||
"web-ext-config.mjs",
|
"web-ext-config.mjs",
|
||||||
|
// Directories need both the bare name and the glob: the glob alone removes
|
||||||
|
// the files but leaves an empty directory entry in the ZIP.
|
||||||
|
"docs",
|
||||||
|
"docs/**",
|
||||||
|
"test",
|
||||||
|
"test/**",
|
||||||
"web-ext-artifacts/**",
|
"web-ext-artifacts/**",
|
||||||
],
|
],
|
||||||
build: {
|
build: {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user