Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a628a77fb | ||
|
|
8679267d53 | ||
|
|
ff61aff5b9 | ||
|
|
f022f5e20d | ||
|
|
1866846d3b | ||
|
|
82fdf88b80 | ||
|
|
3f4b64374b | ||
|
|
cf5e397d53 | ||
|
|
8052c87c23 | ||
|
|
9f8a295a4f | ||
|
|
5a12663c92 | ||
|
|
384eca7589 | ||
|
|
d96277ebd9 | ||
|
|
269e721fe8 | ||
|
|
39a3ac74f4 | ||
|
|
cb7da3a538 | ||
|
|
519131c21f |
@@ -2,13 +2,31 @@ name: Firefox Extension CI
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
|
# edited: removing the WIP: prefix starts the real run.
|
||||||
|
types: [opened, synchronize, reopened, edited]
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- develop
|
- develop
|
||||||
- main
|
- main
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
# The gate and concurrency use the github.* context, Gitea's alias for gitea.*:
|
||||||
|
# it is the form the pattern was canaried with and the one pr-flow detects.
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
# Cancel superseded PR runs, never a push run: main's push uploads the package.
|
||||||
|
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
validate:
|
validate:
|
||||||
|
# WIP gate: skip PRs titled WIP: so early PRs cost no run per push. Gate on
|
||||||
|
# the title and nothing else, since a skipped job reports success on the
|
||||||
|
# required check (see ~/.claude/docs/gitea-ci-pattern.md).
|
||||||
|
if: >-
|
||||||
|
github.event_name != 'pull_request' || (
|
||||||
|
!startsWith(github.event.pull_request.title, 'WIP:') &&
|
||||||
|
!startsWith(github.event.pull_request.title, '[WIP]')
|
||||||
|
)
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check out repository
|
- name: Check out repository
|
||||||
|
|||||||
@@ -35,6 +35,17 @@ Firefox Desktop is the only current implementation and release target. Do not ad
|
|||||||
|
|
||||||
Use two-space indentation in JSON, YAML, and JavaScript configuration. Preserve logical key grouping in `manifest.json`. Use lowercase, hyphenated filenames and include dimensions in icon names. Keep changes focused and avoid adding runtime dependencies without a concrete need.
|
Use two-space indentation in JSON, YAML, and JavaScript configuration. Preserve logical key grouping in `manifest.json`. Use lowercase, hyphenated filenames and include dimensions in icon names. Keep changes focused and avoid adding runtime dependencies without a concrete need.
|
||||||
|
|
||||||
|
## Options Page
|
||||||
|
|
||||||
|
- Keep every setting. Customization in a lightweight extension is what sets
|
||||||
|
this one apart, so simplify wording and layout, but never remove a setting
|
||||||
|
without the maintainer's agreement. Less-used settings can sit under
|
||||||
|
**More options**.
|
||||||
|
- Write hotkeys as `<Ctrl>+L`: named keys in angle brackets, single characters
|
||||||
|
plain. `options/shortcut.js` swaps in Mac key names.
|
||||||
|
- Say that ChatGPT opens with the text filled in, never that the text is sent,
|
||||||
|
searched, or asked. The options design spec explains why.
|
||||||
|
|
||||||
## Testing Guidelines
|
## Testing Guidelines
|
||||||
|
|
||||||
Unit tests cover the pure logic in `lib/` via `node:test`; automated checks
|
Unit tests cover the pure logic in `lib/` via `node:test`; automated checks
|
||||||
@@ -46,10 +57,12 @@ After `npm test`, use `npm run dev` and confirm:
|
|||||||
|
|
||||||
- `gpt <query>` opens ChatGPT, the provider stays non-default, and Firefox
|
- `gpt <query>` opens ChatGPT, the provider stays non-default, and Firefox
|
||||||
search settings can configure it.
|
search settings can configure it.
|
||||||
- The Options/Preferences tab renders the settings plus Restore defaults,
|
- The Options/Preferences tab renders the drawings, every setting (including
|
||||||
|
those under More options), and Restore defaults,
|
||||||
in both light and dark themes and at a narrow window width, and values
|
in both light and dark themes and at a narrow window width, and values
|
||||||
persist across a restart.
|
persist across a restart. Expanding and collapsing More options and the
|
||||||
- **Context menu → Simple** shows the direct **Ask ChatGPT** item only for
|
collapsed sections resizes the inline panel without clipping or scrolling.
|
||||||
|
- **Right-click menu → Simple** shows the direct **Ask ChatGPT** item only for
|
||||||
selected text. **Advanced** shows the contextual submenu; selection takes
|
selected text. **Advanced** shows the contextual submenu; selection takes
|
||||||
precedence over links. **Off** hides the menu. Switch modes without restarting
|
precedence over links. **Off** hides the menu. Switch modes without restarting
|
||||||
and confirm Alt+Shift+G still works in every mode. Restore defaults returns
|
and confirm Alt+Shift+G still works in every mode. Restore defaults returns
|
||||||
@@ -74,7 +87,7 @@ After `npm test`, use `npm run dev` and confirm:
|
|||||||
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
|
- Click the **Ask ChatGPT** toolbar button with no selection, a top-frame
|
||||||
selection, and an iframe selection after a stale top-frame selection.
|
selection, and an iframe selection after a stale top-frame selection.
|
||||||
Check restricted-page fallback, context menu Off, saved prompt and opening
|
Check restricted-page fallback, right-click menu Off, saved prompt and opening
|
||||||
preferences, and consent before shortening long selections. Test both the
|
preferences, and consent before shortening long selections. Test both the
|
||||||
pinned toolbar button and its entry in Firefox’s extensions menu.
|
pinned toolbar button and its entry in Firefox’s extensions menu.
|
||||||
|
|
||||||
|
|||||||
+27
-10
@@ -2,28 +2,45 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version).
|
All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version).
|
||||||
|
|
||||||
## Unreleased
|
## [1.3.0] (2026-10-18)
|
||||||
|
|
||||||
|
- Use the extension's bundled icon for the ChatGPT search engine.
|
||||||
|
- Rewrite the options page for first-time users: usage instructions with a
|
||||||
|
small drawing for each way to use the extension, a preview of the Advanced
|
||||||
|
right-click menu, plainer wording, and the prompt template, temporary chat,
|
||||||
|
and web search settings under More options. Hotkeys are written as
|
||||||
|
`<Ctrl>+L`, with Mac key names on a Mac.
|
||||||
|
- Correct the keyword instructions: a custom keyword works alongside `gpt`
|
||||||
|
rather than replacing it.
|
||||||
|
|
||||||
|
## [1.2.0] (2026-10-18)
|
||||||
|
|
||||||
|
The first update on addons.mozilla.org since 1.0. It also delivers everything
|
||||||
|
in 1.1.0, which was never published there.
|
||||||
|
|
||||||
- Add an Ask ChatGPT toolbar button: use selected text or open a blank chat,
|
- Add an Ask ChatGPT toolbar button: use selected text or open a blank chat,
|
||||||
honoring saved prompt and tab/window preferences. No new permissions.
|
honoring saved prompt and tab/window preferences. No new permissions.
|
||||||
|
Requires Firefox 112 or newer, like the other new entry points.
|
||||||
- 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.
|
||||||
- Preserve dedicated action instructions when shortening selections; ask before
|
- Preserve dedicated action instructions when shortening selections; ask before
|
||||||
sending shortened text and reject URLs that cannot be sent intact.
|
sending shortened text and reject URLs that cannot be sent intact.
|
||||||
|
- Stay responsive with very large selections from the shortcut or toolbar: only
|
||||||
|
the part that can fit in the ChatGPT link is processed.
|
||||||
- Add accessible action dialogs with cancellation and retry, remove the temporary
|
- Add accessible action dialogs with cancellation and retry, remove the temporary
|
||||||
model/effort URL probe, and document URL sharing. No new permissions.
|
model/effort URL probe, and document URL sharing. No new permissions.
|
||||||
- Show up front, in the options panel and README, that ChatGPT may require
|
- Show up front, in the options panel and README, that ChatGPT may require
|
||||||
signing in and that an unsent prompt needs Enter or Send.
|
signing in and that an unsent prompt needs Enter or Send.
|
||||||
|
- Limit the prompt template to 1,000 characters, so an oversized template can
|
||||||
|
no longer stop every other setting from saving.
|
||||||
* Options instructions now show the configured keyboard shortcut, update when
|
- Show the configured keyboard shortcut in the options instructions, updating
|
||||||
it changes, and indicate when no shortcut is assigned. Added a button to
|
when it changes and indicating when none is assigned, with a button to open
|
||||||
open Firefox’s shortcut editor where supported.
|
Firefox’s shortcut editor where supported.
|
||||||
* Shortened the context menu label to “Ask ChatGPT”.
|
- Shorten the context menu label to “Ask ChatGPT”.
|
||||||
* Added quick usage instructions and guidance for changing the search keyword
|
- Add quick usage instructions and guidance for changing the search keyword
|
||||||
and default engine in Firefox to the options panel.
|
and default engine in Firefox to the options panel.
|
||||||
* Added a “Feedback or report a bug” link to the options panel and README.
|
- Add a “Feedback or report a bug” link to the options panel and README.
|
||||||
|
|
||||||
## [1.1.0] (2026-07-30)
|
## [1.1.0] (2026-07-30)
|
||||||
|
|
||||||
@@ -44,7 +61,7 @@ Adds an options panel and two new ways to reach ChatGPT.
|
|||||||
* Permissions added: storage, menus, activeTab. None of them adds a line to
|
* Permissions added: storage, menus, activeTab. None of them adds a line to
|
||||||
Firefox's install prompt.
|
Firefox's install prompt.
|
||||||
|
|
||||||
## [1.0.0]) (2024-06-09)
|
## [1.0.0] (2024-06-09)
|
||||||
|
|
||||||
First release of software.
|
First release of software.
|
||||||
|
|
||||||
|
|||||||
@@ -31,23 +31,24 @@ 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
|
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
|
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
|
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.
|
tab/window preferences, and works even when the right-click menu is Off.
|
||||||
|
|
||||||
The options panel shows your currently configured shortcut and refreshes when
|
The options panel's collapsed **Change the keyboard shortcut** section shows
|
||||||
it changes or you return to the panel. If you remove it, the 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
|
“No keyboard shortcut assigned”. Its **Change keyboard shortcut** button opens
|
||||||
Firefox’s shortcut editor when supported; otherwise follow the menu path above.
|
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, open **More options** and set **Prompt template** to something like
|
||||||
`Explain simply: {query}` or `Translate into Spanish: {query}`. The Simple menu action, Ask normally, toolbar button, and 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
|
||||||
|
|
||||||
Open Firefox’s **Settings → Search**, or paste `about:preferences#search` into
|
Open Firefox’s **Settings → Search**, or paste `about:preferences#search` into
|
||||||
the address bar. Under **Search Shortcuts**, edit ChatGPT’s keyword to replace
|
the address bar. Under **Search Shortcuts**, edit ChatGPT’s keyword to add
|
||||||
`gpt` with your preferred prefix. Under **Default Search Engine**, choose
|
your own prefix; `gpt` keeps working alongside it. Under **Default Search Engine**, choose
|
||||||
ChatGPT to search without a prefix, or choose your previous engine to switch
|
ChatGPT to search without a prefix, or choose your previous engine to switch
|
||||||
back. See [Firefox’s search settings guide](https://support.mozilla.org/en-US/kb/change-your-default-search-settings-firefox)
|
back. See [Firefox’s search settings guide](https://support.mozilla.org/en-US/kb/change-your-default-search-settings-firefox)
|
||||||
for illustrated instructions.
|
for illustrated instructions.
|
||||||
@@ -60,20 +61,25 @@ not change them.
|
|||||||
Open `about:addons`, select this extension, and choose the **Options** tab
|
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;
|
- **Right-click menu** — **Simple** (default) keeps the direct selection action;
|
||||||
**Advanced** offers contextual presets; **Off** hides the menu. The toolbar
|
**Advanced** offers contextual presets; **Off** hides the menu. The toolbar
|
||||||
button and keyboard shortcut work 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 the 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**
|
||||||
to translate directly. An empty or invalid preferred language falls back to
|
to translate directly. An empty or invalid preferred language falls back to
|
||||||
asking. Enter continues; Escape or Cancel opens nothing.
|
asking. Enter continues; Escape or Cancel opens nothing.
|
||||||
|
- **Open ChatGPT in** — a new tab, a new background tab, or a new window.
|
||||||
|
|
||||||
|
Under **More options**, which opens by itself once you have changed one of
|
||||||
|
them:
|
||||||
|
|
||||||
|
- **Prompt template** — wraps your text, for example `Explain simply: {query}`.
|
||||||
|
Up to 1,000 characters.
|
||||||
- **Request a temporary chat** — asks ChatGPT not to save the conversation.
|
- **Request a temporary chat** — asks ChatGPT not to save the conversation.
|
||||||
ChatGPT may not honour this; check its own indicator before relying on it.
|
ChatGPT may not honour this; check its own indicator before relying on it.
|
||||||
- **Ask ChatGPT to use web search** — a hint to ChatGPT's interface, not a
|
- **Ask ChatGPT to use web search** — a hint to ChatGPT's interface, not a
|
||||||
guarantee.
|
guarantee.
|
||||||
- **Open in** — a new tab, a new background tab, or a new window.
|
|
||||||
- **Prompt template** — wraps your text, for example `Explain simply: {query}`.
|
|
||||||
|
|
||||||
Restore defaults resets extension preferences to Simple mode with translation
|
Restore defaults resets extension preferences to Simple mode with translation
|
||||||
hidden; it does not change Firefox's search engine or shortcut settings.
|
hidden; it does not change Firefox's search engine or shortcut settings.
|
||||||
|
|||||||
@@ -96,6 +96,21 @@ send. The hypothesis that address-bar navigation might still be favoured
|
|||||||
(`Sec-Fetch-Site: none` being the most-trusted value) **does not hold in
|
(`Sec-Fetch-Site: none` being the most-trusted value) **does not hold in
|
||||||
practice**.
|
practice**.
|
||||||
|
|
||||||
|
**Correction, 2026-10-18: `q` auto-submits again.**
|
||||||
|
- **Signed out:** in headless Firefox 157, `chatgpt.com/?q=` sent the prompt
|
||||||
|
and ChatGPT answered with no keypress. This happened even when an unrelated
|
||||||
|
page navigated to the URL (`Sec-Fetch-Site: cross-site`).
|
||||||
|
- **Maintainer's Firefox:** the same day, the maintainer reported that prompts
|
||||||
|
were sent without pressing Enter.
|
||||||
|
- **A week earlier:** on 2026-10-12 the signed-out path showed a verification
|
||||||
|
error and the signed-in path still needed Enter
|
||||||
|
(`docs/toolbar-verification.md`).
|
||||||
|
|
||||||
|
So this changed within a week and may differ between accounts. Treat both
|
||||||
|
behaviours as possible. The copy rule below still applies, and the README
|
||||||
|
already says ChatGPT decides whether a prompt is sent. Appendix B covers what
|
||||||
|
this means for selection smuggling.
|
||||||
|
|
||||||
No copy in the options panel, README, or AMO listing may say the query is
|
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
|
*searched*, *sent*, or *asked* — only that ChatGPT opens **with the query filled
|
||||||
in**. The two-keypress behaviour is an upstream ChatGPT change, not a defect in
|
in**. The two-keypress behaviour is an upstream ChatGPT change, not a defect in
|
||||||
@@ -698,7 +713,7 @@ by running commands against copies under `/tmp`; the repo itself was unmodified.
|
|||||||
| Parameter | Rating | Basis |
|
| 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` 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, and confirmed by first-hand test** | Prefill-only in every dated report since 2025-11-11. Tenable TRA-2025-22 documents OpenAI gating auto-submit on `Sec-Fetch-Site`. Reproduced 2026-07-29 in the maintainer's logged-in Firefox via this extension's own search-provider path. |
|
| `q` auto-submits | **Varies; auto-submitting again as of 2026-10-18** | Prefill-only in every dated report from 2025-11-11, when Tenable TRA-2025-22 documented OpenAI gating auto-submit on `Sec-Fetch-Site`, and reproduced 2026-07-29 in the maintainer's logged-in Firefox. On 2026-10-18 it auto-submitted signed out, including after a cross-site navigation, and the maintainer reported the same in their own Firefox. See the correction under the `q` section. |
|
||||||
| `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. |
|
| `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`). |
|
| 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. |
|
| `temporary-chat=true` | **Uncertain** | Community-confirmed originally; multiple reports of it not applying or reverting after the first message; OpenAI reworking the feature. |
|
||||||
@@ -790,6 +805,16 @@ composer until a human presses Enter. **Auto-submit deletes that checkpoint.**
|
|||||||
This is also why the context menu and shortcut remain safe *as long as they only
|
This is also why the context menu and shortcut remain safe *as long as they only
|
||||||
prefill*.
|
prefill*.
|
||||||
|
|
||||||
|
> **2026-10-18:** ChatGPT itself now auto-submits `?q=` prompts, at least for
|
||||||
|
> some accounts (see the correction under the `q` section). While that holds,
|
||||||
|
> the composer checkpoint above is gone for the context menu, toolbar button and
|
||||||
|
> shortcut alike: hidden text in a selection can reach an answered prompt
|
||||||
|
> without review. The extension still adds no submission of its own, and this
|
||||||
|
> appendix's rejection stands. 1.2.0 ships with the behaviour known. Possible
|
||||||
|
> mitigations, such as framing the default Ask prompt as data the way the preset
|
||||||
|
> actions already do, or an option to review page-sourced text first, are open
|
||||||
|
> follow-ups rather than part of this design.
|
||||||
|
|
||||||
**`webNavigation.transitionType` is not a security boundary.** The signal looks
|
**`webNavigation.transitionType` is not a security boundary.** The signal looks
|
||||||
right — a `gpt` search emits `transitionType: "generated"` with
|
right — a `gpt` search emits `transitionType: "generated"` with
|
||||||
`transitionQualifiers: ["from_address_bar"]`, and content cannot forge those
|
`transitionQualifiers: ["from_address_bar"]`, and content cannot forge those
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
# Options page for first-time users
|
||||||
|
|
||||||
|
Status: implemented on `feat/options-page-cleanup` (PR #23).
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Make the options page usable by people who are not comfortable with computers,
|
||||||
|
without losing any setting. Customization in a lightweight extension is what
|
||||||
|
sets this one apart, so the page simplifies wording and layout, never features.
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
1. **Description**, no heading: "This extension adds ChatGPT as a Firefox
|
||||||
|
search engine."
|
||||||
|
2. **Usage instructions**: one labelled line per way to open ChatGPT, each with
|
||||||
|
one drawing:
|
||||||
|
- Address bar
|
||||||
|
- Right-click
|
||||||
|
- Keyboard (hidden when no shortcut is assigned)
|
||||||
|
- Toolbar
|
||||||
|
|
||||||
|
A small tip follows: ChatGPT opens with the text filled in, press
|
||||||
|
`<Enter>` if it isn't sent, and sign-in may be needed.
|
||||||
|
3. **Settings**:
|
||||||
|
- Visible: **Right-click menu** (renamed from "Context menu"), its
|
||||||
|
Translate sub-settings, and **Open ChatGPT in**.
|
||||||
|
- Choosing **Advanced** reveals a drawing of the Ask ChatGPT submenu. It
|
||||||
|
sits in the block that only shows for Advanced, so it previews the one
|
||||||
|
setting newcomers find hardest to picture, at no cost to anyone else.
|
||||||
|
- Under **More options**: prompt template, temporary chat, and web search.
|
||||||
|
The summary names all three. The section opens on load, or when a sync
|
||||||
|
changes one of them, if any differs from its default. That way a
|
||||||
|
customised setting never looks lost.
|
||||||
|
- **Restore defaults** sits on the right in red, because it discards every
|
||||||
|
customised setting at once.
|
||||||
|
4. Two collapsed sections:
|
||||||
|
- **Use your own keyword or make ChatGPT your default search engine**, as
|
||||||
|
numbered steps.
|
||||||
|
- **Change the keyboard shortcut**.
|
||||||
|
5. Project links, centred, stacked on narrow panels.
|
||||||
|
|
||||||
|
## Drawings, not screenshots
|
||||||
|
|
||||||
|
Each drawing is HTML, CSS, and a little inline SVG. The only image is the
|
||||||
|
bundled extension icon. The drawings are `aria-hidden`; the text next to them
|
||||||
|
carries the instructions.
|
||||||
|
|
||||||
|
Real screenshots were considered and rejected for the page:
|
||||||
|
- they go stale when Firefox redesigns its address bar or menus;
|
||||||
|
- the right-click menu looks different on Windows, macOS, and Linux;
|
||||||
|
- they need separate light and dark versions;
|
||||||
|
- they must be bundled, since loading them remotely would repeat the problem
|
||||||
|
fixed in #20.
|
||||||
|
|
||||||
|
Size was not the issue. Screenshots would have added roughly 50–150 KB. The
|
||||||
|
whole rewrite, drawings included, added under 4 KB; the package went from
|
||||||
|
52.8 KB to 56.5 KB.
|
||||||
|
Real screenshots belong on the AMO listing instead, which has none.
|
||||||
|
|
||||||
|
The address-bar drawing shows a ChatGPT label after `gpt` and a space. The
|
||||||
|
basis is the code shipped in Firefox 157.0.1, read from its `omni.ja`; it has
|
||||||
|
not yet been checked on screen:
|
||||||
|
- `UrlbarProviderAliasEngines` gives keyword results a `keyword` payload;
|
||||||
|
- `UrlbarInputBase.onFirstResult` enters search mode for such a result.
|
||||||
|
|
||||||
|
If Firefox shows no label, the drawing should show plain text instead.
|
||||||
|
|
||||||
|
The keyboard drawing draws key caps for the user's actual shortcut, which
|
||||||
|
`options/shortcut.js` builds from `commands.getAll()`.
|
||||||
|
|
||||||
|
## Hotkeys
|
||||||
|
|
||||||
|
Hotkeys are written as `<Ctrl>+L`: named keys in angle brackets, single
|
||||||
|
characters plain. This is the maintainer's own convention from the AMO
|
||||||
|
listing. It applies to the static text and to the live shortcut.
|
||||||
|
|
||||||
|
On macOS the page uses Mac key names, detected with
|
||||||
|
`runtime.getPlatformInfo()`, which needs no permission:
|
||||||
|
- `Ctrl` and `Command` become `Cmd`; Firefox reads a manifest's `Ctrl` as
|
||||||
|
Command there (`ShortcutUtils.chromeModifierKeyMap`).
|
||||||
|
- `MacCtrl` becomes `Ctrl`.
|
||||||
|
- `Alt` becomes `Option`.
|
||||||
|
- `Enter` becomes `Return`.
|
||||||
|
|
||||||
|
Shortcuts set in Firefox's shortcut editor on macOS are stored with
|
||||||
|
`MacCtrl`, `Alt`, `Command`, and `Shift` (`addon-shortcuts.mjs`,
|
||||||
|
`getShortcutForEvent`).
|
||||||
|
|
||||||
|
## Copy rules kept
|
||||||
|
|
||||||
|
These come from the
|
||||||
|
[options design](2026-07-29-extension-options-design.md):
|
||||||
|
- Copy says ChatGPT opens with the text filled in, never that it is sent,
|
||||||
|
searched, or asked.
|
||||||
|
- The temporary-chat label and warning are unchanged, with no "private",
|
||||||
|
"incognito", or "secure" near them.
|
||||||
|
- Web search stays labelled as a hint.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
`npm test` covers:
|
||||||
|
- the key formatter, including the macOS key names;
|
||||||
|
- the static hotkeys in `options.html` matching the formatter;
|
||||||
|
- the key caps drawn for the live shortcut.
|
||||||
|
|
||||||
|
Manual checks are listed in `AGENTS.md`.
|
||||||
|
|
||||||
|
To preview the page outside the extension:
|
||||||
|
1. Copy `options/`, `lib/`, and `icons/` to a scratch folder.
|
||||||
|
2. Add a script before `options.js` that defines a stand-in `browser` with
|
||||||
|
`runtime.getPlatformInfo`, `commands`, and `storage`.
|
||||||
|
3. Serve the folder on localhost.
|
||||||
|
4. Run `firefox --headless -no-remote -profile <tmp> --screenshot out.png
|
||||||
|
--window-size=680,1600 <url>`.
|
||||||
|
|
||||||
|
For dark mode, give the throwaway profile
|
||||||
|
`layout.css.prefers-color-scheme.content-override` = 0 and
|
||||||
|
`ui.systemUsesDarkTheme` = 1. This renders with Firefox's own engine, but not
|
||||||
|
inside `about:addons`.
|
||||||
|
|
||||||
|
## Not in scope
|
||||||
|
|
||||||
|
- Screenshots and an updated description for the AMO listing.
|
||||||
|
- Pinning the toolbar button by default (`browser_action.default_area`).
|
||||||
|
- Rewriting the README's hotkeys in the `<Alt>+<Shift>+G` style.
|
||||||
@@ -3,6 +3,12 @@
|
|||||||
|
|
||||||
export const OPEN_IN_VALUES = ["new-tab", "background-tab", "new-window"];
|
export const OPEN_IN_VALUES = ["new-tab", "background-tab", "new-window"];
|
||||||
|
|
||||||
|
// In UTF-16 units, as maxlength counts. Keeps the template far inside
|
||||||
|
// storage.sync's 8192-byte item quota even when every character is a pasted
|
||||||
|
// control character (six bytes once JSON-escaped), and far beyond any template
|
||||||
|
// the 8,000-character URL budget could send.
|
||||||
|
export const MAX_TEMPLATE_CHARS = 1000;
|
||||||
|
|
||||||
export const DEFAULT_SETTINGS = {
|
export const DEFAULT_SETTINGS = {
|
||||||
contextMenuMode: "simple",
|
contextMenuMode: "simple",
|
||||||
showTranslate: false,
|
showTranslate: false,
|
||||||
|
|||||||
+12
-1
@@ -25,12 +25,23 @@ export function prepareAction(request) {
|
|||||||
}
|
}
|
||||||
const resolved = { ...request, language };
|
const resolved = { ...request, language };
|
||||||
const serialize = text => serializePrompt(request.settings, renderPrompt(resolved, text));
|
const serialize = text => serializePrompt(request.settings, renderPrompt(resolved, text));
|
||||||
|
// Each code point adds at least one URL character, so none past the first
|
||||||
|
// MAX_URL_CHARS can fit. Capping first keeps a select-all on a huge page from
|
||||||
|
// costing seconds of re-encoding. A capped source is never sent as ready: its
|
||||||
|
// kept prefix may trim down to something that fits and silently drop the rest.
|
||||||
|
const points = [];
|
||||||
|
let capped = false;
|
||||||
|
for (const point of source) {
|
||||||
|
if (points.length === MAX_URL_CHARS) { capped = true; break; }
|
||||||
|
points.push(point);
|
||||||
|
}
|
||||||
|
if (!capped) {
|
||||||
const url = serialize(source);
|
const url = serialize(source);
|
||||||
if (url.length <= MAX_URL_CHARS) return { kind: 'ready', url };
|
if (url.length <= MAX_URL_CHARS) return { kind: 'ready', url };
|
||||||
|
}
|
||||||
if (isUrl) return error('This URL is too long to send intact. No part of it has been sent.');
|
if (isUrl) return error('This URL is too long to send intact. No part of it has been sent.');
|
||||||
const marker = '\n[Selected text shortened]';
|
const marker = '\n[Selected text shortened]';
|
||||||
if (serialize(marker).length >= MAX_URL_CHARS) return error('The prompt instructions are too long. Shorten your prompt template and try again.');
|
if (serialize(marker).length >= MAX_URL_CHARS) return error('The prompt instructions are too long. Shorten your prompt template and try again.');
|
||||||
const points = Array.from(source);
|
|
||||||
let low = 0, high = points.length;
|
let low = 0, high = points.length;
|
||||||
while (low < high) {
|
while (low < high) {
|
||||||
const mid = Math.ceil((low + high) / 2);
|
const mid = Math.ceil((low + high) / 2);
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
import { DEFAULT_SETTINGS, OPEN_IN_VALUES } from "./defaults.js";
|
import { DEFAULT_SETTINGS, MAX_TEMPLATE_CHARS, OPEN_IN_VALUES } from "./defaults.js";
|
||||||
|
|
||||||
export function normalizeLanguage(value) {
|
export function normalizeLanguage(value) {
|
||||||
if (typeof value !== "string") return "";
|
if (typeof value !== "string") return "";
|
||||||
@@ -28,7 +28,7 @@ export function mergeSettings(stored) {
|
|||||||
if (OPEN_IN_VALUES.includes(stored.openIn)) {
|
if (OPEN_IN_VALUES.includes(stored.openIn)) {
|
||||||
merged.openIn = stored.openIn;
|
merged.openIn = stored.openIn;
|
||||||
}
|
}
|
||||||
if (typeof stored.promptTemplate === "string") {
|
if (typeof stored.promptTemplate === "string" && stored.promptTemplate.length <= MAX_TEMPLATE_CHARS) {
|
||||||
merged.promptTemplate = stored.promptTemplate;
|
merged.promptTemplate = stored.promptTemplate;
|
||||||
}
|
}
|
||||||
return merged;
|
return merged;
|
||||||
|
|||||||
+2
-3
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"name": "Search with ChatGPT powered by OpenAI!",
|
"name": "Search with ChatGPT powered by OpenAI!",
|
||||||
"description": "Adds a ChatGPT search engine",
|
"description": "Adds a ChatGPT search engine, plus a right-click menu, toolbar button and shortcut that open ChatGPT with your selected text.",
|
||||||
"manifest_version": 2,
|
"manifest_version": 2,
|
||||||
"author": "Mathew Guest <mat@zavage.net>",
|
"author": "Mathew Guest <mat@zavage.net>",
|
||||||
"homepage_url": "https://zavage-software.com/portfolio/search-with-ChatGPT",
|
"homepage_url": "https://zavage-software.com/portfolio/search-with-ChatGPT",
|
||||||
"version": "1.1",
|
"version": "1.3",
|
||||||
"icons": {
|
"icons": {
|
||||||
"16": "icons/powered-by-openai-icon-16x16.png",
|
"16": "icons/powered-by-openai-icon-16x16.png",
|
||||||
"32": "icons/powered-by-openai-icon-32x32.png",
|
"32": "icons/powered-by-openai-icon-32x32.png",
|
||||||
@@ -59,7 +59,6 @@
|
|||||||
"name": "ChatGPT",
|
"name": "ChatGPT",
|
||||||
"search_url": "https://chatgpt.com?q={searchTerms}",
|
"search_url": "https://chatgpt.com?q={searchTerms}",
|
||||||
"keyword": "gpt",
|
"keyword": "gpt",
|
||||||
"favicon_url": "https://zavage-software.com/static/ChatGPT-favicon.png",
|
|
||||||
"is_default": false,
|
"is_default": false,
|
||||||
"encoding": "UTF-8"
|
"encoding": "UTF-8"
|
||||||
}
|
}
|
||||||
|
|||||||
+351
-17
@@ -5,6 +5,11 @@
|
|||||||
--border: #8f8f9d;
|
--border: #8f8f9d;
|
||||||
--field-bg: #ffffff;
|
--field-bg: #ffffff;
|
||||||
--accent: #0060df;
|
--accent: #0060df;
|
||||||
|
/* Backdrop for the drawings and inline code. */
|
||||||
|
--surface: #f0f0f4;
|
||||||
|
--tooltip-bg: #15141a;
|
||||||
|
--tooltip-fg: #fbfbfe;
|
||||||
|
--danger: #d70022;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
@@ -15,6 +20,10 @@
|
|||||||
--border: #8f8f9d;
|
--border: #8f8f9d;
|
||||||
--field-bg: #2b2a33;
|
--field-bg: #2b2a33;
|
||||||
--accent: #0df;
|
--accent: #0df;
|
||||||
|
--surface: #1c1b22;
|
||||||
|
--tooltip-bg: #52525e;
|
||||||
|
--tooltip-fg: #fbfbfe;
|
||||||
|
--danger: #ff848b;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,7 +39,270 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.intro {
|
.intro {
|
||||||
margin: 0 0 1.5rem;
|
margin: 0 0 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ways {
|
||||||
|
margin: 0 0 0.75rem;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ways > li {
|
||||||
|
margin-bottom: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
vertical-align: -3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Simplified drawings of Firefox's address bar and right-click menu. They are
|
||||||
|
not screenshots, so they follow the theme and never go out of date. */
|
||||||
|
.drawing {
|
||||||
|
max-width: 24rem;
|
||||||
|
margin: 0.45rem auto 0;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--surface);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawing img {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawing-urlbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.45rem;
|
||||||
|
padding: 0.35rem 0.5rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--field-bg);
|
||||||
|
outline: 2px solid var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawing-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.3rem;
|
||||||
|
padding: 0.1rem 0.45rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: color-mix(in srgb, var(--fg) 10%, var(--field-bg));
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawing-typed {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The text cursor. */
|
||||||
|
.drawing-typed::after {
|
||||||
|
content: "";
|
||||||
|
display: inline-block;
|
||||||
|
width: 1px;
|
||||||
|
height: 1.1em;
|
||||||
|
margin-inline-start: 1px;
|
||||||
|
background: currentColor;
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawing-page {
|
||||||
|
margin: 0 0 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawing mark {
|
||||||
|
color: inherit;
|
||||||
|
background: color-mix(in srgb, var(--accent) 30%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawing-toolbar {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawing-address {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 0.3rem 0.6rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--field-bg);
|
||||||
|
color: var(--fg-dim);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawing-button {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
flex: none;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawing-button svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
fill: none;
|
||||||
|
stroke: currentColor;
|
||||||
|
stroke-width: 1.3;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-linejoin: round;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawing-button-hover {
|
||||||
|
background: color-mix(in srgb, var(--fg) 14%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The mouse pointer resting on the hovered button. */
|
||||||
|
.drawing-button svg.drawing-pointer {
|
||||||
|
position: absolute;
|
||||||
|
top: 16px;
|
||||||
|
left: 17px;
|
||||||
|
width: 12px;
|
||||||
|
height: 18px;
|
||||||
|
fill: #fff;
|
||||||
|
stroke: #000;
|
||||||
|
stroke-width: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawing-tooltip {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + 0.65rem);
|
||||||
|
right: 0;
|
||||||
|
padding: 0.2rem 0.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--tooltip-bg);
|
||||||
|
color: var(--tooltip-fg);
|
||||||
|
white-space: nowrap;
|
||||||
|
box-shadow: 0 1px 4px rgb(0 0 0 / 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Top padding keeps the heading clear of the tooltip at narrow widths. */
|
||||||
|
.drawing-content {
|
||||||
|
margin: 0.6rem 0 0;
|
||||||
|
padding: 1.9rem 0.75rem 1rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--field-bg);
|
||||||
|
font-size: 1.05rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawing-menu {
|
||||||
|
width: 11rem;
|
||||||
|
/* Opens at the pointer, just under the selection. */
|
||||||
|
margin-inline-start: 3.5rem;
|
||||||
|
padding: 0.25rem 0;
|
||||||
|
border: 1px solid color-mix(in srgb, var(--border) 60%, transparent);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--field-bg);
|
||||||
|
box-shadow: 0 2px 8px rgb(0 0 0 / 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawing-menu > div {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.45rem;
|
||||||
|
/* Line the plain items up with the text of the item that has an icon. */
|
||||||
|
padding: 0.2rem 0.75rem 0.2rem calc(0.75rem + 16px + 0.45rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawing-menu > .drawing-menu-active {
|
||||||
|
padding-inline-start: 0.75rem;
|
||||||
|
background: color-mix(in srgb, var(--accent) 20%, var(--field-bg));
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawing-menu hr {
|
||||||
|
margin: 0.25rem 0;
|
||||||
|
border: 0;
|
||||||
|
border-top: 1px solid color-mix(in srgb, var(--border) 50%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The Advanced submenu, open beside the highlighted Ask ChatGPT item. */
|
||||||
|
.drawing-advanced {
|
||||||
|
margin-bottom: 1.15rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawing-menus {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawing-menus > .drawing-menu {
|
||||||
|
flex: none;
|
||||||
|
width: 10rem;
|
||||||
|
margin-inline-start: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawing-menus > .drawing-submenu {
|
||||||
|
width: 8.5rem;
|
||||||
|
/* Lines its first item up with Ask ChatGPT, overlapping the menu edge. */
|
||||||
|
margin-top: 3.75rem;
|
||||||
|
margin-inline-start: -0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Submenu items have no icon, so the highlighted one keeps the icon gap. */
|
||||||
|
.drawing-submenu > .drawing-menu-active {
|
||||||
|
padding-inline-start: calc(0.75rem + 16px + 0.45rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawing-submenu-arrow {
|
||||||
|
margin-inline-start: auto;
|
||||||
|
font-size: 1.2em;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The shortcut's keys, held down. shortcut.js fills these in. */
|
||||||
|
.drawing-keycaps {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.3rem 0;
|
||||||
|
color: var(--fg-dim);
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawing-key {
|
||||||
|
min-width: 1.4rem;
|
||||||
|
padding: 0.35rem 0.65rem;
|
||||||
|
border: 1px solid color-mix(in srgb, var(--accent) 55%, var(--border));
|
||||||
|
border-bottom-width: 3px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: color-mix(in srgb, var(--accent) 12%, var(--field-bg));
|
||||||
|
color: var(--fg);
|
||||||
|
text-align: center;
|
||||||
|
transform: translateY(1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint.intro-hint {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage,
|
||||||
|
.settings {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
padding-top: 1rem;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint.settings-scope {
|
||||||
|
margin: 0 0 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.field {
|
.field {
|
||||||
@@ -65,7 +337,8 @@ select {
|
|||||||
|
|
||||||
input[type="text"]:focus-visible,
|
input[type="text"]:focus-visible,
|
||||||
select:focus-visible,
|
select:focus-visible,
|
||||||
button:focus-visible {
|
button:focus-visible,
|
||||||
|
summary:focus-visible {
|
||||||
outline: 2px solid var(--accent);
|
outline: 2px solid var(--accent);
|
||||||
outline-offset: 1px;
|
outline-offset: 1px;
|
||||||
}
|
}
|
||||||
@@ -76,18 +349,30 @@ button:focus-visible {
|
|||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag {
|
summary {
|
||||||
font-weight: 400;
|
cursor: pointer;
|
||||||
font-size: 0.75rem;
|
font-weight: 600;
|
||||||
color: var(--fg-dim);
|
color: var(--fg);
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 3px;
|
|
||||||
padding: 0 0.3rem;
|
|
||||||
vertical-align: 1px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
details[open] > summary {
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-options {
|
||||||
|
margin-bottom: 1.15rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-detail {
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--fg-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reversed so the button stays at the right edge and "Saved" appears beside
|
||||||
|
it without moving it. */
|
||||||
.actions {
|
.actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: row-reverse;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -104,35 +389,69 @@ button {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Restore defaults discards every customised setting at once. */
|
||||||
|
button.danger {
|
||||||
|
color: var(--danger);
|
||||||
|
border-color: var(--danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
button.danger:hover {
|
||||||
|
color: var(--bg);
|
||||||
|
background: var(--danger);
|
||||||
|
}
|
||||||
|
|
||||||
#status {
|
#status {
|
||||||
color: var(--fg-dim);
|
color: var(--fg-dim);
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.note {
|
.note {
|
||||||
margin-top: 1.75rem;
|
margin-top: 1.25rem;
|
||||||
padding-top: 1rem;
|
padding-top: 1rem;
|
||||||
border-top: 1px solid var(--border);
|
border-top: 1px solid var(--border);
|
||||||
color: var(--fg-dim);
|
color: var(--fg-dim);
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
code,
|
.note summary {
|
||||||
kbd {
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
padding: 0 0.25em;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: var(--surface);
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.note h2 {
|
/* Written as <Ctrl>+L, so the keys read as words rather than code. */
|
||||||
|
kbd {
|
||||||
|
font: inherit;
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage h2,
|
||||||
|
.settings h2 {
|
||||||
margin: 0 0 0.5rem;
|
margin: 0 0 0.5rem;
|
||||||
color: var(--fg);
|
color: var(--fg);
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.note p {
|
.note p,
|
||||||
|
.note ol {
|
||||||
margin: 0 0 0.75rem;
|
margin: 0 0 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.note ol {
|
||||||
|
padding-inline-start: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note li {
|
||||||
|
margin-bottom: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
.note code {
|
.note code {
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
@@ -140,8 +459,9 @@ kbd {
|
|||||||
.project-links {
|
.project-links {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
gap: 0.5rem 0.65rem;
|
gap: 0.5rem 0.65rem;
|
||||||
margin-top: 1rem;
|
margin-top: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-link {
|
.project-link {
|
||||||
@@ -167,9 +487,23 @@ a:focus-visible {
|
|||||||
/* The inline about:addons surface can be very narrow. */
|
/* The inline about:addons surface can be very narrow. */
|
||||||
@media (max-width: 26rem) {
|
@media (max-width: 26rem) {
|
||||||
.actions {
|
.actions {
|
||||||
align-items: flex-start;
|
align-items: flex-end;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.drawing-menu {
|
||||||
|
margin-inline-start: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stack the links rather than wrap one onto a line that starts with "|". */
|
||||||
|
.project-links {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-separator {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[hidden] { display: none !important; }
|
[hidden] { display: none !important; }
|
||||||
|
|||||||
+155
-54
@@ -7,37 +7,120 @@
|
|||||||
<link rel="stylesheet" href="options.css" />
|
<link rel="stylesheet" href="options.css" />
|
||||||
|
|
||||||
<p class="intro">
|
<p class="intro">
|
||||||
This extension adds <a href="https://chatgpt.com/" target="_blank" rel="noopener noreferrer">ChatGPT</a> as a Firefox search engine. Type
|
This extension adds <a href="https://chatgpt.com/" target="_blank" rel="noopener noreferrer">ChatGPT</a> as a Firefox search engine.
|
||||||
<code>gpt your question</code> in the address bar, select text on a webpage and
|
|
||||||
choose <strong>Ask ChatGPT</strong> from the right-click menu.<span id="shortcut-intro"></span>
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p class="intro">
|
<section class="usage" aria-labelledby="usage-title">
|
||||||
Click the <strong>Ask ChatGPT</strong> toolbar button to ask about selected
|
<h2 id="usage-title">Usage instructions</h2>
|
||||||
text, or open a blank chat when nothing is selected. The button works in
|
|
||||||
every context-menu mode and follows your settings below.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p class="intro">
|
<!-- Hotkeys are written <Ctrl>+L: named keys in angle brackets, letters plain.
|
||||||
ChatGPT may require you to sign in. If your prompt is not sent automatically,
|
shortcut.js rewrites each [data-shortcut] for the user's platform. The
|
||||||
press <strong>Enter</strong> or click <strong>Send</strong> in ChatGPT.
|
drawings are decorative; the text beside them carries the instructions. -->
|
||||||
|
<ul class="ways">
|
||||||
|
<li>
|
||||||
|
<strong>Address bar:</strong> press <kbd data-shortcut="Ctrl+L"><Ctrl>+L</kbd>,
|
||||||
|
type <code>gpt</code> and a space, then your question, and press
|
||||||
|
<kbd data-shortcut="Enter"><Enter></kbd>.
|
||||||
|
<div class="drawing" aria-hidden="true">
|
||||||
|
<div class="drawing-urlbar">
|
||||||
|
<span class="drawing-chip"><img src="../icons/powered-by-openai-icon-16x16.png" srcset="../icons/powered-by-openai-icon-32x32.png 2x" alt="" />ChatGPT</span>
|
||||||
|
<span class="drawing-typed">how do magnets work?</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Right-click:</strong> select text on a webpage, right-click it, and
|
||||||
|
choose <strong>Ask ChatGPT</strong>.
|
||||||
|
<div class="drawing" aria-hidden="true">
|
||||||
|
<p class="drawing-page">Fridge <mark>magnets stick</mark> to steel because…</p>
|
||||||
|
<div class="drawing-menu">
|
||||||
|
<div>Copy</div>
|
||||||
|
<div>Select All</div>
|
||||||
|
<hr />
|
||||||
|
<div class="drawing-menu-active"><img src="../icons/powered-by-openai-icon-16x16.png" srcset="../icons/powered-by-openai-icon-32x32.png 2x" alt="" />Ask ChatGPT</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li id="shortcut-intro" hidden>
|
||||||
|
<strong>Keyboard:</strong> select text and press <kbd id="shortcut-intro-key"></kbd>.
|
||||||
|
With nothing selected, it opens a blank chat.
|
||||||
|
<div class="drawing" aria-hidden="true">
|
||||||
|
<div class="drawing-keycaps" id="shortcut-keycaps"></div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Toolbar:</strong> select text and click the
|
||||||
|
<img class="inline-icon" src="../icons/powered-by-openai-icon-16x16.png" srcset="../icons/powered-by-openai-icon-32x32.png 2x" alt="" />
|
||||||
|
<strong>Ask ChatGPT</strong> button. With nothing selected, it opens a
|
||||||
|
blank chat. Can’t see the button? Look in the puzzle-piece Extensions menu.
|
||||||
|
<div class="drawing" aria-hidden="true">
|
||||||
|
<div class="drawing-toolbar">
|
||||||
|
<span class="drawing-address">example.com</span>
|
||||||
|
<span class="drawing-button">
|
||||||
|
<svg viewBox="0 0 16 16"><path d="M2 5.5h3a2 2 0 1 1 3.2 0h3.3v2.8a2 2 0 1 1 0 3.2v3H2z" /></svg>
|
||||||
|
</span>
|
||||||
|
<span class="drawing-button drawing-button-hover">
|
||||||
|
<img src="../icons/powered-by-openai-icon-16x16.png" srcset="../icons/powered-by-openai-icon-32x32.png 2x" alt="" />
|
||||||
|
<svg class="drawing-pointer" viewBox="0 0 12 18"><path d="M1 1v14.5l3.6-3.4 2.6 5.6 2.2-1-2.6-5.5H11z" /></svg>
|
||||||
|
</span>
|
||||||
|
<span class="drawing-button">
|
||||||
|
<svg viewBox="0 0 16 16"><path d="M3 4.5h10M3 8h10M3 11.5h10" /></svg>
|
||||||
|
</span>
|
||||||
|
<span class="drawing-tooltip">Ask ChatGPT</span>
|
||||||
|
</div>
|
||||||
|
<p class="drawing-content"><mark>Why fridge magnets stick</mark></p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p class="hint intro-hint">
|
||||||
|
ChatGPT opens with your text filled in. If it isn’t sent automatically, press
|
||||||
|
<kbd data-shortcut="Enter"><Enter></kbd> or click Send. ChatGPT may ask you to sign in first.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="settings" aria-labelledby="settings-title">
|
||||||
|
<h2 id="settings-title">Settings</h2>
|
||||||
|
<p class="hint settings-scope">
|
||||||
|
These apply to the right-click menu, toolbar button, and keyboard shortcut.
|
||||||
|
Typing <code>gpt</code> in the address bar isn’t affected.
|
||||||
</p>
|
</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">Right-click menu</label>
|
||||||
<select id="contextMenuMode">
|
<select id="contextMenuMode">
|
||||||
<option value="off">Off</option>
|
<option value="off">Off</option>
|
||||||
<option value="simple">Simple</option>
|
<option value="simple">Simple</option>
|
||||||
<option value="advanced">Advanced</option>
|
<option value="advanced">Advanced</option>
|
||||||
</select>
|
</select>
|
||||||
<p class="hint">Simple asks about selected text. Advanced adds Explain, Summarize, Debug code, and link/page actions.</p>
|
<p class="hint">
|
||||||
<p class="hint" id="shortcut-hint">The keyboard shortcut always runs your default action.</p>
|
Simple shows Ask ChatGPT. Advanced adds Explain, Summarize, Debug code,
|
||||||
|
and more. With Off, the toolbar button and keyboard shortcut still work.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="advanced-settings" class="dependent-settings" hidden>
|
<div id="advanced-settings" class="dependent-settings" hidden>
|
||||||
|
<!-- Shown only with Advanced, so the drawing previews what it adds. -->
|
||||||
|
<div class="drawing drawing-advanced" aria-hidden="true">
|
||||||
|
<div class="drawing-menus">
|
||||||
|
<div class="drawing-menu">
|
||||||
|
<div>Copy</div>
|
||||||
|
<div>Select All</div>
|
||||||
|
<hr />
|
||||||
|
<div class="drawing-menu-active"><img src="../icons/powered-by-openai-icon-16x16.png" srcset="../icons/powered-by-openai-icon-32x32.png 2x" alt="" />Ask ChatGPT<span class="drawing-submenu-arrow">›</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="drawing-menu drawing-submenu">
|
||||||
|
<div>Ask normally</div>
|
||||||
|
<hr />
|
||||||
|
<div class="drawing-menu-active">Explain</div>
|
||||||
|
<div>Summarize</div>
|
||||||
|
<div>Debug code</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="field checkbox">
|
<div class="field checkbox">
|
||||||
<label><input type="checkbox" id="showTranslate"> Show Translate in advanced menu</label>
|
<label><input type="checkbox" id="showTranslate"> Show Translate in the Advanced menu</label>
|
||||||
</div>
|
</div>
|
||||||
<div id="translation-settings" hidden>
|
<div id="translation-settings" hidden>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
@@ -56,6 +139,31 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label for="openIn">Open ChatGPT in</label>
|
||||||
|
<select id="openIn">
|
||||||
|
<option value="new-tab">A new tab</option>
|
||||||
|
<option value="background-tab">A new background tab</option>
|
||||||
|
<option value="new-window">A new window</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Opens on load when one of these differs from its default (options.js),
|
||||||
|
so a customised setting never looks lost. -->
|
||||||
|
<details id="more-options" class="more-options">
|
||||||
|
<summary>More options: <span class="summary-detail">prompt template, temporary chat, web search</span></summary>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label for="promptTemplate">Prompt template</label>
|
||||||
|
<input type="text" id="promptTemplate" spellcheck="false" maxlength="1000" />
|
||||||
|
<p class="hint">
|
||||||
|
<code>{query}</code> is replaced with your selected text, as in
|
||||||
|
<code>Explain simply: {query}</code>. Used by Ask ChatGPT, Ask normally,
|
||||||
|
the toolbar button, and the keyboard shortcut; other Advanced actions
|
||||||
|
such as Summarize use their own prompts. Up to 1,000 characters.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="field checkbox">
|
<div class="field checkbox">
|
||||||
<label><input type="checkbox" id="temporaryChat" /> Request a temporary chat</label>
|
<label><input type="checkbox" id="temporaryChat" /> Request a temporary chat</label>
|
||||||
<p class="hint">
|
<p class="hint">
|
||||||
@@ -68,56 +176,49 @@
|
|||||||
<label><input type="checkbox" id="webSearch" /> Ask ChatGPT to use web search (hint only)</label>
|
<label><input type="checkbox" id="webSearch" /> Ask ChatGPT to use web search (hint only)</label>
|
||||||
<p class="hint">A hint to ChatGPT's interface, not a guarantee.</p>
|
<p class="hint">A hint to ChatGPT's interface, not a guarantee.</p>
|
||||||
</div>
|
</div>
|
||||||
|
</details>
|
||||||
<div class="field">
|
|
||||||
<label for="openIn">Open in</label>
|
|
||||||
<select id="openIn">
|
|
||||||
<option value="new-tab">A new tab</option>
|
|
||||||
<option value="background-tab">A new background tab</option>
|
|
||||||
<option value="new-window">A new window</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<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, the toolbar button, and the keyboard shortcut. Other advanced actions use their own prompts.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button type="button" id="restore">Restore defaults</button>
|
<button type="button" id="restore" class="danger">Restore defaults</button>
|
||||||
<span id="status" role="status" aria-live="polite"></span>
|
<span id="status" role="status" aria-live="polite"></span>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<section class="note" aria-labelledby="search-settings-title">
|
|
||||||
<h2 id="search-settings-title">Search keyword and default engine</h2>
|
|
||||||
<p>
|
|
||||||
Open Firefox’s <strong>Settings → Search</strong>, or paste
|
|
||||||
<code>about:preferences#search</code> into the address bar.
|
|
||||||
Under <strong>Search Shortcuts</strong>, edit ChatGPT’s keyword to change
|
|
||||||
<code>gpt</code>. Under <strong>Default Search Engine</strong>, choose
|
|
||||||
ChatGPT to use it for address-bar searches without a prefix. Choose your
|
|
||||||
previous engine there to switch back.
|
|
||||||
<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, 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.
|
|
||||||
</p>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="note" aria-labelledby="shortcut-title">
|
<details class="note">
|
||||||
<h2 id="shortcut-title">Keyboard shortcut</h2>
|
<summary>Use your own keyword or make ChatGPT your default search engine</summary>
|
||||||
|
<ol>
|
||||||
|
<li>
|
||||||
|
Open Firefox’s <strong>Settings → Search</strong>, or paste
|
||||||
|
<code>about:preferences#search</code> into the address bar.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Under <strong>Search Shortcuts</strong>, edit ChatGPT’s keyword to add
|
||||||
|
your own. <code>gpt</code> keeps working too.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Under <strong>Default Search Engine</strong>, choose ChatGPT to use it
|
||||||
|
without typing <code>gpt</code>. Choose your previous engine there to
|
||||||
|
switch back.
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
<p>
|
||||||
|
These are Firefox settings, so <strong>Restore defaults</strong> doesn’t
|
||||||
|
change them.
|
||||||
|
<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>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details class="note">
|
||||||
|
<summary>Change the keyboard shortcut</summary>
|
||||||
<p id="shortcut-current" role="status" aria-live="polite">Checking keyboard shortcut…</p>
|
<p id="shortcut-current" role="status" aria-live="polite">Checking keyboard shortcut…</p>
|
||||||
<button type="button" id="change-shortcut" hidden>Change keyboard shortcut</button>
|
<button type="button" id="change-shortcut" hidden>Change keyboard shortcut</button>
|
||||||
<p class="hint">
|
<p class="hint">
|
||||||
You can also open Add-ons, then the gear menu →
|
Or open Add-ons, click the gear menu, and choose
|
||||||
<em>Manage Extension Shortcuts</em> to change or remove the shortcut.
|
<em>Manage Extension Shortcuts</em>. <strong>Restore defaults</strong>
|
||||||
|
doesn’t change your shortcut.
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</details>
|
||||||
|
|
||||||
<nav class="project-links" aria-label="Project links">
|
<nav class="project-links" aria-label="Project links">
|
||||||
<a href="https://zavage-software.com/portfolio/search-with-ChatGPT" target="_blank" rel="noopener noreferrer">Project website</a>
|
<a href="https://zavage-software.com/portfolio/search-with-ChatGPT" target="_blank" rel="noopener noreferrer">Project website</a>
|
||||||
|
|||||||
+31
-4
@@ -1,13 +1,27 @@
|
|||||||
import { DEFAULT_SETTINGS } from "../lib/defaults.js";
|
import { DEFAULT_SETTINGS } from "../lib/defaults.js";
|
||||||
import { load, save } from "../lib/settings.js";
|
import { load, save } from "../lib/settings.js";
|
||||||
import { initializeMenuSettings, MENU_SETTING_KEYS } from "./menu-settings.js";
|
import { initializeMenuSettings, MENU_SETTING_KEYS } from "./menu-settings.js";
|
||||||
import { initializeShortcut } from "./shortcut.js";
|
import { initializeShortcut, renderStaticShortcuts } from "./shortcut.js";
|
||||||
|
|
||||||
const SAVE_DEBOUNCE_MS = 400;
|
const SAVE_DEBOUNCE_MS = 400;
|
||||||
const STATUS_CLEAR_MS = 1500;
|
const STATUS_CLEAR_MS = 1500;
|
||||||
|
|
||||||
const field = (id) => document.getElementById(id);
|
const field = (id) => document.getElementById(id);
|
||||||
|
|
||||||
|
// The settings inside the collapsed "More options" section, read from the page
|
||||||
|
// so moving a setting there needs no change here.
|
||||||
|
const MORE_OPTION_KEYS = [...field("more-options").querySelectorAll("[id]")]
|
||||||
|
.map(({ id }) => id)
|
||||||
|
.filter((id) => Object.hasOwn(DEFAULT_SETTINGS, id));
|
||||||
|
|
||||||
|
// A customised setting should never look lost inside a closed section. This
|
||||||
|
// only ever opens it, so it never undoes the user collapsing it.
|
||||||
|
function revealCustomised(settings, keys = MORE_OPTION_KEYS) {
|
||||||
|
if (keys.some((key) => settings[key] !== DEFAULT_SETTINGS[key])) {
|
||||||
|
field("more-options").open = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let saveTimer = null;
|
let saveTimer = null;
|
||||||
let statusTimer = null;
|
let statusTimer = null;
|
||||||
|
|
||||||
@@ -66,8 +80,14 @@ function scheduleSave() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Module scripts are deferred, so the DOM is already parsed here.
|
// Module scripts are deferred, so the DOM is already parsed here.
|
||||||
void initializeShortcut({ commands: browser.commands, document, window });
|
const [{ os }, initial] = await Promise.all([
|
||||||
render(await load());
|
browser.runtime.getPlatformInfo().catch(() => ({})),
|
||||||
|
load(),
|
||||||
|
]);
|
||||||
|
renderStaticShortcuts(document, os);
|
||||||
|
void initializeShortcut({ commands: browser.commands, document, window, os });
|
||||||
|
render(initial);
|
||||||
|
revealCustomised(initial);
|
||||||
|
|
||||||
for (const id of Object.keys(DEFAULT_SETTINGS)) {
|
for (const id of Object.keys(DEFAULT_SETTINGS)) {
|
||||||
if (MENU_SETTING_KEYS.includes(id)) continue;
|
if (MENU_SETTING_KEYS.includes(id)) continue;
|
||||||
@@ -93,5 +113,12 @@ browser.storage.onChanged.addListener(async (changes, area) => {
|
|||||||
if (area !== "sync") {
|
if (area !== "sync") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
render(await load());
|
const settings = await load();
|
||||||
|
render(settings);
|
||||||
|
// Only keys whose value really changed, such as a sync from another device:
|
||||||
|
// this page saves every key at once, so checking them all would re-open the
|
||||||
|
// section whenever any other setting is edited.
|
||||||
|
revealCustomised(settings, MORE_OPTION_KEYS.filter(
|
||||||
|
(key) => key in changes && changes[key].newValue !== changes[key].oldValue,
|
||||||
|
));
|
||||||
});
|
});
|
||||||
|
|||||||
+42
-8
@@ -1,18 +1,52 @@
|
|||||||
|
// Firefox stores Mac shortcuts with Command, MacCtrl, and Alt, and reads a
|
||||||
|
// manifest's Ctrl as Command there (ShortcutUtils' chromeModifierKeyMap).
|
||||||
|
const MAC_KEY_NAMES = { Ctrl: "Cmd", Command: "Cmd", MacCtrl: "Ctrl", Alt: "Option", Enter: "Return" };
|
||||||
|
const OTHER_KEY_NAMES = { Command: "Ctrl", MacCtrl: "Ctrl" };
|
||||||
|
|
||||||
|
// The keys of a Firefox shortcut such as "Alt+Shift+G", named for the platform.
|
||||||
|
export function shortcutKeys(shortcut, os) {
|
||||||
|
const names = os === "mac" ? MAC_KEY_NAMES : OTHER_KEY_NAMES;
|
||||||
|
return shortcut.split("+").map((key) => names[key] ?? key);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Writes a shortcut the way the page does: named keys in angle brackets and
|
||||||
|
// single characters plain, "<Alt>+<Shift>+G".
|
||||||
|
export function formatShortcut(shortcut, os) {
|
||||||
|
return shortcutKeys(shortcut, os)
|
||||||
|
.map((name) => (name.length > 1 ? `<${name}>` : name))
|
||||||
|
.join("+");
|
||||||
|
}
|
||||||
|
|
||||||
|
// The page's static hotkeys are written for Windows and Linux.
|
||||||
|
export function renderStaticShortcuts(document, os) {
|
||||||
|
for (const element of document.querySelectorAll("[data-shortcut]")) {
|
||||||
|
element.textContent = formatShortcut(element.dataset.shortcut, os);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Firefox owns shortcut preferences; never copy them into extension storage.
|
// Firefox owns shortcut preferences; never copy them into extension storage.
|
||||||
export async function initializeShortcut({ commands, document, window }) {
|
export async function initializeShortcut({ commands, document, window, os }) {
|
||||||
const field = (id) => document.getElementById(id);
|
const field = (id) => document.getElementById(id);
|
||||||
let refreshId = 0;
|
let refreshId = 0;
|
||||||
|
|
||||||
|
// One key cap per key for the drawing, joined by plain "+" text.
|
||||||
|
function keycaps(shortcut) {
|
||||||
|
return shortcutKeys(shortcut, os).flatMap((name, index) => {
|
||||||
|
const key = document.createElement("kbd");
|
||||||
|
key.className = "drawing-key";
|
||||||
|
key.textContent = name;
|
||||||
|
return index ? ["+", key] : [key];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function render(shortcut) {
|
function render(shortcut) {
|
||||||
field("shortcut-intro").textContent = shortcut
|
const keys = shortcut ? formatShortcut(shortcut, os) : "";
|
||||||
? ` Or press ${shortcut} to use your selection. With nothing selected, the shortcut opens a blank chat.`
|
field("shortcut-intro").hidden = !keys;
|
||||||
: "";
|
field("shortcut-intro-key").textContent = keys;
|
||||||
field("shortcut-hint").textContent = shortcut
|
field("shortcut-keycaps").replaceChildren(...(keys ? keycaps(shortcut) : []));
|
||||||
? `${shortcut} runs your default action even when the context menu is off.`
|
|
||||||
: "The keyboard shortcut runs your default action independently of the context menu.";
|
|
||||||
field("shortcut-current").textContent = shortcut === null
|
field("shortcut-current").textContent = shortcut === null
|
||||||
? "Could not read the keyboard shortcut. Check Manage Extension Shortcuts in Add-ons."
|
? "Could not read the keyboard shortcut. Check Manage Extension Shortcuts in Add-ons."
|
||||||
: shortcut ? `Current shortcut: ${shortcut}` : "No keyboard shortcut assigned.";
|
: keys ? `Current shortcut: ${keys}` : "No keyboard shortcut assigned.";
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refresh() {
|
async function refresh() {
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "search-with-chatgpt-powered-by-openai-extension",
|
"name": "search-with-chatgpt-powered-by-openai-extension",
|
||||||
"version": "1.0.0",
|
"version": "1.3.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "search-with-chatgpt-powered-by-openai-extension",
|
"name": "search-with-chatgpt-powered-by-openai-extension",
|
||||||
"version": "1.0.0",
|
"version": "1.3.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"web-ext": "10.5.0"
|
"web-ext": "10.5.0"
|
||||||
},
|
},
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "search-with-chatgpt-powered-by-openai-extension",
|
"name": "search-with-chatgpt-powered-by-openai-extension",
|
||||||
"version": "1.1.0",
|
"version": "1.3.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Development tooling for the Search with ChatGPT Firefox extension.",
|
"description": "Development tooling for the Search with ChatGPT Firefox extension.",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { DEFAULT_SETTINGS } from '../lib/defaults.js';
|
import { DEFAULT_SETTINGS } from '../lib/defaults.js';
|
||||||
|
import { MAX_URL_CHARS } from '../lib/build-url.js';
|
||||||
const { prepareAction } = await import('../lib/prepare-action.js').catch(()=>({}));
|
const { prepareAction } = await import('../lib/prepare-action.js').catch(()=>({}));
|
||||||
const req = (overrides={}) => ({actionId:'ask',source:'Hello',settings:{...DEFAULT_SETTINGS},...overrides});
|
const req = (overrides={}) => ({actionId:'ask',source:'Hello',settings:{...DEFAULT_SETTINGS},...overrides});
|
||||||
test('ready actions preserve source and hints, translation asks only when needed', () => {
|
test('ready actions preserve source and hints, translation asks only when needed', () => {
|
||||||
@@ -49,3 +50,22 @@ test('long selections preserve instructions, repetition and Unicode with explici
|
|||||||
const translated=prepareAction(req({actionId:'translate',language:'Spanish',source:'x'.repeat(20000)}));
|
const translated=prepareAction(req({actionId:'translate',language:'Spanish',source:'x'.repeat(20000)}));
|
||||||
assert.equal(translated.kind,'shortened');assert.match(new URL(translated.url).searchParams.get('q'), /Spanish/);
|
assert.equal(translated.kind,'shortened');assert.match(new URL(translated.url).searchParams.get('q'), /Spanish/);
|
||||||
});
|
});
|
||||||
|
test('huge selections are capped before shortening, with identical output', () => {
|
||||||
|
assert.equal(typeof prepareAction, 'function');
|
||||||
|
// Select-all on a very large page; shortening this took over a second uncapped.
|
||||||
|
const source='😀 word '.repeat(625000);
|
||||||
|
const started=performance.now();
|
||||||
|
const out=prepareAction(req({source}));
|
||||||
|
const elapsed=performance.now()-started;
|
||||||
|
assert.equal(out.kind,'shortened');
|
||||||
|
assert.ok(elapsed<250,`took ${elapsed.toFixed(0)} ms`);
|
||||||
|
const capped=Array.from(source).slice(0,MAX_URL_CHARS).join('');
|
||||||
|
assert.deepEqual(prepareAction(req({source:capped})),out);
|
||||||
|
});
|
||||||
|
test('a selection too long to send whole always needs consent, even if its start would fit', () => {
|
||||||
|
assert.equal(typeof prepareAction, 'function');
|
||||||
|
// The first MAX_URL_CHARS code points trim down to "a"; sending just that would drop the rest unasked.
|
||||||
|
const out=prepareAction(req({source:'a'+' '.repeat(MAX_URL_CHARS)+'b'}));
|
||||||
|
assert.equal(out.kind,'shortened');
|
||||||
|
assert.ok(new URL(out.url).searchParams.get('q').includes('[Selected text shortened]'));
|
||||||
|
});
|
||||||
|
|||||||
+27
-1
@@ -1,7 +1,9 @@
|
|||||||
import test from "node:test";
|
import test from "node:test";
|
||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
|
|
||||||
import { DEFAULT_SETTINGS } from "../lib/defaults.js";
|
import { readFileSync } from "node:fs";
|
||||||
|
|
||||||
|
import { DEFAULT_SETTINGS, MAX_TEMPLATE_CHARS } from "../lib/defaults.js";
|
||||||
import { mergeSettings } from "../lib/settings.js";
|
import { mergeSettings } from "../lib/settings.js";
|
||||||
|
|
||||||
test("empty object yields the defaults", () => {
|
test("empty object yields the defaults", () => {
|
||||||
@@ -67,6 +69,30 @@ test("wrong types fall back to the default", () => {
|
|||||||
assert.equal(merged.promptTemplate, DEFAULT_SETTINGS.promptTemplate);
|
assert.equal(merged.promptTemplate, DEFAULT_SETTINGS.promptTemplate);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("a prompt template over the length limit falls back to the default", () => {
|
||||||
|
assert.equal(typeof MAX_TEMPLATE_CHARS, "number");
|
||||||
|
const atLimit = "x".repeat(MAX_TEMPLATE_CHARS);
|
||||||
|
assert.equal(mergeSettings({ promptTemplate: atLimit }).promptTemplate, atLimit);
|
||||||
|
assert.equal(mergeSettings({ promptTemplate: `${atLimit}x` }).promptTemplate,
|
||||||
|
DEFAULT_SETTINGS.promptTemplate);
|
||||||
|
});
|
||||||
|
|
||||||
|
// storage.sync rejects any item over 8192 bytes (key plus JSON value), and the
|
||||||
|
// options page writes every field in one set(), so one oversized template would
|
||||||
|
// make every later save fail. The worst character is a pasted control
|
||||||
|
// character, which JSON escapes to six bytes.
|
||||||
|
test("a template at the limit fits storage.sync's per-item quota in the worst case", () => {
|
||||||
|
const worst = "\u0001".repeat(MAX_TEMPLATE_CHARS);
|
||||||
|
const bytes = new TextEncoder().encode("promptTemplate" + JSON.stringify(worst)).length;
|
||||||
|
assert.ok(bytes <= 8192, `${bytes} bytes`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("the options page enforces the same template limit", () => {
|
||||||
|
const html = readFileSync(new URL("../options/options.html", import.meta.url), "utf8");
|
||||||
|
const input = html.match(/<input[^>]*id="promptTemplate"[^>]*>/)?.[0] ?? "";
|
||||||
|
assert.equal(input.match(/maxlength="(\d+)"/)?.[1], String(MAX_TEMPLATE_CHARS));
|
||||||
|
});
|
||||||
|
|
||||||
test("an out-of-range openIn falls back to the default", () => {
|
test("an out-of-range openIn falls back to the default", () => {
|
||||||
assert.equal(mergeSettings({ openIn: "teleport" }).openIn, DEFAULT_SETTINGS.openIn);
|
assert.equal(mergeSettings({ openIn: "teleport" }).openIn, DEFAULT_SETTINGS.openIn);
|
||||||
assert.equal(mergeSettings({ openIn: "" }).openIn, DEFAULT_SETTINGS.openIn);
|
assert.equal(mergeSettings({ openIn: "" }).openIn, DEFAULT_SETTINGS.openIn);
|
||||||
|
|||||||
+70
-12
@@ -1,32 +1,87 @@
|
|||||||
import test from "node:test";
|
import test from "node:test";
|
||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import { initializeShortcut } from "../options/shortcut.js";
|
import { readFileSync } from "node:fs";
|
||||||
|
import { formatShortcut, initializeShortcut, renderStaticShortcuts } from "../options/shortcut.js";
|
||||||
|
|
||||||
function setup(commands) {
|
function setup(commands, os) {
|
||||||
const elements = Object.fromEntries([
|
const elements = Object.fromEntries([
|
||||||
"shortcut-intro", "shortcut-hint", "shortcut-current", "change-shortcut",
|
"shortcut-intro", "shortcut-intro-key", "shortcut-keycaps", "shortcut-current", "change-shortcut",
|
||||||
].map((id) => [id, Object.assign(new EventTarget(), { textContent: "", hidden: true })]));
|
].map((id) => [id, Object.assign(new EventTarget(), {
|
||||||
|
textContent: "",
|
||||||
|
hidden: true,
|
||||||
|
children: [],
|
||||||
|
replaceChildren(...children) { this.children = children; },
|
||||||
|
})]));
|
||||||
const document = Object.assign(new EventTarget(), {
|
const document = Object.assign(new EventTarget(), {
|
||||||
visibilityState: "visible",
|
visibilityState: "visible",
|
||||||
getElementById: (id) => elements[id],
|
getElementById: (id) => elements[id],
|
||||||
|
createElement: (tagName) => ({ tagName, className: "", textContent: "" }),
|
||||||
});
|
});
|
||||||
const window = new EventTarget();
|
const window = new EventTarget();
|
||||||
const ready = initializeShortcut({ commands, document, window });
|
const ready = initializeShortcut({ commands, document, window, os });
|
||||||
return { elements, document, window, ready };
|
return { elements, document, window, ready };
|
||||||
}
|
}
|
||||||
|
|
||||||
const flush = () => new Promise((resolve) => setImmediate(resolve));
|
const flush = () => new Promise((resolve) => setImmediate(resolve));
|
||||||
const command = (shortcut) => ({ name: "ask-chatgpt", description: "Ask ChatGPT", shortcut });
|
const command = (shortcut) => ({ name: "ask-chatgpt", description: "Ask ChatGPT", shortcut });
|
||||||
|
// The drawing's key caps and the "+" text between them, in order.
|
||||||
|
const drawn = (element) => element.children.map((child) => child.textContent ?? child);
|
||||||
|
|
||||||
test("shows the configured command rather than another command or the default", async () => {
|
test("shows the configured command rather than another command or the default", async () => {
|
||||||
const { elements, ready } = setup({ getAll: async () => [
|
const { elements, ready } = setup({ getAll: async () => [
|
||||||
{ name: "other", description: "Other", shortcut: "Alt+O" }, command("Ctrl+Shift+Y"),
|
{ name: "other", description: "Other", shortcut: "Alt+O" }, command("Ctrl+Shift+Y"),
|
||||||
] });
|
] });
|
||||||
await ready;
|
await ready;
|
||||||
for (const id of ["shortcut-intro", "shortcut-hint", "shortcut-current"]) {
|
assert.equal(elements["shortcut-intro"].hidden, false);
|
||||||
assert.match(elements[id].textContent, /Ctrl\+Shift\+Y/);
|
assert.equal(elements["shortcut-intro-key"].textContent, "<Ctrl>+<Shift>+Y");
|
||||||
assert.doesNotMatch(elements[id].textContent, /Alt\+Shift\+G|Alt\+O/);
|
assert.equal(elements["shortcut-current"].textContent, "Current shortcut: <Ctrl>+<Shift>+Y");
|
||||||
|
assert.deepEqual(drawn(elements["shortcut-keycaps"]), ["Ctrl", "+", "Shift", "+", "Y"]);
|
||||||
|
assert.ok(elements["shortcut-keycaps"].children
|
||||||
|
.filter((child) => typeof child !== "string")
|
||||||
|
.every(({ tagName, className }) => tagName === "kbd" && className === "drawing-key"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("writes keys the way the page does, with Mac names on a Mac", () => {
|
||||||
|
assert.equal(formatShortcut("Alt+Shift+G", "linux"), "<Alt>+<Shift>+G");
|
||||||
|
assert.equal(formatShortcut("Ctrl+L", "win"), "<Ctrl>+L");
|
||||||
|
assert.equal(formatShortcut("Enter", undefined), "<Enter>");
|
||||||
|
assert.equal(formatShortcut("Ctrl+Shift+F5", "win"), "<Ctrl>+<Shift>+<F5>");
|
||||||
|
// Firefox names punctuation and media keys rather than using the character.
|
||||||
|
assert.equal(formatShortcut("Ctrl+Shift+Comma", "win"), "<Ctrl>+<Shift>+<Comma>");
|
||||||
|
assert.equal(formatShortcut("MediaPlayPause", "linux"), "<MediaPlayPause>");
|
||||||
|
assert.equal(formatShortcut("Alt+Shift+G", "mac"), "<Option>+<Shift>+G");
|
||||||
|
// Firefox reads a manifest's Ctrl as Command on a Mac, and stores shortcuts
|
||||||
|
// the user sets there with Command and MacCtrl.
|
||||||
|
assert.equal(formatShortcut("Ctrl+L", "mac"), "<Cmd>+L");
|
||||||
|
assert.equal(formatShortcut("Command+Shift+Y", "mac"), "<Cmd>+<Shift>+Y");
|
||||||
|
assert.equal(formatShortcut("MacCtrl+Alt+Y", "mac"), "<Ctrl>+<Option>+Y");
|
||||||
|
assert.equal(formatShortcut("Enter", "mac"), "<Return>");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a Mac shows its own key names for the configured shortcut", async () => {
|
||||||
|
const { elements, ready } = setup({ getAll: async () => [command("Alt+Shift+G")] }, "mac");
|
||||||
|
await ready;
|
||||||
|
assert.equal(elements["shortcut-intro-key"].textContent, "<Option>+<Shift>+G");
|
||||||
|
assert.deepEqual(drawn(elements["shortcut-keycaps"]), ["Option", "+", "Shift", "+", "G"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("the page's written hotkeys match the formatter and are rewritten per platform", () => {
|
||||||
|
const html = readFileSync(new URL("../options/options.html", import.meta.url), "utf8");
|
||||||
|
const written = [...html.matchAll(/data-shortcut="([^"]+)">([^<]*)</g)];
|
||||||
|
assert.ok(written.length >= 2);
|
||||||
|
// Every hotkey must match the pattern above, or this test would skip it.
|
||||||
|
assert.equal(written.length, html.split("data-shortcut=").length - 1);
|
||||||
|
for (const [, shortcut, text] of written) {
|
||||||
|
assert.equal(text.replaceAll("<", "<").replaceAll(">", ">"), formatShortcut(shortcut, "linux"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const elements = written.map(([, shortcut]) => ({ dataset: { shortcut }, textContent: "" }));
|
||||||
|
renderStaticShortcuts({ querySelectorAll: () => elements }, "mac");
|
||||||
|
assert.deepEqual(
|
||||||
|
elements.map(({ textContent }) => textContent),
|
||||||
|
written.map(([, shortcut]) => formatShortcut(shortcut, "mac")),
|
||||||
|
);
|
||||||
|
assert.ok(elements.some(({ textContent }) => textContent === "<Cmd>+L"));
|
||||||
});
|
});
|
||||||
|
|
||||||
test("updates an open page when the shortcut is removed", async () => {
|
test("updates an open page when the shortcut is removed", async () => {
|
||||||
@@ -41,7 +96,9 @@ test("updates an open page when the shortcut is removed", async () => {
|
|||||||
changed({ name: "ask-chatgpt", newShortcut: "", oldShortcut: "Alt+Shift+G" });
|
changed({ name: "ask-chatgpt", newShortcut: "", oldShortcut: "Alt+Shift+G" });
|
||||||
await flush();
|
await flush();
|
||||||
assert.match(elements["shortcut-current"].textContent, /No keyboard shortcut assigned/);
|
assert.match(elements["shortcut-current"].textContent, /No keyboard shortcut assigned/);
|
||||||
assert.doesNotMatch(elements["shortcut-intro"].textContent, /press|Alt\+Shift\+G/i);
|
assert.equal(elements["shortcut-intro"].hidden, true);
|
||||||
|
assert.equal(elements["shortcut-intro-key"].textContent, "");
|
||||||
|
assert.deepEqual(elements["shortcut-keycaps"].children, []);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("refreshes on focus and visibility without the change event API", async () => {
|
test("refreshes on focus and visibility without the change event API", async () => {
|
||||||
@@ -51,11 +108,11 @@ test("refreshes on focus and visibility without the change event API", async ()
|
|||||||
shortcut = "Ctrl+Shift+Y";
|
shortcut = "Ctrl+Shift+Y";
|
||||||
window.dispatchEvent(new Event("focus"));
|
window.dispatchEvent(new Event("focus"));
|
||||||
await flush();
|
await flush();
|
||||||
assert.match(elements["shortcut-current"].textContent, /Ctrl\+Shift\+Y/);
|
assert.match(elements["shortcut-current"].textContent, /<Ctrl>\+<Shift>\+Y/);
|
||||||
shortcut = "Alt+J";
|
shortcut = "Alt+J";
|
||||||
document.dispatchEvent(new Event("visibilitychange"));
|
document.dispatchEvent(new Event("visibilitychange"));
|
||||||
await flush();
|
await flush();
|
||||||
assert.match(elements["shortcut-current"].textContent, /Alt\+J/);
|
assert.match(elements["shortcut-current"].textContent, /<Alt>\+J/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("missing commands and failed reads never claim the default is assigned", async () => {
|
test("missing commands and failed reads never claim the default is assigned", async () => {
|
||||||
@@ -64,7 +121,8 @@ test("missing commands and failed reads never claim the default is assigned", as
|
|||||||
}]) {
|
}]) {
|
||||||
const { elements, ready } = setup(commands);
|
const { elements, ready } = setup(commands);
|
||||||
await ready;
|
await ready;
|
||||||
assert.doesNotMatch(elements["shortcut-current"].textContent, /Alt\+Shift\+G/);
|
assert.doesNotMatch(elements["shortcut-current"].textContent, /<Alt>\+<Shift>\+G|Alt\+Shift\+G/);
|
||||||
|
assert.equal(elements["shortcut-intro"].hidden, true);
|
||||||
assert.equal(elements["change-shortcut"].hidden, true);
|
assert.equal(elements["change-shortcut"].hidden, true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user