diff --git a/AGENTS.md b/AGENTS.md index 38ca2ae..5cd9fae 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -50,10 +50,21 @@ After `npm test`, use `npm run dev` and confirm: in both light and dark themes and at a narrow window width, and values persist across a restart. - The right-click **Ask ChatGPT** item appears only when text is selected. -- `Alt+Shift+G` acts on the selection, including a selection inside an iframe, - and opens a blank chat when nothing is selected or on a restricted page such - as `about:config`. Press the key to test this — `commands.getAll()` reports a - shortcut as registered even when Firefox has silently overridden it. +- `Alt+Shift+G` acts on the selection and opens a blank chat when nothing is + selected or on a restricted page such as `about:config`. Press the key to test + this — `commands.getAll()` reports a shortcut as registered even when Firefox + has silently overridden it. The injected snippet is gated on + `document.hasFocus()`, so cover all four frame cases: + - Select in the top frame → the top-frame text arrives. + - Select inside an iframe → the iframe text arrives. + - Select in the top frame, *then* select inside an iframe → the iframe text + arrives. `hasFocus()` is true for a focused frame and all its ancestors, so + if the stale top-frame text arrives instead, exclude ancestors by also + checking that `document.activeElement` is not the frame element. + - Press `Ctrl+F`, find a term, then press the shortcut without clicking back + into the page → a blank chat is expected. Firefox leaves focus in the + findbar, so no content frame reports focus. This is a deliberate trade: a + stale cross-origin frame silently seeding the prompt was the worse failure. Inspect ZIP contents whenever packaging rules or assets change; `test/` and `docs/` must never appear, not even as empty directory entries. diff --git a/lib/build-url.js b/lib/build-url.js index 10e3e08..398828b 100644 --- a/lib/build-url.js +++ b/lib/build-url.js @@ -56,6 +56,9 @@ function lastWhitespaceIndex(text) { } // Returns the longest prefix of `prompt` whose finished URL fits the budget. +// The guarantee assumes `settings` came through mergeSettings(): only `q` is +// ever trimmed, so a model longer than MAX_MODEL_CHARS could blow the budget on +// its own and leave no room for `q` at all. Every real caller goes via load(). // Slicing happens by CODE POINT on the decoded string, so a surrogate pair can // never be split and a percent-escape can never be severed (encoding happens // afterwards, via URLSearchParams). diff --git a/options/options.html b/options/options.html index 8920222..2ebd7cd 100644 --- a/options/options.html +++ b/options/options.html @@ -9,7 +9,9 @@
- + +

ChatGPT may ignore this and use your account default. Leave blank to always use your account default. diff --git a/options/options.js b/options/options.js index cc1ef03..371a192 100644 --- a/options/options.js +++ b/options/options.js @@ -1,4 +1,4 @@ -import { DEFAULT_SETTINGS } from "../lib/defaults.js"; +import { DEFAULT_SETTINGS, MAX_MODEL_CHARS } from "../lib/defaults.js"; import { load, save } from "../lib/settings.js"; const SAVE_DEBOUNCE_MS = 400; @@ -59,6 +59,11 @@ function scheduleSave() { saveTimer = setTimeout(() => persist(collect()), SAVE_DEBOUNCE_MS); } +// Derived rather than hard-coded in the markup: if the two ever disagreed, the +// input would accept a model that mergeSettings() then silently discards, +// blanking the setting with no explanation. +field("model").maxLength = MAX_MODEL_CHARS; + // Module scripts are deferred, so the DOM is already parsed here. render(await load());