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:
golem 2026-07-29 20:35:25 -06:00
parent 5edb288681
commit f057c66155
2 changed files with 610 additions and 0 deletions

@ -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.022.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 20242025; 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-lock.json",
"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/**",
],
build: {