diff --git a/background/background.js b/background/background.js index 8d473a4..71f5068 100644 --- a/background/background.js +++ b/background/background.js @@ -10,6 +10,17 @@ const COMMAND_ID = "ask-chatgpt"; // A commands invocation confers activeTab (ext-commands.js grants it before // firing onCommand), so executeScript works with no host permission. // +// Every frame keeps its own independent Selection, and a selection made in a +// non-focused frame is not cleared when the user selects elsewhere. Without a +// filter, a stale selection in an unfocused (possibly cross-origin) iframe +// could win over — or be pulled alongside — the selection the user actually +// made, with the winner among multiple non-empty results decided by an +// executeScript ordering that isn't specified. The injected snippet is +// therefore gated on document.hasFocus(), which is true for the focused +// document and all of its ancestors: the top frame wins when the user +// selected there, and an iframe's selection is only picked up when it is +// itself the focused frame. +// // Failure has three measured shapes, all handled here: a thrown // "Missing host permission for the tab", a thrown variant naming frames, and a // SILENT resolution to [null] on parent-process about: pages. Never index into @@ -32,7 +43,7 @@ async function readSelection() { let results; try { results = await browser.tabs.executeScript(tabId, { - code: "window.getSelection().toString()", + code: "document.hasFocus() ? window.getSelection().toString() : ''", allFrames: true, matchAboutBlank: true, }); diff --git a/lib/defaults.js b/lib/defaults.js index c7e67f7..b3dd454 100644 --- a/lib/defaults.js +++ b/lib/defaults.js @@ -4,6 +4,13 @@ export const OPEN_IN_VALUES = ["new-tab", "background-tab", "new-window"]; +// An unbounded model string can, on its own, exceed MAX_URL_CHARS in +// lib/build-url.js — fitToBudget() only ever trims `q`, so a long enough +// model silently pushes the whole URL over budget and drops the query +// entirely with no signal to the user. 200 is generous for any real model +// slug while keeping the URL budget meaningful. +export const MAX_MODEL_CHARS = 200; + export const DEFAULT_SETTINGS = { model: "", temporaryChat: false, diff --git a/lib/settings.js b/lib/settings.js index d3d4856..1060a0a 100644 --- a/lib/settings.js +++ b/lib/settings.js @@ -1,4 +1,4 @@ -import { DEFAULT_SETTINGS, OPEN_IN_VALUES } from "./defaults.js"; +import { DEFAULT_SETTINGS, OPEN_IN_VALUES, MAX_MODEL_CHARS } from "./defaults.js"; // Pure. Layers stored values over the defaults, dropping unknown keys and // falling back on wrong types or out-of-range values. A fresh or signed-out @@ -8,7 +8,7 @@ export function mergeSettings(stored) { if (stored === null || typeof stored !== "object") { return merged; } - if (typeof stored.model === "string") { + if (typeof stored.model === "string" && stored.model.length <= MAX_MODEL_CHARS) { merged.model = stored.model; } if (typeof stored.temporaryChat === "boolean") { diff --git a/options/options.html b/options/options.html index 07f9e8d..8920222 100644 --- a/options/options.html +++ b/options/options.html @@ -9,7 +9,7 @@