diff --git a/index.json b/index.json index 75eefe8..fb1f3b4 100644 --- a/index.json +++ b/index.json @@ -1,6 +1,6 @@ { "version": 1, - "updated_at": "2026-06-08T06:45:59.000Z", + "updated_at": "2026-06-15T06:08:00.000Z", "scripts": [ { "id": "codex-context-ring-restore", @@ -100,6 +100,23 @@ "homepage": "", "script_url": "https://raw.githubusercontent.com/hL091015/CodexPlusPlusScriptMarket/main/scripts/zh_CN%E6%B1%89%E5%8C%96.user.js", "sha256": "72214D31D425D1CE936B457AA43FCC40DF55F4DE3B9B140F9510C7F392CDC845" + }, + { + "id": "bennett-ui-improvements", + "name": "Bennett UI Improvements", + "description": "迁移自 b-nnett Bennett UI 的 renderer-only UI 增强", + "version": "1.0.5-bigpizza.1", + "author": "bennett; JHees", + "tags": [ + "codex", + "ui", + "sidebar", + "settings", + "polish" + ], + "homepage": "", + "script_url": "https://raw.githubusercontent.com/BigPizzaV3/CodexPlusPlusScriptMarket/main/scripts/bennett-ui-improvements.js", + "sha256": "4c13009815d74b9aa911e80660cf70596c87340d2ed2f194dbe37915f9cc969f" } ] } diff --git a/scripts/bennett-ui-improvements.js b/scripts/bennett-ui-improvements.js new file mode 100644 index 0000000..7c892b1 --- /dev/null +++ b/scripts/bennett-ui-improvements.js @@ -0,0 +1,7981 @@ +/* + * Bennett UI Improvements for BigPizzaV3 Codex++ + * + * Source project: https://github.com/b-nnett/codex-plusplus-bennett-ui + * Original tweak id: co.bennett.ui-improvements + * Original author: bennett + * Original license: MIT License, Copyright (c) 2026 Bennett + * + * This file is a compatibility migration from the b-nnett Codex++ tweak + * runtime to the BigPizzaV3 Codex++ renderer-only user script runtime. + * The UI implementation below is not original work by the migrator; the + * wrapper only adapts storage/logging/renderer lifecycle assumptions. + * + * MIT permission notice from the source project applies: permission is + * granted to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies, provided the copyright notice and permission notice + * are included in all copies or substantial portions of the Software. + */ + +(() => { + "use strict"; + + const INSTALL_KEY = "__bennettUiImprovementsBigPizza"; + const VERSION = "1.0.5-bigpizza.1"; + const previous = window[INSTALL_KEY]; + if (previous && typeof previous.stop === "function") { + try { + previous.stop(); + } catch (error) { + console.warn("[Bennett UI/BigPizza] previous stop failed", error); + } + } + + const module = { exports: {} }; + const exports = module.exports; +/** + * Bennett's UI Improvements + * + * A bag of small, individually-toggleable UI tweaks for Codex. Settings + * live on a dedicated sidebar entry under the "Tweaks" group. + * + * Features + * -------- + * • hide-upgrade-prompts Hides the sidebar "Upgrade" pill and the + * top-bar "Get Plus" button. Pure DOM filter, + * fully reversible. + * • show-usage-in-sidebar (experimental) Renders a single usage box where + * the upgrade pill was. Click toggles between + * 5h and Weekly; hover replaces content with + * "Resets: HH:MM" or "Resets: Wed, HH:MM". + * Red when <15% remaining. + * Sources data from Codex's authenticated + * /wham/usage app-server endpoint. + * • square-sidebar Flatten the rounded seam between sidebar and + * main content panel. + * • settings-search Adds a small search field to Codex Settings. + * • match-sidebar-width Force the settings page sidebar to match the + * main UI sidebar's width, eliminating the + * layout jump when opening/closing Settings. + * • sidebar-action-grid Render the four main sidebar actions as a 2x2 + * grid of filled buttons. + * • sidebar-project-backgrounds Add subtle grouped backgrounds behind + * project rows in the main sidebar. + * • sidebar-chat-multi-select Cmd/Ctrl-click sidebar chats to select + * multiple rows and run batch actions. + * • show-pinned-chat-project-names Shows a small project name under + * pinned sidebar chats. + * • show-message-metrics-on-hover Shows Codex token metrics beside + * assistant messages on hover. + * • slash-menu-polish Tightens the composer slash menu with denser rows, + * clearer active state, and calmer section headers. + * + * Authoring notes + * --------------- + * • Renderer + main; main reads local Codex session JSONL for metrics. + * • Each feature returns a `dispose()` so toggling off is clean. + * • Match-by-text-content for resilience: Codex's main shell has no + * stable testids/aria-labels for these widgets. + */ + +/** @type {import("@codex-plusplus/sdk").Tweak} */ +module.exports = { + start(api) { + if (api.process === "main") { + startMainMetricsProvider(api); + startMainUsageProvider(api); + startMainProjectLabelProvider(api); + startMainSidebarBatchMenuProvider(api); + startMainSlashMenuShortcutBridge(api); + return; + } + + const state = { + api, + features: new Map(/* id -> { dispose } */), + defaults: { + "hide-upgrade-prompts": true, + "show-usage-in-sidebar": true, + "show-message-metrics-on-hover": false, + "square-sidebar": false, + "settings-search": true, + "match-sidebar-width": true, + "sidebar-action-grid": true, + "sidebar-project-backgrounds": true, + "sidebar-chat-multi-select": false, + "show-pinned-chat-project-names": false, + "slash-menu-polish": true, + }, + }; + this._state = state; + + // ── settings page ────────────────────────────────────────────────── + // We require `registerPage`. The older `register()` API would render + // these toggles as a *nested section* inside Codex++'s built-in + // "Tweaks" page — that's misleading, since this tweak is supposed to + // own its own sidebar entry. If the runtime is too old we just log + // and skip the UI; the features themselves still activate below. + if (typeof api.settings?.registerPage !== "function") { + api.log.warn( + "registerPage unavailable — Codex++ runtime is too old. " + + "Restart Codex to pick up the latest preload. Settings UI not mounted.", + ); + } else { + this._pageHandle = api.settings.registerPage({ + id: "main", + title: "UI Improvements", + description: "Bennett's small quality-of-life tweaks.", + iconSvg: + '", + render: (root) => renderSettings(root, state), + }); + } + + // ── activate features per stored prefs ───────────────────────────── + for (const id of Object.keys(state.defaults)) { + const enabled = readFlag(api, id, state.defaults[id]); + if (enabled && FEATURES[id]) activateFeature(state, id); + } + }, + + stop() { + const s = this._state; + if (!s) return; + for (const [, f] of s.features) { + try { + f.dispose?.(); + } catch (e) { + s.api.log.warn("dispose failed", e); + } + } + s.features.clear(); + this._pageHandle?.unregister(); + }, +}; + +// ─────────────────────────────────────────────────────────── settings UI ── + +/** + * Render the dedicated page. Mirrors Codex's standard form: one + * `flex flex-col gap-2` section per group, rounded card with rows. + */ +function renderSettings(root, state) { + const features = [ + { + id: "hide-upgrade-prompts", + title: "Hide upgrade prompts", + description: + 'Hide the "Upgrade" pill in the app sidebar and the "Get Plus" button in the top bar.', + }, + { + id: "show-usage-in-sidebar", + title: "Show usage in sidebar (experimental)", + description: + "Render 5-hour and weekly rate limits where the upgrade button was. Open the rate-limits breakdown (account menu → Rate limits) at least once to seed the values.", + }, + { + id: "show-message-metrics-on-hover", + title: "Show message metrics on hover", + description: + "Show per-turn token usage beside assistant messages.", + }, + { + id: "square-sidebar", + title: "Square sidebar corners", + description: + "Remove the rounded inner corners on the main content panel so it sits flush against the sidebar.", + }, + { + id: "settings-search", + title: "Settings search", + description: + "Add a search field above the Settings tabs so sections can be filtered quickly.", + }, + { + id: "match-sidebar-width", + title: "Match settings sidebar width", + description: + "Stop the layout jump when opening Settings: the settings sidebar (fixed at 300px) is forced to match the main UI sidebar's current width.", + }, + { + id: "sidebar-action-grid", + title: "Sidebar action grid", + description: + "Render New chat, Search, Plugins, and Automations as a compact 2x2 grid of filled buttons.", + }, + { + id: "sidebar-project-backgrounds", + title: "Sidebar project backgrounds", + description: + "Add subtle grouped backgrounds behind project rows so adjacent projects are easier to scan.", + }, + { + id: "sidebar-chat-multi-select", + title: "Multi-select sidebar chats", + description: + "Cmd/Ctrl-click sidebar chats to select multiple rows, then right-click for batch actions.", + }, + { + id: "show-pinned-chat-project-names", + title: "Show project label for pinned chats", + description: + "Show a smaller, subdued project label under pinned chats, and under all chats in chronological list mode.", + }, + { + id: "slash-menu-polish", + title: "Slash menu polish", + description: + "Tighten the composer slash menu with denser rows, clearer active state, and calmer section headers.", + }, + ]; + + const section = el("section", "flex flex-col gap-2"); + section.appendChild(sectionTitle("Features")); + + const card = roundedCard(); + for (const f of features) { + card.appendChild(featureRow(state, f)); + } + section.appendChild(card); + root.appendChild(section); +} + +/** + * Heuristic sidebar finder. Codex's left rail is typically a flex column + * pinned to x=0 with substantial height. We rank candidates by: + * • bounding-rect.left near 0 + * • height > 60% of viewport + * • narrow-ish width (< 360px) for collapsed/expanded sidebars + * • presence of `nav` or aria-label="Primary" + * and pick the best. Returns the chosen element + a few selector hints. + * + * Currently unused — kept around for ad-hoc DOM debugging during tweak + * development. Wire it up to a temporary button if needed. + */ +// eslint-disable-next-line no-unused-vars +async function dumpSidebar(api) { + const candidates = []; + const all = document.querySelectorAll( + 'aside, nav, [role="navigation"], [data-testid*="sidebar" i], div', + ); + const vh = window.innerHeight; + for (const el of all) { + const r = el.getBoundingClientRect(); + if (r.left > 8) continue; + if (r.height < vh * 0.6) continue; + if (r.width < 40 || r.width > 420) continue; + let score = 0; + if (el.tagName === "ASIDE" || el.tagName === "NAV") score += 5; + if (el.getAttribute("role") === "navigation") score += 3; + if (el.querySelector("nav")) score += 1; + if (/sidebar/i.test(el.getAttribute("data-testid") || "")) score += 4; + if (/rounded/.test(el.className || "")) score += 2; + score += Math.max(0, 200 - r.width) / 100; // prefer narrower + candidates.push({ el, score, rect: r }); + } + candidates.sort((a, b) => b.score - a.score); + const top = candidates[0]; + if (!top) return { ok: false, reason: "no candidate" }; + + const html = top.el.outerHTML; + const summary = candidates.slice(0, 5).map((c) => ({ + tag: c.el.tagName.toLowerCase(), + classes: c.el.className, + rect: { + x: Math.round(c.rect.left), + y: Math.round(c.rect.top), + w: Math.round(c.rect.width), + h: Math.round(c.rect.height), + }, + score: c.score, + })); + + const payload = + `\n` + + summary.map((s) => "").join("\n") + + `\n\n\n` + + html; + + let wrotePath = null; + try { + if (typeof api.fs?.write === "function") { + await api.fs.write("sidebar-dump.html", payload); + wrotePath = "sidebar-dump.html (in tweak data dir)"; + } + } catch (e) { + api.log.warn("fs.write failed", e); + } + + let copied = false; + try { + await navigator.clipboard.writeText(payload); + copied = true; + } catch (e) { + api.log.warn("clipboard write failed", e); + } + + return { ok: true, copied, wrotePath, summary }; +} + +function featureRow(state, f) { + const row = el("div", "flex items-center justify-between gap-4 p-3"); + + const left = el("div", "flex min-w-0 flex-col gap-1"); + const label = el("div", "min-w-0 text-sm text-token-text-primary"); + label.textContent = f.title; + left.appendChild(label); + if (f.description) { + const desc = el("div", "text-token-text-secondary min-w-0 text-sm"); + desc.textContent = f.description; + left.appendChild(desc); + } + row.appendChild(left); + + const initial = readFlag(state.api, f.id, state.defaults[f.id]); + const sw = switchControl(initial, async (next) => { + writeFlag(state.api, f.id, next); + window.dispatchEvent(new CustomEvent("codexpp-ui-improvements-setting-changed", { + detail: { id: f.id, value: next }, + })); + if (next) activateFeature(state, f.id); + else deactivateFeature(state, f.id); + }); + row.appendChild(sw); + return row; +} + +// ─────────────────────────────────────────────────────────── feature reg ── + +function activateFeature(state, id) { + if (state.features.has(id)) return; + const fn = FEATURES[id]; + if (!fn) { + state.api.log.warn("unknown feature", id); + return; + } + try { + const dispose = fn(state.api); + state.features.set(id, { dispose }); + state.api.log.info("activated", id); + } catch (e) { + state.api.log.error("activate failed", id, e); + } +} + +function deactivateFeature(state, id) { + const f = state.features.get(id); + if (!f) return; + try { + f.dispose?.(); + } finally { + state.features.delete(id); + state.api.log.info("deactivated", id); + } +} + +// ─────────────────────────────────────────────────────────────── features ── + +const FEATURES = { + /** + * Hide the "Upgrade" / "Get Plus" buttons. We match by visible text + * across the document, skipping anything inside Codex's settings shell + * or our own injected panels. Hidden via inline `display:none` so we + * can restore it cleanly on dispose. + */ + "hide-upgrade-prompts"(api) { + // Two matcher tiers: + // • EXACT: short pill labels we trust (case-insensitive, exact match). + // • CONTAINS: longer phrases that may appear with trailing icons/arrows + // or wrapped in extra spans. We substring-match (case-insensitive). + const EXACT = new Set([ + "upgrade", + "get plus", + "get chatgpt plus", + "upgrade plan", + "upgrade your plan", + "upgrade to plus", + ]); + const CONTAINS = ["upgrade for higher limits"]; + const hidden = new Set(/* HTMLElement */); + + const isInsideOurShell = (el) => { + let n = el; + while (n) { + if (n instanceof HTMLElement && n.dataset?.codexpp) return true; + n = n.parentElement; + } + return false; + }; + + // Codex sometimes splits the label across icon + text spans, so we use + // textContent and collapse whitespace. + const normText = (el) => + (el.textContent || "").replace(/\s+/g, " ").trim().toLowerCase(); + + const matches = (text) => { + if (!text) return false; + if (EXACT.has(text)) return true; + for (const c of CONTAINS) if (text.includes(c)) return true; + return false; + }; + + const scan = () => { + const candidates = document.querySelectorAll( + 'button, a, [role="button"], [role="menuitem"]', + ); + for (const el of candidates) { + if (hidden.has(el)) continue; + if (isInsideOurShell(el)) continue; + const t = normText(el); + if (t.length === 0 || t.length > 80) continue; + if (!matches(t)) continue; + const host = el.closest('[class*="rounded"], [class*="badge"]') || el; + if (!(host instanceof HTMLElement)) continue; + host.dataset.codexppPrevDisplay = host.style.display || ""; + host.style.display = "none"; + hidden.add(host); + api.log.info("hid upgrade element", { text: t }); + } + }; + + scan(); + const obs = new MutationObserver(scan); + obs.observe(document.documentElement, { childList: true, subtree: true }); + + return () => { + obs.disconnect(); + for (const el of hidden) { + if ("codexppPrevDisplay" in el.dataset) { + el.style.display = el.dataset.codexppPrevDisplay; + delete el.dataset.codexppPrevDisplay; + } + } + hidden.clear(); + }; + }, + + /** + * Surface 5h + Weekly rate limits in the sidebar slot where the "Upgrade" + * pill lives. Sources its data from Codex's authenticated app-server usage + * endpoint, with Codex's rendered rate-limit UI as a fallback. + * + * Strategy + * -------- + * 1. Fetch `/wham/usage` through Codex's existing renderer fetch bridge. + * 2. Parse the expanded/compact rendered labels only when the bridge is + * unavailable or the request fails. + * 3. Persist the latest snapshot and refresh the mounted sidebar box in + * place. Re-mount only when Codex replaces the sidebar subtree. + */ + "show-usage-in-sidebar"(api) { + /** + * Persisted snapshot: + * { fiveHour:{label,pct,resetAt} | null, + * weekly: {label,pct,resetAt} | null, + * at:number } + * `pct` is REMAINING (Codex displays remaining %, e.g. "100%"). + * `resetAt` is whatever Codex shows verbatim (typically "HH:MM", + * or "Wed, HH:MM" for weekly API data). + */ + let snapshot = null; // Do not render persisted quota data before this page fetches fresh usage. + let mounted = null; // HTMLElement currently rendered in the sidebar + let directUsageAvailable = false; + let directUsageInFlight = false; + let directUsageLastAttemptAt = 0; + let directUsageFailureLogged = false; + let directUsageSuccessLogged = false; + let usageBridgeReadyLogged = false; + let usageBridgeScriptInjected = false; + let bridgeRequestSeq = 0; + let lastMountedMode = null; + let accountMode = "unknown"; // "official" | "api" | "unknown" + let accountModeInFlight = false; + let accountModeLastCheckedAt = 0; + let accountModeLogged = false; + + const log = (...a) => api.log.info("[usage]", ...a); + const ASIDE_SELECTOR = [ + "aside.pointer-events-auto.relative.flex.overflow-hidden", + "aside.pointer-events-auto.relative.flex.overflow-visible", + "aside.pointer-events-auto.relative.flex", + ].join(", "); + + // ── parsing ──────────────────────────────────────────────────────── + const isVisibleElement = (node) => { + if (!(node instanceof HTMLElement) || !node.isConnected) return false; + if (node.closest("[hidden], [inert], [aria-hidden='true']")) return false; + const style = window.getComputedStyle(node); + if ( + style.display === "none" || + style.visibility === "hidden" || + style.opacity === "0" + ) { + return false; + } + const rect = node.getBoundingClientRect(); + return rect.width > 0 && rect.height > 0; + }; + + const applySnapshot = (partial, source) => { + if (!partial?.fiveHour && !partial?.weekly) return false; + if (accountMode === "api") return false; + const next = { + fiveHour: partial.fiveHour || snapshot?.fiveHour || null, + weekly: partial.weekly || snapshot?.weekly || null, + at: Date.now(), + }; + const changed = + JSON.stringify(next.fiveHour) !== JSON.stringify(snapshot?.fiveHour) || + JSON.stringify(next.weekly) !== JSON.stringify(snapshot?.weekly); + snapshot = next; + writeSnapshot(api, snapshot); + if (changed) { + log(`parsed snapshot from ${source}`, snapshot); + ensureMounted(); + } + return changed; + }; + + const ensureUsageBridgeScript = () => { + if (usageBridgeScriptInjected) return; + usageBridgeScriptInjected = true; + window.addEventListener( + "codexpp-usage-bridge-ready", + (event) => { + if (usageBridgeReadyLogged) return; + usageBridgeReadyLogged = true; + api.log.info("[usage] bridge ready", event.detail); + }, + { once: true }, + ); + const script = document.createElement("script"); + script.dataset.codexppUsageBridge = "true"; + script.textContent = `(() => { + if (window.__codexppUsageBridgeInstalled) return; + window.__codexppUsageBridgeInstalled = true; + const pending = new Set(); + window.dispatchEvent(new CustomEvent("codexpp-usage-bridge-ready", { + detail: { + hasElectronBridge: typeof window.electronBridge?.sendMessageFromView === "function", + }, + })); + window.addEventListener("codexpp-usage-request", (event) => { + const message = event.detail; + if (!message || typeof message !== "object" || !message.requestId) return; + pending.add(message.requestId); + let forwarded = false; + const bridge = window.electronBridge; + if (typeof bridge?.sendMessageFromView === "function") { + forwarded = true; + bridge.sendMessageFromView(message).catch(() => {}); + } + const forwardedEvent = new CustomEvent("codex-message-from-view", { + detail: message, + }); + if (forwarded) forwardedEvent.__codexForwardedViaBridge = true; + window.dispatchEvent(forwardedEvent); + }); + window.addEventListener("message", (event) => { + const data = event.data; + if ( + !data || + typeof data !== "object" || + data.type !== "fetch-response" || + !pending.has(data.requestId) + ) { + return; + } + pending.delete(data.requestId); + window.dispatchEvent(new CustomEvent("codexpp-usage-response", { + detail: data, + })); + window.postMessage({ + type: "codexpp-usage-response", + detail: data, + }, "*"); + }); + })();`; + (document.head || document.documentElement).appendChild(script); + script.remove(); + }; + + const dispatchCodexViewMessage = (message) => { + ensureUsageBridgeScript(); + window.dispatchEvent( + new CustomEvent("codexpp-usage-request", { detail: message }), + ); + + let forwarded = false; + const bridge = window.electronBridge; + if (typeof bridge?.sendMessageFromView === "function") { + forwarded = true; + bridge.sendMessageFromView(message).catch((e) => { + if (!directUsageFailureLogged) { + directUsageFailureLogged = true; + api.log.warn("[usage] bridge send failed", e); + } + }); + } + const event = new CustomEvent("codex-message-from-view", { + detail: message, + }); + if (forwarded) event.__codexForwardedViaBridge = true; + window.dispatchEvent(event); + }; + + const fetchCodexAppServerJson = async (url, timeoutMs = 10_000) => { + try { + return await api.ipc.invoke("usage-fetch", url); + } catch { + // Older runtimes or a failed main-webview probe fall through to the + // renderer bridge attempt below. + } + + const hostId = + new URL(window.location.href).searchParams.get("hostId")?.trim() || + "local"; + const requestId = `codexpp-usage-${Date.now()}-${++bridgeRequestSeq}`; + + return new Promise((resolve, reject) => { + let done = false; + const cleanup = () => { + done = true; + window.removeEventListener("message", onMessage); + window.removeEventListener("codexpp-usage-response", onBridgeResponse); + window.clearTimeout(timer); + }; + const finish = (fn, value) => { + if (done) return; + cleanup(); + fn(value); + }; + const onMessage = (event) => { + const data = + event.data?.type === "codexpp-usage-response" + ? event.data.detail + : event.data; + handleResponse(data); + }; + const onBridgeResponse = (event) => { + handleResponse(event.detail); + }; + const handleResponse = (data) => { + if ( + !data || + typeof data !== "object" || + data.type !== "fetch-response" || + data.requestId !== requestId + ) { + return; + } + if (data.responseType === "success") { + try { + const body = JSON.parse(data.bodyJsonString); + if (data.status >= 200 && data.status < 300) { + finish(resolve, body); + } else { + finish(reject, new Error(`HTTP ${data.status}`)); + } + } catch (e) { + finish(reject, e); + } + } else { + finish(reject, new Error(data.error || "fetch failed")); + } + }; + const timer = window.setTimeout(() => { + dispatchCodexViewMessage({ type: "cancel-fetch", requestId }); + finish(reject, new Error("usage request timed out")); + }, timeoutMs); + window.addEventListener("message", onMessage); + window.addEventListener("codexpp-usage-response", onBridgeResponse); + dispatchCodexViewMessage({ + type: "fetch", + hostId, + requestId, + method: "GET", + url, + }); + }); + }; + + const bridgePostJson = async (path, payload = {}, timeoutMs = 2_500) => { + const bridge = window.__codexSessionDeleteBridge; + if (typeof bridge !== "function") return null; + return await Promise.race([ + bridge(path, payload), + new Promise((resolve) => window.setTimeout(() => resolve(null), timeoutMs)), + ]); + }; + + const activeRelayProfile = (settings) => { + if (!settings || typeof settings !== "object") return null; + const profiles = Array.isArray(settings.relayProfiles) ? settings.relayProfiles : []; + const activeId = + typeof settings.activeRelayId === "string" && settings.activeRelayId.trim() + ? settings.activeRelayId + : "default"; + return profiles.find((profile) => profile?.id === activeId) || profiles[0] || null; + }; + + const fieldValue = (object, ...keys) => { + if (!object || typeof object !== "object") return undefined; + for (const key of keys) { + if (Object.prototype.hasOwnProperty.call(object, key)) return object[key]; + } + return undefined; + }; + + const catalogLooksLikeApiMode = (catalog) => { + if (!catalog || typeof catalog !== "object") return false; + const provider = String(catalog.model_provider || catalog.provider_name || "").toLowerCase(); + if (!provider) return false; + return !["openai", "chatgpt"].includes(provider); + }; + + const refreshAccountMode = async (force = false) => { + if (accountModeInFlight) return accountMode; + const now = Date.now(); + if (!force && accountModeLastCheckedAt && now - accountModeLastCheckedAt < 10_000) { + return accountMode; + } + accountModeLastCheckedAt = now; + accountModeInFlight = true; + try { + let nextMode = "unknown"; + let settingsMode = "unknown"; + const settings = await bridgePostJson("/settings/get", {}); + const profile = activeRelayProfile(settings); + const relayMode = fieldValue(profile, "relayMode", "relay_mode"); + const officialMixApiKey = !!fieldValue(profile, "officialMixApiKey", "official_mix_api_key"); + const legacyApiConfigured = !!( + String(fieldValue(settings, "relayApiKey", "relay_api_key") || "").trim() || + String(fieldValue(settings, "relayBaseUrl", "relay_base_url") || "").trim() + ); + if (relayMode === "official" && !officialMixApiKey) { + nextMode = "official"; + } else if (relayMode === "pureApi" || relayMode === "pure_api") { + nextMode = "api"; + } else if (relayMode === "mixedApi" || relayMode === "mixed_api" || officialMixApiKey) { + nextMode = "api"; + } else if (!relayMode && legacyApiConfigured) { + nextMode = "api"; + } + + if (nextMode === "unknown") { + const catalog = await bridgePostJson("/codex-model-catalog", {}); + if (catalogLooksLikeApiMode(catalog)) nextMode = "api"; + else if (catalog?.model_provider === "openai" || catalog?.provider_name === "openai") { + nextMode = "official"; + } + } + if (nextMode === "unknown") nextMode = settingsMode; + + if (nextMode !== "unknown" && nextMode !== accountMode) { + accountMode = nextMode; + if (accountMode === "api") { + snapshot = { + fiveHour: { label: "API", pct: null, resetAt: null, apiMode: true }, + weekly: null, + at: Date.now(), + apiMode: true, + }; + } else if (snapshot?.apiMode) { + snapshot = null; + } + ensureMounted(true); + } + if (!accountModeLogged && accountMode !== "unknown") { + accountModeLogged = true; + log("account mode", accountMode); + } + return accountMode; + } catch (e) { + return accountMode; + } finally { + accountModeInFlight = false; + } + }; + + const remainingPercent = (usedPercent) => { + const used = Number(usedPercent); + if (!Number.isFinite(used)) return null; + return Math.round(Math.min(Math.max(100 - used, 0), 100)); + }; + + const formatResetAt = (epochSeconds, includeDay = false) => { + const seconds = Number(epochSeconds); + if (!Number.isFinite(seconds)) return null; + const date = new Date(seconds * 1000); + if (!Number.isFinite(date.getTime())) return null; + return date.toLocaleTimeString(undefined, { + ...(includeDay ? { weekday: "short" } : {}), + hour: "numeric", + minute: "2-digit", + }); + }; + + const normalizeUsageWindow = (window, label) => { + if (!window || typeof window !== "object") return null; + const pct = remainingPercent(window.used_percent); + if (pct == null) return null; + const minutes = Number(window.limit_window_seconds) / 60; + const includeResetDay = Number.isFinite(minutes) && minutes >= 1440; + return { + label, + pct, + resetAt: formatResetAt(window.reset_at, includeResetDay), + }; + }; + + const pickClosestWindow = (windows, targetMinutes, predicate) => { + let best = null; + let bestDistance = Infinity; + for (const window of windows) { + const minutes = Number(window?.limit_window_seconds) / 60; + if (!Number.isFinite(minutes) || !predicate(minutes)) continue; + const distance = Math.abs(minutes - targetMinutes); + if ( + !best || + distance < bestDistance || + (distance === bestDistance && + minutes > Number(best.limit_window_seconds) / 60) + ) { + best = window; + bestDistance = distance; + } + } + return best; + }; + + const snapshotFromUsageStatus = (status) => { + const limits = []; + const pushLimit = (rateLimit) => { + if (!rateLimit || typeof rateLimit !== "object") return; + if (rateLimit.primary_window) limits.push(rateLimit.primary_window); + if (rateLimit.secondary_window) limits.push(rateLimit.secondary_window); + }; + + pushLimit(status?.rate_limit); + if (Array.isArray(status?.additional_rate_limits)) { + for (const item of status.additional_rate_limits) { + pushLimit(item?.rate_limit); + } + } + + const five = pickClosestWindow( + limits, + 300, + (minutes) => minutes > 0 && minutes < 1440, + ); + const weekly = pickClosestWindow( + limits, + 7 * 24 * 60, + (minutes) => minutes >= 1440, + ); + + return { + fiveHour: normalizeUsageWindow(five, "5h"), + weekly: normalizeUsageWindow(weekly, "Weekly"), + }; + }; + + const collectUsageWindows = (value, out = [], seen = new WeakSet()) => { + if (!value || typeof value !== "object") return out; + if (seen.has(value)) return out; + seen.add(value); + if ( + "used_percent" in value && + "limit_window_seconds" in value && + "reset_at" in value + ) { + out.push(value); + } + if (Array.isArray(value)) { + for (const item of value) collectUsageWindows(item, out, seen); + } else { + for (const item of Object.values(value)) { + collectUsageWindows(item, out, seen); + } + } + return out; + }; + + const snapshotFromUsageWindows = (windows) => { + const five = pickClosestWindow( + windows, + 300, + (minutes) => minutes > 0 && minutes < 1440, + ); + const weekly = pickClosestWindow( + windows, + 7 * 24 * 60, + (minutes) => minutes >= 1440, + ); + return { + fiveHour: normalizeUsageWindow(five, "5h"), + weekly: normalizeUsageWindow(weekly, "Weekly"), + }; + }; + + const applyUsageEvent = (message) => { + if (!message || typeof message !== "object") return false; + const windows = collectUsageWindows(message); + if (!windows.length) return false; + const partial = snapshotFromUsageWindows(windows); + if (!partial.fiveHour && !partial.weekly) return false; + directUsageAvailable = true; + applySnapshot(partial, "rate-limit-event"); + return true; + }; + + const onUsageMessage = (event) => { + const data = event.data; + if (!data || typeof data !== "object") return; + applyUsageEvent(data); + }; + + const refreshUsageFromApi = async () => { + if ((await refreshAccountMode()) !== "official") return false; + if (directUsageInFlight) return false; + const now = Date.now(); + if (directUsageLastAttemptAt && now - directUsageLastAttemptAt < 15_000) { + return false; + } + directUsageLastAttemptAt = now; + directUsageInFlight = true; + try { + const status = await fetchCodexAppServerJson("/wham/usage"); + const partial = snapshotFromUsageStatus(status); + if (partial.fiveHour || partial.weekly) { + directUsageAvailable = true; + directUsageFailureLogged = false; + if (!directUsageSuccessLogged) { + directUsageSuccessLogged = true; + log("api active", partial); + } + applySnapshot(partial, "api"); + return true; + } + return false; + } catch (e) { + if (!directUsageFailureLogged) { + directUsageFailureLogged = true; + api.log.warn("[usage] /wham/usage unavailable; falling back to DOM", e); + } + return false; + } finally { + directUsageInFlight = false; + } + }; + + /** + * Codex's expanded breakdown is a 2-column CSS grid: label in col-1, + * value in col-2. We locate the grid by its unique class signature, + * then walk children pairwise. + * + * Returns the breakdown grid element, or null. + */ + const findBreakdownGrid = () => { + // The full class string is long and may shift; we anchor on the + // distinctive `grid-cols-[minmax(0,1fr)_auto]` token. + const grids = document.querySelectorAll( + 'div[class*="grid-cols-[minmax(0,1fr)_auto]"]', + ); + for (const g of grids) { + if (!isVisibleElement(g)) continue; + const txt = (g.textContent || "").toLowerCase(); + if ( + (txt.includes("5h") || txt.includes("hourly")) && + txt.includes("week") + ) + return g; + } + return null; + }; + + /** + * Parse a value span (e.g. "100%·16:19") into `{ pct, resetAt }`. + * Falls back to `null` fields when a piece is missing. + */ + const parseValueText = (txt, root) => { + const pctMatch = txt.match(/(\d{1,3})\s*%/); + const pct = pctMatch ? Math.max(0, Math.min(100, +pctMatch[1])) : null; + // Prefer the inner [title="HH:MM"] attribute, else regex the text. + const titled = root?.querySelector?.("[title]"); + let resetAt = titled ? titled.getAttribute("title") : null; + if (!resetAt) { + const tMatch = + txt.match(/\b(\d{1,2}:\d{2})\b/) || + txt.match(/\b(\d+\s*(?:m|h|d))\b/i); + resetAt = tMatch ? tMatch[1] : null; + } + return { pct, resetAt }; + }; + + const parseValue = (span) => { + const txt = (span.textContent || "").replace(/\s+/g, " ").trim(); + return parseValueText(txt, span); + }; + + const scanBreakdown = (grid) => { + const kids = Array.from(grid.children); + let five = null; + let week = null; + // Pair (label, value) — col-1 then col-2 in DOM order. + for (let i = 0; i + 1 < kids.length; i += 2) { + const labelTxt = (kids[i].textContent || "") + .replace(/\s+/g, " ") + .trim(); + const value = parseValue(kids[i + 1]); + const lower = labelTxt.toLowerCase(); + if (!five && (lower === "5h" || lower.startsWith("hourly"))) { + five = { label: labelTxt, ...value }; + } else if (!week && lower.startsWith("week")) { + week = { label: labelTxt, ...value }; + } + } + if (!five && !week) return false; + applySnapshot({ fiveHour: five, weekly: week }, "breakdown"); + return true; + }; + + const parseCompactUsageNode = (node) => { + if (!(node instanceof HTMLElement)) return null; + if (node.closest('[data-codexpp="usage-box"]')) return null; + if (!isVisibleElement(node)) return null; + const text = (node.textContent || "").replace(/\s+/g, " ").trim(); + if (!text || text.length > 160 || !/%/.test(text)) return null; + const lower = text.toLowerCase(); + const hasFive = /\b(5h|5\s*hour|hourly)\b/.test(lower); + const hasWeek = /\b(weekly|week)\b/.test(lower); + if (!hasFive && !hasWeek) return null; + + const value = parseValueText(text, node); + if (value.pct == null) return null; + const label = hasFive && !hasWeek ? "5h" : hasWeek && !hasFive ? "Weekly" : null; + if (!label) return null; + return label === "5h" + ? { fiveHour: { label, ...value } } + : { weekly: { label, ...value } }; + }; + + const scanCompactUsage = () => { + const candidates = document.querySelectorAll( + 'button, [role="button"], [role="status"], [aria-label], [title], span', + ); + for (const node of candidates) { + const partial = parseCompactUsageNode(node); + if (partial) applySnapshot(partial, "compact"); + } + }; + + // ── sidebar mount ───────────────────────────────────────────────── + /** + * Find the sidebar slot for the upgrade pill. The pill itself is + * hidden by `hide-upgrade-prompts`, so we mount as a sibling that + * replaces its visual footprint. We anchor on the parent of any + * button/link with text "Upgrade" (case-insensitive), or fall back + * to the bottom of the sidebar group. + * + * Returns the parent element to mount into, or null if not found. + */ + const compactSidebarText = (node) => + (node?.textContent || "").replace(/\s+/g, " ").trim().toLowerCase(); + + const looksLikeSettingsSidebar = (sidebar) => { + if (!(sidebar instanceof HTMLElement)) return false; + if ( + sidebar.matches(".window-fx-sidebar-surface.w-token-sidebar") || + sidebar.closest(".window-fx-sidebar-surface.w-token-sidebar") || + sidebar.querySelector("[data-codexpp-settings-search]") + ) { + return true; + } + const text = compactSidebarText(sidebar); + const englishSettings = + text.includes("general") && + text.includes("appearance") && + (text.includes("account") || text.includes("configuration")); + const chineseSettings = + text.includes("常规") && + text.includes("外观") && + ( + text.includes("配置") || + text.includes("个性化") || + text.includes("键盘快捷键") || + text.includes("mcp 服务器") || + text.includes("钩子") || + text.includes("连接") || + text.includes("环境") || + text.includes("工作树") || + text.includes("已归档") + ); + return englishSettings || chineseSettings; + }; + + const looksLikeMainAppSidebar = (sidebar) => { + const text = compactSidebarText(sidebar); + const hasNewChat = /\bnew chat\b|\bquick chat\b|新建|新对话/.test(text); + const hasSearch = /\bsearch\b|搜索/.test(text); + const hasProjectOrHistory = + /\bprojects?\b|\bhistory\b|\bchats?\b|项目|历史|会话/.test(text); + return (hasNewChat && hasSearch) || (hasSearch && hasProjectOrHistory); + }; + + const findUsageSidebar = () => { + const candidates = Array.from(document.querySelectorAll(ASIDE_SELECTOR)) + .filter((node) => node instanceof HTMLElement && isVisibleElement(node)) + .filter((sidebar) => { + const rect = sidebar.getBoundingClientRect(); + return rect.width >= 180 && !looksLikeSettingsSidebar(sidebar); + }); + return candidates.find(looksLikeMainAppSidebar) || null; + }; + + const controlText = (node) => + [ + node.getAttribute?.("aria-label"), + node.getAttribute?.("title"), + node.textContent, + ] + .filter(Boolean) + .join(" ") + .replace(/\s+/g, " ") + .trim() + .toLowerCase(); + + const isSettingsOrDeviceButton = (button) => { + const text = controlText(button); + return ( + /\bsettings?\b|preferences?|设置|偏好/.test(text) || + /\bmobile\b|\bphone\b|\bdevice\b|手机|移动|设备|连接/.test(text) + ); + }; + + const nearestControlRow = (sidebar, button) => { + const sidebarRect = sidebar.getBoundingClientRect(); + let row = button.parentElement; + while (row && row !== document.body && row !== sidebar.parentElement) { + if (!(row instanceof HTMLElement)) break; + const rect = row.getBoundingClientRect(); + const style = window.getComputedStyle(row); + const buttonCount = row.querySelectorAll('button, a, [role="button"]').length; + const insideSidebar = + rect.left >= sidebarRect.left - 8 && + rect.right <= sidebarRect.right + 8; + const looksLikeControlLayer = + insideSidebar && + rect.height > 0 && + rect.height <= 88 && + (style.display === "flex" || style.display === "grid" || buttonCount >= 2); + if (looksLikeControlLayer) return row; + row = row.parentElement; + } + return button.parentElement instanceof HTMLElement ? button.parentElement : null; + }; + + const createInlineSlot = (row, anchor) => { + const existing = row.querySelector(':scope > [data-codexpp="usage-slot"]'); + if (existing instanceof HTMLElement) return existing; + const slot = document.createElement("div"); + slot.dataset.codexpp = "usage-slot"; + slot.dataset.codexppUsageSlot = "controls-inline"; + slot.className = "flex shrink-0 items-center"; + if (anchor?.parentElement === row) row.insertBefore(slot, anchor); + else row.appendChild(slot); + return slot; + }; + + const findSidebarSlot = () => { + const sidebar = findUsageSidebar(); + if (!sidebar) return null; + const existingSlot = sidebar.querySelector('[data-codexpp="usage-slot"]'); + if (existingSlot instanceof HTMLElement) return existingSlot; + + const controls = Array.from(sidebar.querySelectorAll('button, a, [role="button"]')) + .filter((button) => button instanceof HTMLElement && isVisibleElement(button)); + const preferredControls = controls.filter(isSettingsOrDeviceButton); + const ordered = (preferredControls.length ? preferredControls : controls).sort((a, b) => { + const ar = a.getBoundingClientRect(); + const br = b.getBoundingClientRect(); + return br.bottom - ar.bottom; + }); + + for (const button of ordered) { + const row = nearestControlRow(sidebar, button); + if (row) return createInlineSlot(row, button); + } + + return null; + }; + + const displaySnapshot = () => + accountMode === "api" + ? { + fiveHour: { label: "API", pct: null, resetAt: null, apiMode: true }, + weekly: null, + at: Date.now(), + apiMode: true, + } + : + snapshot && (snapshot.fiveHour || snapshot.weekly) + ? snapshot + : { + fiveHour: { label: "5h", pct: null, resetAt: null }, + weekly: { label: "Weekly", pct: null, resetAt: null }, + at: 0, + }; + + const ensureMounted = (forceRebuild = false) => { + const visibleSnapshot = displaySnapshot(); + const slot = findSidebarSlot(); + document.querySelectorAll('[data-codexpp="usage-floating-slot"]').forEach((node) => node.remove()); + if (!slot) { + if (mounted) { + mounted.remove(); + mounted = null; + } + for (const stale of document.querySelectorAll( + '[data-codexpp="usage-box"], [data-codexpp="usage-boxes"]', + )) { + stale.remove(); + } + if (!ensureMounted._warned) { + log("ensureMounted: no sidebar slot found yet"); + ensureMounted._warned = true; + } + return; + } + + // Defensive: remove any stale boxes left by a previous mount cycle + // (hot-reload, stop() race, or an older shape of this tweak that + // used `data-codexpp="usage-boxes"`). + for (const stale of document.querySelectorAll( + '[data-codexpp="usage-box"], [data-codexpp="usage-boxes"]', + )) { + if (stale !== mounted) stale.remove(); + } + + if (mounted && slot.contains(mounted) && !forceRebuild) { + mounted._refresh?.(visibleSnapshot); + return; + } + if (mounted) mounted.remove(); + mounted = renderUsageBox(api, visibleSnapshot); + mounted.dataset.codexpp = "usage-box"; + slot.appendChild(mounted); + lastMountedMode = slot.dataset.codexppUsageSlot || "unknown"; + mounted.style.flex = "0 1 auto"; + mounted.style.width = "auto"; + mounted.style.minWidth = "4.75rem"; + mounted.style.maxWidth = "8.5rem"; + if (slot.dataset.codexppUsageSlot === "settings-inline-windows" || slot.dataset.codexppUsageSlot === "controls-inline") { + mounted.style.width = "auto"; + mounted.style.minWidth = "4.75rem"; + } + log("mounted usage box", { + mode: lastMountedMode, + slotTag: slot.tagName, + slotClass: slot.className, + }); + }; + + // Initial render from persisted snapshot (so first paint isn't empty + // even before the user opens the popover). + ensureMounted(true); + + // ── observers ───────────────────────────────────────────────────── + // We throttle to one tick per animation frame so a flood of React + // re-renders can't tank the renderer (Codex mutates the DOM heavily + // while typing). Coalesces N onMutate() calls into one scan. + let scheduled = false; + const onMutate = () => { + if (scheduled) return; + scheduled = true; + requestAnimationFrame(() => { + scheduled = false; + refreshAccountMode().then((mode) => { + if (mode === "official") refreshUsageFromApi(); + }); + if (accountMode === "official" && !directUsageAvailable) { + const grid = findBreakdownGrid(); + if (grid) scanBreakdown(grid); + scanCompactUsage(); + } + ensureMounted(); + }); + }; + + onMutate(); + const obs = new MutationObserver(onMutate); + obs.observe(document.documentElement, { childList: true, subtree: true }); + const interval = window.setInterval(onMutate, 15_000); + window.addEventListener("focus", onMutate); + window.addEventListener("message", onUsageMessage); + document.addEventListener("visibilitychange", onMutate); + + log("active", { snapshot }); + + return () => { + obs.disconnect(); + window.clearInterval(interval); + window.removeEventListener("focus", onMutate); + window.removeEventListener("message", onUsageMessage); + document.removeEventListener("visibilitychange", onMutate); + if (mounted) { + mounted.remove(); + mounted = null; + } + for (const slot of document.querySelectorAll('[data-codexpp="usage-slot"]')) { + if (slot instanceof HTMLElement && slot.children.length === 0) slot.remove(); + } + for (const slot of document.querySelectorAll('[data-codexpp="usage-floating-slot"]')) { + slot.remove(); + } + }; + }, + + /** + * Square sidebar: the visual "rounded sidebar" is actually the main + * content panel — `
` — + * which has `border-radius: 12.5px 0 0 12.5px` (TL+BL via Tailwind's + * logical `rounded-s-2xl`). Its rounded left edge curves into the + * sidebar, making the sidebar's TR+BR corners *appear* rounded. + * Flattening `.main-surface`'s left side squares the seam. + */ + "square-sidebar"() { + const STYLE_ID = "codexpp-square-sidebar"; + document.getElementById(STYLE_ID)?.remove(); + + const style = document.createElement("style"); + style.id = STYLE_ID; + style.textContent = ` + /* Flatten the main panel's left (logical-start) corners. + Codex applies these via Tailwind's rounded-s-2xl utility. */ + .main-surface { + border-start-start-radius: 0 !important; + border-end-start-radius: 0 !important; + } + `; + document.head.appendChild(style); + + return () => { + style.remove(); + }; + }, + + /** + * Refine the composer slash menu by lightly annotating the live DOM. + * + * Live DOM shape captured via Electron CDP: + * [data-composer-overlay-floating-ui] > slash panel + * slash panel [data-list-navigation-item="true"] + * slash panel .sticky.top-0 section headers + */ + "slash-menu-polish"() { + const STYLE_ID = "codexpp-slash-menu-polish"; + const MENU_ATTR = "data-codexpp-slash-menu"; + const OVERLAY_ATTR = "data-codexpp-slash-overlay"; + const TOPBAR_ATTR = "data-codexpp-slash-topbar"; + const SECTION_ATTR = "data-codexpp-slash-section"; + const SECTION_EMPTY_ATTR = "data-codexpp-slash-section-empty"; + const SECTION_TITLE_ATTR = "data-codexpp-slash-section-title"; + const SECTION_ICON_ATTR = "data-codexpp-slash-section-icon"; + const INPUT_MODE_ATTR = "data-codexpp-slash-input-mode"; + const PROGRAM_SCROLL_ATTR = "data-codexpp-slash-programmatic-scroll"; + const HOVER_SUPPRESS_ATTR = "data-codexpp-slash-hover-suppressed"; + const OVERLAY_NOISE_ATTR = "data-codexpp-slash-overlay-noise"; + const FAVORITES_GROUP_ATTR = "data-codexpp-slash-favorites"; + const FAVORITE_KEY_ATTR = "data-codexpp-slash-favorite-key"; + const FAVORITE_CLONE_ATTR = "data-codexpp-slash-favorite-clone"; + const FAVORITE_SOURCE_SECTION_ATTR = "data-codexpp-slash-favorite-source-section"; + const FAVORITE_DUPLICATE_HIDDEN_ATTR = + "data-codexpp-slash-favorite-duplicate-hidden"; + const FAVORITES_STORAGE_KEY = "codexpp.slashMenuFavorites.v1"; + const FAVORITE_BUTTON_CLASS = "codexpp-slash-favorite-button"; + const SKILL_COPY_CLASS = "codexpp-slash-skill-copy"; + document.getElementById(STYLE_ID)?.remove(); + + const style = document.createElement("style"); + style.id = STYLE_ID; + style.textContent = ` + [data-composer-overlay-floating-ui="true"] { + isolation: isolate; + } + + [data-composer-overlay-floating-ui="true"][${OVERLAY_ATTR}="true"] + > :not([${MENU_ATTR}="true"]) { + display: none !important; + } + + [data-composer-overlay-floating-ui="true"] + > [${OVERLAY_NOISE_ATTR}="true"] { + display: none !important; + } + + [data-composer-overlay-floating-ui="true"] + [data-codexpp="nav-group"], + [data-composer-overlay-floating-ui="true"] + [data-codexpp="pages-group"], + [data-composer-overlay-floating-ui="true"] + [data-codexpp="nav-config"], + [data-composer-overlay-floating-ui="true"] + [data-codexpp="nav-tweaks"], + [data-composer-overlay-floating-ui="true"] + [data-codexpp^="nav-page-"] { + display: none !important; + height: 0 !important; + min-height: 0 !important; + margin: 0 !important; + padding: 0 !important; + border: 0 !important; + overflow: hidden !important; + pointer-events: none !important; + } + + [class*="[container-name:home-main-content]"] + [data-codexpp="nav-group"], + [class*="[container-name:home-main-content]"] + [data-codexpp="pages-group"], + [class*="[container-name:home-main-content]"] + [data-codexpp="nav-config"], + [class*="[container-name:home-main-content]"] + [data-codexpp="nav-tweaks"], + [class*="[container-name:home-main-content]"] + [data-codexpp^="nav-tweak"], + [class*="[container-name:home-main-content]"] + [data-codexpp^="nav-page-"] { + display: none !important; + height: 0 !important; + min-height: 0 !important; + margin: 0 !important; + padding: 0 !important; + border: 0 !important; + overflow: hidden !important; + pointer-events: none !important; + } + + [data-composer-overlay-floating-ui="true"] + > [${MENU_ATTR}="true"] { + width: min(100%, calc(100vw - 1rem)) !important; + max-width: calc(100vw - 1rem) !important; + border-color: color-mix(in srgb, currentColor 13%, transparent) !important; + background-color: var(--color-token-dropdown-background) !important; + background-color: color-mix( + in srgb, + var(--color-token-dropdown-background) 94%, + var(--color-token-main-surface-primary) 6% + ) !important; + box-shadow: + 0 18px 48px rgb(0 0 0 / 0.28), + 0 1px 0 rgb(255 255 255 / 0.06) inset !important; + padding: 0.375rem !important; + backdrop-filter: blur(16px) saturate(130%) !important; + overflow-x: hidden !important; + } + + [data-composer-overlay-floating-ui="true"] + [${MENU_ATTR}="true"] + .vertical-scroll-fade-mask { + gap: 0.125rem !important; + overflow-x: hidden !important; + overscroll-behavior-x: none !important; + padding-top: 0.5rem !important; + } + + [data-composer-overlay-floating-ui="true"] + [${MENU_ATTR}="true"] + .vertical-scroll-fade-mask + > div:not([${TOPBAR_ATTR}]) { + display: flex !important; + flex: 0 0 auto !important; + flex-direction: column !important; + height: auto !important; + min-width: 0 !important; + max-width: 100% !important; + overflow-x: hidden !important; + overflow-y: visible !important; + } + + [data-composer-overlay-floating-ui="true"] + [${MENU_ATTR}="true"] + .vertical-scroll-fade-mask + > div[${SECTION_ATTR}]:not(:first-child) { + border-top: 1px solid color-mix(in srgb, currentColor 14%, transparent) !important; + margin-top: 0.25rem !important; + padding-top: 0.25rem !important; + } + + [data-composer-overlay-floating-ui="true"] + [${MENU_ATTR}="true"] + .vertical-scroll-fade-mask + > div[${SECTION_EMPTY_ATTR}="true"] { + display: none !important; + margin: 0 !important; + padding: 0 !important; + border: 0 !important; + } + + [data-composer-overlay-floating-ui="true"] + [${MENU_ATTR}="true"] + [${TOPBAR_ATTR}="true"] { + display: flex !important; + flex: none !important; + min-width: 0 !important; + align-items: center !important; + justify-content: space-between !important; + gap: 0.75rem !important; + margin: -0.375rem -0.375rem 0 !important; + border-bottom: 1px solid color-mix(in srgb, currentColor 10%, transparent) !important; + background-color: var(--color-token-dropdown-background) !important; + background-image: linear-gradient( + to bottom, + color-mix(in srgb, var(--color-token-dropdown-background) 98%, transparent), + color-mix(in srgb, var(--color-token-dropdown-background) 90%, transparent) + ) !important; + padding: 0.375rem 0.5rem 0.375rem 0.625rem !important; + color: var(--color-token-text-primary) !important; + backdrop-filter: blur(16px) saturate(130%) !important; + } + + [data-composer-overlay-floating-ui="true"] + [${MENU_ATTR}="true"] + [${SECTION_TITLE_ATTR}] { + min-width: 0 !important; + overflow: hidden !important; + text-overflow: ellipsis !important; + white-space: nowrap !important; + font-size: 0.75rem !important; + font-weight: 600 !important; + letter-spacing: 0 !important; + line-height: 1rem !important; + text-transform: none !important; + } + + [data-composer-overlay-floating-ui="true"] + [${MENU_ATTR}="true"] + [${SECTION_TITLE_ATTR}][data-changing="true"] { + animation: codexpp-slash-title-change 180ms ease !important; + } + + @keyframes codexpp-slash-title-change { + 0% { + opacity: 0; + transform: translateY(0.25rem); + } + 100% { + opacity: 1; + transform: translateY(0); + } + } + + [data-composer-overlay-floating-ui="true"] + [${MENU_ATTR}="true"] + .codexpp-slash-section-icons { + display: flex !important; + flex: none !important; + align-items: center !important; + gap: 0.125rem !important; + } + + [data-composer-overlay-floating-ui="true"] + [${MENU_ATTR}="true"] + [${SECTION_ICON_ATTR}] { + display: inline-flex !important; + position: relative !important; + width: 1.5rem !important; + height: 1.5rem !important; + flex: none !important; + align-items: center !important; + justify-content: center !important; + border: 0 !important; + border-radius: 999px !important; + background: transparent !important; + color: var(--color-token-text-secondary) !important; + font-weight: 800 !important; + opacity: 0.78 !important; + overflow: hidden !important; + transition: + color 140ms ease, + opacity 140ms ease, + transform 140ms ease !important; + } + + [data-composer-overlay-floating-ui="true"] + [${MENU_ATTR}="true"] + [${SECTION_ICON_ATTR}]::before { + content: "" !important; + position: absolute !important; + inset: 0 !important; + border-radius: inherit !important; + background: var(--codexpp-section-color, var(--color-token-text-primary)) !important; + box-shadow: 0 0 0 1px color-mix(in srgb, #fff 24%, transparent) inset !important; + opacity: 0 !important; + transform: scale(0.62) !important; + transition: + opacity 160ms ease, + transform 180ms cubic-bezier(0.2, 0.8, 0.2, 1) !important; + } + + [data-composer-overlay-floating-ui="true"] + [${MENU_ATTR}="true"] + [${SECTION_ICON_ATTR}][data-active="true"] { + color: #fff !important; + font-weight: 900 !important; + opacity: 1 !important; + } + + [data-composer-overlay-floating-ui="true"] + [${MENU_ATTR}="true"] + [${SECTION_ICON_ATTR}][data-active="true"]::before { + opacity: 1 !important; + transform: scale(1) !important; + } + + [data-composer-overlay-floating-ui="true"] + [${MENU_ATTR}="true"] + [${SECTION_ICON_ATTR}]:hover:not([data-active="true"]) { + background: color-mix(in srgb, currentColor 8%, transparent) !important; + opacity: 1 !important; + } + + [data-composer-overlay-floating-ui="true"] + [${MENU_ATTR}="true"] + [${SECTION_ICON_ATTR}][data-active="true"]:hover { + color: #fff !important; + } + + [data-composer-overlay-floating-ui="true"] + [${MENU_ATTR}="true"] + [${SECTION_ICON_ATTR}] + svg { + position: relative !important; + z-index: 1 !important; + width: 0.9375rem !important; + height: 0.9375rem !important; + } + + [data-composer-overlay-floating-ui="true"] + [${MENU_ATTR}="true"] + [${SECTION_ICON_ATTR}][data-active="true"] + svg path { + stroke-width: 1.8 !important; + } + + [data-composer-overlay-floating-ui="true"] + [${MENU_ATTR}="true"] + [data-list-navigation-item="true"][${FAVORITE_DUPLICATE_HIDDEN_ATTR}="true"] { + display: none !important; + } + + [data-composer-overlay-floating-ui="true"] + [${MENU_ATTR}="true"] + [data-list-navigation-item="true"] { + box-sizing: border-box !important; + position: relative !important; + width: 100% !important; + min-height: 1.75rem !important; + height: 1.75rem !important; + padding: 0 0.625rem !important; + color: var(--color-token-text-primary) !important; + opacity: 0.9 !important; + max-width: 100% !important; + min-width: 0 !important; + overflow-x: hidden !important; + transition: + background-color 120ms ease, + color 120ms ease, + opacity 120ms ease !important; + } + + [data-composer-overlay-floating-ui="true"] + [${MENU_ATTR}="true"] + [data-list-navigation-item="true"] + > div { + min-height: 1.25rem !important; + gap: 0.625rem !important; + min-width: 0 !important; + max-width: 100% !important; + overflow-x: hidden !important; + } + + [data-composer-overlay-floating-ui="true"] + [${MENU_ATTR}="true"] + [data-list-navigation-item="true"] + svg { + width: 1rem !important; + height: 1rem !important; + color: var(--color-token-description-foreground) !important; + } + + [data-composer-overlay-floating-ui="true"] + [${MENU_ATTR}="true"] + [data-list-navigation-item="true"]:hover, + [data-composer-overlay-floating-ui="true"] + [${MENU_ATTR}="true"] + [data-list-navigation-item="true"]:focus-visible { + background-color: color-mix(in srgb, currentColor 7%, transparent) !important; + opacity: 1 !important; + } + + [data-composer-overlay-floating-ui="true"] + [${MENU_ATTR}="true"] + [data-list-navigation-item="true"][aria-selected="true"] { + background-color: color-mix( + in srgb, + var(--color-token-text-primary, currentColor) 11%, + transparent + ) !important; + color: var(--color-token-text-primary) !important; + opacity: 1 !important; + } + + [data-composer-overlay-floating-ui="true"] + [${MENU_ATTR}="true"] + [data-list-navigation-item="true"][${FAVORITE_CLONE_ATTR}="true"]:hover, + [data-composer-overlay-floating-ui="true"] + [${MENU_ATTR}="true"] + [data-list-navigation-item="true"][${FAVORITE_CLONE_ATTR}="true"][aria-selected="true"] { + background-color: color-mix( + in srgb, + var(--color-token-text-primary, currentColor) 10%, + transparent + ) !important; + opacity: 1 !important; + } + + [data-composer-overlay-floating-ui="true"] + [${MENU_ATTR}="true"] + [data-list-navigation-item="true"][aria-selected="true"] + svg { + color: var(--color-token-text-primary) !important; + } + + [data-composer-overlay-floating-ui="true"] + [${MENU_ATTR}="true"][${INPUT_MODE_ATTR}="keyboard"] + [data-list-navigation-item="true"]:hover:not([aria-selected="true"]) { + background-color: transparent !important; + opacity: 0.9 !important; + } + + [data-composer-overlay-floating-ui="true"] + [${MENU_ATTR}="true"][${PROGRAM_SCROLL_ATTR}="true"] + .vertical-scroll-fade-mask + [data-list-navigation-item="true"], + [data-composer-overlay-floating-ui="true"] + [${MENU_ATTR}="true"][${HOVER_SUPPRESS_ATTR}="true"] + .vertical-scroll-fade-mask + [data-list-navigation-item="true"] { + pointer-events: none !important; + } + + [data-composer-overlay-floating-ui="true"] + [${MENU_ATTR}="true"][${HOVER_SUPPRESS_ATTR}="true"] + [data-list-navigation-item="true"]:hover:not([aria-selected="true"]), + [data-composer-overlay-floating-ui="true"] + [${MENU_ATTR}="true"][${HOVER_SUPPRESS_ATTR}="true"] + [data-list-navigation-item="true"]:focus-visible:not([aria-selected="true"]) { + background-color: transparent !important; + opacity: 0.9 !important; + } + + [data-composer-overlay-floating-ui="true"] + [${MENU_ATTR}="true"] + [data-list-navigation-item="true"] + div, + [data-composer-overlay-floating-ui="true"] + [${MENU_ATTR}="true"] + [data-list-navigation-item="true"] + span { + min-width: 0 !important; + } + + [data-composer-overlay-floating-ui="true"] + [${MENU_ATTR}="true"] + [data-list-navigation-item="true"] + .text-token-description-foreground, + [data-composer-overlay-floating-ui="true"] + [${MENU_ATTR}="true"] + [data-list-navigation-item="true"] + span[class*="text-token-description-foreground"] { + color: var(--color-token-text-secondary) !important; + } + + [data-composer-overlay-floating-ui="true"] + [${MENU_ATTR}="true"] + [data-list-navigation-item="true"] + span.ml-auto { + max-width: 40% !important; + overflow: hidden !important; + text-overflow: ellipsis !important; + white-space: nowrap !important; + border: 1px solid color-mix(in srgb, currentColor 12%, transparent) !important; + border-radius: 999px !important; + padding: 0 0.375rem !important; + font-size: 0.6875rem !important; + line-height: 1rem !important; + color: var(--color-token-text-secondary) !important; + background-color: color-mix(in srgb, currentColor 5%, transparent) !important; + } + + [data-composer-overlay-floating-ui="true"] + [${MENU_ATTR}="true"] + .${FAVORITE_BUTTON_CLASS} { + display: inline-flex !important; + width: 1.25rem !important; + height: 1.25rem !important; + flex: 0 0 1.25rem !important; + align-items: center !important; + justify-content: center !important; + border: 0 !important; + border-radius: 999px !important; + background: transparent !important; + color: var(--color-token-text-secondary) !important; + cursor: pointer !important; + opacity: 0 !important; + transform: scale(0.92) !important; + transition: + color 120ms ease, + opacity 120ms ease, + transform 120ms ease !important; + } + + [data-composer-overlay-floating-ui="true"] + [${MENU_ATTR}="true"] + [data-list-navigation-item="true"]:hover + .${FAVORITE_BUTTON_CLASS}, + [data-composer-overlay-floating-ui="true"] + [${MENU_ATTR}="true"] + [data-list-navigation-item="true"][aria-selected="true"] + .${FAVORITE_BUTTON_CLASS}, + [data-composer-overlay-floating-ui="true"] + [${MENU_ATTR}="true"] + .${FAVORITE_BUTTON_CLASS}[data-favorite="true"] { + opacity: 1 !important; + transform: scale(1) !important; + } + + [data-composer-overlay-floating-ui="true"] + [${MENU_ATTR}="true"][${HOVER_SUPPRESS_ATTR}="true"] + [data-list-navigation-item="true"]:hover + .${FAVORITE_BUTTON_CLASS}:not([data-favorite="true"]) { + opacity: 0 !important; + transform: scale(0.92) !important; + } + + [data-composer-overlay-floating-ui="true"] + [${MENU_ATTR}="true"] + .${FAVORITE_BUTTON_CLASS}[data-favorite="true"] { + color: #f4c95d !important; + } + + [data-composer-overlay-floating-ui="true"] + [${MENU_ATTR}="true"] + .${FAVORITE_BUTTON_CLASS}:hover { + color: #ffd76a !important; + } + + [data-composer-overlay-floating-ui="true"] + [${MENU_ATTR}="true"] + .${FAVORITE_BUTTON_CLASS} + svg { + width: 0.875rem !important; + height: 0.875rem !important; + color: currentColor !important; + stroke-width: 2 !important; + } + + [data-composer-overlay-floating-ui="true"] + [${MENU_ATTR}="true"] + .sticky.top-0 { + position: static !important; + height: 0 !important; + margin: 0 !important; + overflow: hidden !important; + padding: 0 !important; + border: 0 !important; + opacity: 0 !important; + pointer-events: none !important; + } + + [data-composer-overlay-floating-ui="true"] + [${MENU_ATTR}="true"] + [data-list-navigation-item="true"][${SECTION_ATTR}="skills"], + [data-composer-overlay-floating-ui="true"] + [${MENU_ATTR}="true"] + [data-list-navigation-item="true"][${FAVORITE_SOURCE_SECTION_ATTR}="skills"] { + height: auto !important; + min-height: 2.875rem !important; + padding-top: 0.3125rem !important; + padding-bottom: 0.3125rem !important; + } + + [data-composer-overlay-floating-ui="true"] + [${MENU_ATTR}="true"] + [data-list-navigation-item="true"][${SECTION_ATTR}="skills"] + > div, + [data-composer-overlay-floating-ui="true"] + [${MENU_ATTR}="true"] + [data-list-navigation-item="true"][${FAVORITE_SOURCE_SECTION_ATTR}="skills"] + > div { + align-items: center !important; + } + + [data-composer-overlay-floating-ui="true"] + [${MENU_ATTR}="true"] + .${SKILL_COPY_CLASS} { + display: flex !important; + min-width: 0 !important; + flex: 1 1 auto !important; + flex-direction: column !important; + gap: 0.0625rem !important; + overflow: hidden !important; + } + + [data-composer-overlay-floating-ui="true"] + [${MENU_ATTR}="true"] + .${SKILL_COPY_CLASS} + > div { + max-width: 100% !important; + flex: none !important; + overflow: hidden !important; + text-overflow: ellipsis !important; + white-space: nowrap !important; + line-height: 1rem !important; + } + + [data-composer-overlay-floating-ui="true"] + [${MENU_ATTR}="true"] + .${SKILL_COPY_CLASS} + > span { + max-width: 100% !important; + flex: none !important; + overflow: hidden !important; + text-overflow: ellipsis !important; + white-space: nowrap !important; + color: var(--color-token-text-secondary) !important; + font-size: 0.75rem !important; + line-height: 1rem !important; + } + `; + document.head.appendChild(style); + + const scrollHandlers = new Map(); + const pointerHandlers = new Map(); + const hoverGuardHandlers = new Map(); + const wheelHandlers = new Map(); + const hoverScrollStates = new WeakMap(); + const hoverSuppressStates = new WeakMap(); + const scrollAnimations = new Map(); + const titleTimers = new Map(); + const HOVER_GUARD_EVENTS = [ + "pointermove", + "pointerover", + "pointerenter", + "mousemove", + "mouseover", + "mouseenter", + ]; + const NAV_NOISE_SELECTOR = [ + '[data-codexpp="nav-group"]', + '[data-codexpp="pages-group"]', + '[data-codexpp="nav-config"]', + '[data-codexpp="nav-tweaks"]', + '[data-codexpp^="nav-tweak"]', + '[data-codexpp^="nav-page-"]', + ].join(", "); + const OBSERVER_OPTIONS = { + characterData: true, + childList: true, + subtree: true, + }; + let scanFrame = 0; + let scanTimer = 0; + let homePruneFrame = 0; + let hardPruneTimer = 0; + let disposed = false; + let observer = null; + let documentHoverGuard = null; + let slashRowScrollAllowedUntil = 0; + const nativeScrollIntoView = Element.prototype.scrollIntoView; + + const normText = (node) => + String(node?.textContent || "").replace(/\s+/g, " ").trim(); + + const isOverlayNoise = (node) => { + if (node instanceof HTMLElement) { + const codexpp = node.getAttribute("data-codexpp") || ""; + if ( + codexpp === "nav-group" || + codexpp === "pages-group" || + codexpp.startsWith("nav-page-") || + codexpp === "nav-config" || + codexpp === "nav-tweaks" + ) { + return true; + } + } + const text = normText(node); + return ( + /^Codex\+\+\b/.test(text) || + /^Tweaks\b/.test(text) || + /\bTweak Store\b/.test(text) || + /Better TerminalKeyboard ShortcutsDatabase Explorer/.test(text) + ); + }; + + const stopHoverSelectionEvent = (menu, event) => { + if (!(menu instanceof HTMLElement)) return; + trackPointerPosition(menu, event); + if (shouldBlockSuppressedHover(menu, event)) { + event.stopPropagation(); + event.stopImmediatePropagation?.(); + return; + } + freezeHoverScroll(menu); + event.stopPropagation(); + event.stopImmediatePropagation?.(); + }; + + const installDocumentHoverGuard = () => { + if (documentHoverGuard) return; + documentHoverGuard = (event) => { + const target = event.target; + if (!(target instanceof Element)) return; + stopHoverSelectionEvent(target.closest(`[${MENU_ATTR}="true"]`), event); + }; + HOVER_GUARD_EVENTS.forEach((type) => + window.addEventListener(type, documentHoverGuard, true), + ); + HOVER_GUARD_EVENTS.forEach((type) => + document.addEventListener(type, documentHoverGuard, true), + ); + }; + + const allowSlashRowScrollIntoView = (duration = 220) => { + slashRowScrollAllowedUntil = Math.max( + slashRowScrollAllowedUntil, + performance.now() + duration, + ); + }; + + const isSlashMenuRow = (node) => + node instanceof HTMLElement && + node.matches('[data-list-navigation-item="true"]') && + !!node.closest(`[${MENU_ATTR}="true"]`); + + const hoverSuppressStateFor = (menu) => { + let state = hoverSuppressStates.get(menu); + if (!state) { + state = { + active: false, + pointerX: null, + pointerY: null, + releaseAfter: 0, + }; + hoverSuppressStates.set(menu, state); + } + return state; + }; + + const eventPointerPosition = (event) => { + if (!event || typeof event.clientX !== "number" || typeof event.clientY !== "number") { + return null; + } + return { x: event.clientX, y: event.clientY }; + }; + + const trackPointerPosition = (menu, event) => { + const point = eventPointerPosition(event); + if (!point) return; + const state = hoverSuppressStateFor(menu); + if (!state.active) { + state.pointerX = point.x; + state.pointerY = point.y; + } + }; + + const clearHoverSelection = (menu) => { + const state = hoverSuppressStateFor(menu); + const pointerTarget = + typeof state.pointerX === "number" && typeof state.pointerY === "number" + ? document.elementFromPoint(state.pointerX, state.pointerY) + : null; + const rows = new Set( + Array.from(menu.querySelectorAll('[data-list-navigation-item="true"]:hover')), + ); + const pointerRow = pointerTarget?.closest?.('[data-list-navigation-item="true"]'); + if (pointerRow instanceof HTMLElement && menu.contains(pointerRow)) { + rows.add(pointerRow); + } + menu + .querySelectorAll('[data-list-navigation-item="true"][aria-selected="true"]') + .forEach((row) => { + if (!(row instanceof HTMLElement)) return; + rows.add(row); + const rect = row.getBoundingClientRect(); + if ( + typeof state.pointerX === "number" && + typeof state.pointerY === "number" && + state.pointerX >= rect.left && + state.pointerX <= rect.right && + state.pointerY >= rect.top && + state.pointerY <= rect.bottom + ) { + rows.add(row); + } + }); + rows.forEach((row) => { + if (!(row instanceof HTMLElement)) return; + row.setAttribute("aria-selected", "false"); + row.blur(); + }); + }; + + const suppressHoverUntilPointerMoves = (menu, duration = 900) => { + if (!(menu instanceof HTMLElement)) return; + const state = hoverSuppressStateFor(menu); + state.active = true; + state.releaseAfter = performance.now() + duration; + menu.setAttribute(HOVER_SUPPRESS_ATTR, "true"); + clearHoverSelection(menu); + [0, 80, 240].forEach((delay) => { + window.setTimeout(() => { + if (menu.hasAttribute(HOVER_SUPPRESS_ATTR)) clearHoverSelection(menu); + }, delay); + }); + }; + + const clearHoverSuppression = (menu) => { + if (!(menu instanceof HTMLElement)) return; + const state = hoverSuppressStateFor(menu); + state.active = false; + state.releaseAfter = 0; + menu.removeAttribute(HOVER_SUPPRESS_ATTR); + if (!menu.hasAttribute(PROGRAM_SCROLL_ATTR)) { + menu.setAttribute(INPUT_MODE_ATTR, "pointer"); + } + }; + + const shouldBlockSuppressedHover = (menu, event) => { + const state = hoverSuppressStateFor(menu); + if (!state.active) return false; + const point = eventPointerPosition(event); + const now = performance.now(); + const scroller = menu.querySelector(".vertical-scroll-fade-mask"); + const programmatic = + menu.hasAttribute(PROGRAM_SCROLL_ATTR) || + (scroller instanceof HTMLElement && + typeof hoverScrollStateFor(scroller).programmaticTarget === "number" && + now < hoverScrollStateFor(scroller).programmaticUntil); + + if (!point) return true; + if (state.pointerX === null || state.pointerY === null) { + if (!programmatic && now >= state.releaseAfter - 450) { + clearHoverSuppression(menu); + state.pointerX = point.x; + state.pointerY = point.y; + return false; + } + state.pointerX = point.x; + state.pointerY = point.y; + return true; + } + const moved = Math.hypot(point.x - state.pointerX, point.y - state.pointerY); + if (moved >= 5 && !programmatic && now >= state.releaseAfter - 450) { + clearHoverSuppression(menu); + state.pointerX = point.x; + state.pointerY = point.y; + return false; + } + return true; + }; + + const hoverScrollStateFor = (scroller) => { + let state = hoverScrollStates.get(scroller); + if (!state) { + state = { + freezeTop: scroller.scrollTop, + freezeUntil: 0, + lastTop: scroller.scrollTop, + programmaticTarget: null, + programmaticUntil: 0, + restoreFrame: 0, + }; + hoverScrollStates.set(scroller, state); + } + return state; + }; + + const enforceHoverScrollFreeze = (scroller) => { + const state = hoverScrollStateFor(scroller); + const now = performance.now(); + const currentTop = scroller.scrollTop; + const programmaticActive = + typeof state.programmaticTarget === "number" && now < state.programmaticUntil; + const programmaticDown = programmaticActive && state.programmaticTarget >= state.lastTop; + + if (now <= state.freezeUntil && (!programmaticActive || programmaticDown)) { + if (currentTop < state.freezeTop - 1) { + scroller.scrollTop = state.freezeTop; + state.lastTop = state.freezeTop; + return true; + } + state.freezeTop = Math.max(state.freezeTop, currentTop); + } + + state.lastTop = scroller.scrollTop; + return false; + }; + + const requestHoverScrollFreezeFrame = (scroller) => { + const state = hoverScrollStateFor(scroller); + if (state.restoreFrame) return; + const tick = () => { + state.restoreFrame = 0; + enforceHoverScrollFreeze(scroller); + if (performance.now() <= state.freezeUntil) { + state.restoreFrame = requestAnimationFrame(tick); + } + }; + state.restoreFrame = requestAnimationFrame(tick); + }; + + const queueHoverScrollFreezeChecks = (scroller) => { + [0, 16, 80, 180, 360].forEach((delay) => { + window.setTimeout(() => enforceHoverScrollFreeze(scroller), delay); + }); + }; + + const freezeHoverScroll = (menu) => { + const scroller = menu.querySelector(".vertical-scroll-fade-mask"); + if (!(scroller instanceof HTMLElement)) return; + const state = hoverScrollStateFor(scroller); + const now = performance.now(); + if ( + typeof state.programmaticTarget === "number" && + now < state.programmaticUntil && + state.programmaticTarget < scroller.scrollTop - 1 + ) { + state.freezeUntil = 0; + state.freezeTop = scroller.scrollTop; + state.lastTop = scroller.scrollTop; + return; + } + const stableTop = Math.max(state.lastTop, scroller.scrollTop); + state.freezeTop = Math.max(state.freezeTop, stableTop); + state.freezeUntil = Math.max(state.freezeUntil, now + 450); + requestHoverScrollFreezeFrame(scroller); + queueHoverScrollFreezeChecks(scroller); + }; + + const clearHoverScrollFreeze = (scroller) => { + const state = hoverScrollStateFor(scroller); + state.freezeUntil = 0; + state.freezeTop = scroller.scrollTop; + state.lastTop = scroller.scrollTop; + }; + + const allowProgrammaticScroll = (scroller, targetTop, duration = 900) => { + const state = hoverScrollStateFor(scroller); + if (targetTop < scroller.scrollTop - 1) { + state.freezeUntil = 0; + state.freezeTop = targetTop; + } + state.programmaticTarget = targetTop; + state.programmaticUntil = performance.now() + duration; + }; + + const patchedScrollIntoView = function (...args) { + if (isSlashMenuRow(this) && performance.now() > slashRowScrollAllowedUntil) { + return; + } + return nativeScrollIntoView.apply(this, args); + }; + + const looksLikeSlashPanel = (node) => { + if (!(node instanceof HTMLElement)) return false; + if (node.hasAttribute(MENU_ATTR)) return true; + if (/^No commands$/i.test(normText(node))) return true; + const scroller = node.querySelector(".vertical-scroll-fade-mask"); + return ( + scroller instanceof HTMLElement && + node.querySelector('[data-list-navigation-item="true"]') + ); + }; + + const isOverlaySlashActive = (overlay) => + isSlashQueryActive() || + Array.from(overlay.children).some((child) => looksLikeSlashPanel(child)); + + const markOverlayNoise = (overlay) => { + const active = isOverlaySlashActive(overlay); + Array.from(overlay.children).forEach((child) => { + if (!(child instanceof HTMLElement)) return; + if (active && !looksLikeSlashPanel(child) && isOverlayNoise(child)) { + child.setAttribute(OVERLAY_NOISE_ATTR, "true"); + } else { + child.removeAttribute(OVERLAY_NOISE_ATTR); + } + }); + }; + + const pruneOverlayNoise = (overlay) => { + if (!isOverlaySlashActive(overlay)) return; + Array.from(overlay.children).forEach((child) => { + if (!(child instanceof HTMLElement)) return; + if (looksLikeSlashPanel(child) || !isOverlayNoise(child)) return; + child.remove(); + }); + }; + + const pruneMenuNoise = (menu) => { + menu.querySelectorAll(NAV_NOISE_SELECTOR).forEach((node) => node.remove()); + }; + + const pruneHomeContentNoise = () => { + document + .querySelectorAll( + [ + '[class*="[container-name:home-main-content]"] [data-codexpp="nav-group"]', + '[class*="[container-name:home-main-content]"] [data-codexpp="pages-group"]', + '[class*="[container-name:home-main-content]"] [data-codexpp="nav-config"]', + '[class*="[container-name:home-main-content]"] [data-codexpp="nav-tweaks"]', + '[class*="[container-name:home-main-content]"] [data-codexpp^="nav-tweak"]', + '[class*="[container-name:home-main-content]"] [data-codexpp^="nav-page-"]', + ].join(", "), + ) + .forEach((node) => node.remove()); + }; + + const shouldPruneHomeContentNoise = () => + isSlashQueryActive() || + !!document.querySelector( + '[data-composer-overlay-floating-ui="true"], [data-codexpp-slash-menu="true"]', + ); + + const scheduleHomeContentPrune = () => { + if (homePruneFrame) return; + homePruneFrame = requestAnimationFrame(() => { + homePruneFrame = 0; + if (shouldPruneHomeContentNoise()) pruneHomeContentNoise(); + }); + }; + + const hardPruneNoise = () => { + try { + pruneHomeContentNoise(); + document + .querySelectorAll(`[${MENU_ATTR}="true"]`) + .forEach((menu) => { + if (menu instanceof HTMLElement) pruneMenuNoise(menu); + }); + } catch { + // Ignore transient DOM shapes while Codex is replacing the slash panel. + } + }; + + const scheduleHardPruneNoise = () => { + if (hardPruneTimer || disposed) return; + hardPruneTimer = window.setTimeout(() => { + hardPruneTimer = 0; + if (disposed || !shouldPruneHomeContentNoise()) return; + observer?.disconnect(); + hardPruneNoise(); + requestAnimationFrame(() => { + if (!disposed) { + observer?.observe(document.body, OBSERVER_OPTIONS); + scheduleScan(); + } + }); + }, 60); + }; + + const sectionKey = (title) => + String(title || "General") + .replace(/\s+/g, "-") + .replace(/[^a-z0-9-]/gi, "") + .toLowerCase() || "general"; + + const sectionColor = (key, index) => { + const known = { + favorites: "#f4c95d", + general: "#8ab4ff", + skills: "#7dd3a8", + mcp: "#f0b86a", + tools: "#c4a7ff", + }; + return known[key] || ["#8ab4ff", "#7dd3a8", "#f0b86a", "#c4a7ff"][index % 4]; + }; + + const sectionIconSvg = (key) => { + if (key === "favorites") { + return ( + '" + ); + } + if (key === "skills") { + return ( + '" + ); + } + return ( + '" + ); + }; + + const starIconSvg = (filled) => + filled + ? '' + : ''; + + const readFavorites = () => { + try { + const raw = window.localStorage?.getItem(FAVORITES_STORAGE_KEY); + const values = JSON.parse(raw || "[]"); + return new Set(Array.isArray(values) ? values.filter(Boolean) : []); + } catch { + return new Set(); + } + }; + + const writeFavorites = (favorites) => { + try { + window.localStorage?.setItem( + FAVORITES_STORAGE_KEY, + JSON.stringify(Array.from(favorites).sort()), + ); + } catch { + // Ignore storage failures; the row controls still update for this render. + } + }; + + const rowFavoriteKey = (button, fallbackSectionKey) => { + const section = fallbackSectionKey || button.getAttribute(SECTION_ATTR) || "general"; + const text = normText(button) + .replace(/\s+/g, " ") + .trim() + .toLowerCase(); + return text ? `${section}:${text}` : ""; + }; + + const isSlashSearchActive = () => + Array.from(document.querySelectorAll('.ProseMirror[contenteditable="true"]')).some( + (editor) => { + if (!(editor instanceof HTMLElement)) return false; + const text = normText(editor); + return text.startsWith("/") && text.length > 1; + }, + ); + + const refreshFavoriteViews = () => { + document + .querySelectorAll(`[${MENU_ATTR}="true"] .vertical-scroll-fade-mask`) + .forEach((scroller) => { + if (!(scroller instanceof HTMLElement)) return; + syncFavoriteControls(scroller); + syncFavoritesSection(scroller); + const sections = groupSections(scroller); + const topbar = scroller.previousElementSibling; + if (topbar instanceof HTMLElement) renderTopbarIcons(topbar, sections); + updateTopbar(scroller, sections); + }); + }; + + const toggleFavorite = (key) => { + if (!key) return; + const favorites = readFavorites(); + if (favorites.has(key)) favorites.delete(key); + else favorites.add(key); + writeFavorites(favorites); + refreshFavoriteViews(); + scheduleScan(); + }; + + const stripNativeCommandState = (row) => { + if (!(row instanceof HTMLElement)) return; + const stripNode = (node) => { + if (!(node instanceof HTMLElement)) return; + node.removeAttribute(FAVORITE_DUPLICATE_HIDDEN_ATTR); + for (const attr of Array.from(node.attributes)) { + if ( + attr.name === "cmdk-item" || + attr.name === "data-value" || + (attr.name.startsWith("data-codexpp-") && + !attr.name.startsWith("data-codexpp-slash-")) + ) { + node.removeAttribute(attr.name); + } + } + }; + stripNode(row); + row.querySelectorAll("*").forEach(stripNode); + }; + + const ensureFavoriteControl = (button, key, favorites = readFavorites()) => { + if (!key) return; + button.setAttribute(FAVORITE_KEY_ATTR, key); + const inner = button.firstElementChild instanceof HTMLElement ? button.firstElementChild : button; + let control = button.querySelector(`:scope .${FAVORITE_BUTTON_CLASS}`); + if (!(control instanceof HTMLElement)) { + control = document.createElement("span"); + control.setAttribute("role", "button"); + control.setAttribute("tabindex", "-1"); + control.className = FAVORITE_BUTTON_CLASS; + control.addEventListener("pointerdown", (event) => { + event.preventDefault(); + event.stopPropagation(); + }); + control.addEventListener("mousedown", (event) => { + event.preventDefault(); + event.stopPropagation(); + }); + control.addEventListener("pointerup", (event) => { + event.preventDefault(); + event.stopPropagation(); + toggleFavorite(button.getAttribute(FAVORITE_KEY_ATTR) || key); + }); + control.addEventListener("click", (event) => { + event.preventDefault(); + event.stopPropagation(); + }); + const shortcut = inner.querySelector("span.ml-auto"); + if (shortcut instanceof HTMLElement) inner.insertBefore(control, shortcut); + else inner.appendChild(control); + } + const active = favorites.has(key); + control.dataset.favorite = active ? "true" : "false"; + control.setAttribute("aria-label", active ? "Remove from favorites" : "Add to favorites"); + control.innerHTML = starIconSvg(active); + }; + + const unwrapSkillRow = (button) => { + const copy = button.querySelector(`.${SKILL_COPY_CLASS}`); + if (!copy || !copy.parentElement) return; + while (copy.firstChild) copy.parentElement.insertBefore(copy.firstChild, copy); + copy.remove(); + }; + + const wrapSkillRow = (button) => { + const inner = button.firstElementChild; + if (!(inner instanceof HTMLElement)) return; + if (inner.querySelector(`.${SKILL_COPY_CLASS}`)) return; + const children = Array.from(inner.children); + const title = children.find( + (node) => + node instanceof HTMLElement && + node.tagName === "DIV" && + normText(node).length > 0, + ); + const description = children.find( + (node) => + node instanceof HTMLElement && + node.tagName === "SPAN" && + String(node.className).includes("text-token-description-foreground"), + ); + if (!title || !description) return; + const copy = document.createElement("div"); + copy.className = SKILL_COPY_CLASS; + inner.insertBefore(copy, title); + copy.appendChild(title); + copy.appendChild(description); + }; + + const sourceCommandRows = (scroller) => + Array.from(scroller.querySelectorAll('[data-list-navigation-item="true"]')).filter( + (row) => + row instanceof HTMLElement && + !row.closest(`[${FAVORITES_GROUP_ATTR}="true"]`) && + !row.hasAttribute(FAVORITE_CLONE_ATTR), + ); + + const syncSectionVisibility = (scroller) => { + Array.from(scroller.children).forEach((group) => { + if (!(group instanceof HTMLElement) || group.hasAttribute(TOPBAR_ATTR)) return; + const rows = Array.from( + group.querySelectorAll('[data-list-navigation-item="true"]'), + ).filter((row) => row instanceof HTMLElement); + const hasVisibleRows = rows.some( + (row) => !row.hasAttribute(FAVORITE_DUPLICATE_HIDDEN_ATTR), + ); + group.setAttribute(SECTION_EMPTY_ATTR, hasVisibleRows ? "false" : "true"); + }); + }; + + const syncFavoriteSourceVisibility = (scroller, favoriteKeys = new Set()) => { + const hideDuplicates = isSlashSearchActive() && favoriteKeys.size > 0; + let hiddenSelectedKey = ""; + sourceCommandRows(scroller).forEach((row) => { + const key = row.getAttribute(FAVORITE_KEY_ATTR) || rowFavoriteKey(row); + if (hideDuplicates && key && favoriteKeys.has(key)) { + if (row.getAttribute("aria-selected") === "true") hiddenSelectedKey = key; + row.setAttribute(FAVORITE_DUPLICATE_HIDDEN_ATTR, "true"); + } else { + row.removeAttribute(FAVORITE_DUPLICATE_HIDDEN_ATTR); + } + }); + syncSectionVisibility(scroller); + if (!hiddenSelectedKey) return; + const favorite = favoriteRows(scroller).find( + (row) => row.getAttribute(FAVORITE_KEY_ATTR) === hiddenSelectedKey, + ); + if (favorite instanceof HTMLElement) selectNavigationRow(scroller, favorite); + }; + + const syncFavoriteControls = (scroller) => { + const favorites = readFavorites(); + sourceCommandRows(scroller).forEach((row) => { + const key = row.getAttribute(FAVORITE_KEY_ATTR) || rowFavoriteKey(row); + ensureFavoriteControl(row, key, favorites); + }); + scroller + .querySelectorAll(`[${FAVORITE_CLONE_ATTR}="true"]`) + .forEach((row) => { + if (!(row instanceof HTMLElement)) return; + stripNativeCommandState(row); + const key = row.getAttribute(FAVORITE_KEY_ATTR) || rowFavoriteKey(row, "favorites"); + ensureFavoriteControl(row, key, favorites); + }); + }; + + const removeFavoriteSection = (scroller) => { + scroller + .querySelectorAll(`:scope > [${FAVORITES_GROUP_ATTR}="true"]`) + .forEach((group) => group.remove()); + syncFavoriteSourceVisibility(scroller); + syncSectionVisibility(scroller); + delete scroller.dataset.codexppSlashFavoriteSelectionReady; + delete scroller.dataset.codexppSlashFavoriteSelectionTouched; + }; + + const syncFavoritesSection = (scroller) => { + const favorites = readFavorites(); + const rowsByKey = new Map(); + sourceCommandRows(scroller).forEach((row) => { + const key = row.getAttribute(FAVORITE_KEY_ATTR) || rowFavoriteKey(row); + if (key && favorites.has(key) && !rowsByKey.has(key)) rowsByKey.set(key, row); + }); + const entries = Array.from(rowsByKey.entries()); + if (entries.length === 0) { + removeFavoriteSection(scroller); + return; + } + const entryKeys = new Set(entries.map(([key]) => key)); + syncFavoriteSourceVisibility(scroller, entryKeys); + + let group = scroller.querySelector(`:scope > [${FAVORITES_GROUP_ATTR}="true"]`); + if (!(group instanceof HTMLElement)) { + group = document.createElement("div"); + group.setAttribute(FAVORITES_GROUP_ATTR, "true"); + scroller.insertBefore(group, scroller.firstElementChild); + } else if (group !== scroller.firstElementChild) { + scroller.insertBefore(group, scroller.firstElementChild); + } + + const signature = entries.map(([key]) => key).join("|"); + if (group.dataset.signature === signature) return; + group.dataset.signature = signature; + delete scroller.dataset.codexppSlashFavoriteSelectionReady; + delete scroller.dataset.codexppSlashFavoriteSelectionTouched; + group.replaceChildren(); + + const header = document.createElement("div"); + header.className = "sticky top-0"; + header.textContent = "Favorites"; + group.appendChild(header); + + entries.forEach(([key, sourceRow]) => { + const clone = sourceRow.cloneNode(true); + if (!(clone instanceof HTMLElement)) return; + clone.setAttribute(FAVORITE_CLONE_ATTR, "true"); + clone.setAttribute(FAVORITE_KEY_ATTR, key); + clone.setAttribute( + FAVORITE_SOURCE_SECTION_ATTR, + sourceRow.getAttribute(SECTION_ATTR) || "", + ); + stripNativeCommandState(clone); + clone.removeAttribute("aria-selected"); + clone.querySelectorAll(`.${FAVORITE_BUTTON_CLASS}`).forEach((node) => node.remove()); + ["pointermove", "mousemove", "mouseover"].forEach((type) => { + clone.addEventListener(type, (event) => { + event.stopPropagation(); + }); + }); + clone.addEventListener("click", (event) => { + if (event.target instanceof HTMLElement && event.target.closest(`.${FAVORITE_BUTTON_CLASS}`)) { + return; + } + event.preventDefault(); + event.stopPropagation(); + sourceRow.click(); + }); + clone.addEventListener("keydown", (event) => { + if (event.key !== "Enter" && event.key !== " ") return; + event.preventDefault(); + event.stopPropagation(); + sourceRow.click(); + }); + group.appendChild(clone); + }); + }; + + const navigationRows = (scroller) => + Array.from(scroller.querySelectorAll('[data-list-navigation-item="true"]')).filter( + (row) => row instanceof HTMLElement && row.offsetParent !== null, + ); + + const favoriteRows = (scroller) => + Array.from( + scroller.querySelectorAll( + `[${FAVORITES_GROUP_ATTR}="true"] [data-list-navigation-item="true"]`, + ), + ).filter((row) => row instanceof HTMLElement && row.offsetParent !== null); + + const selectedNavigationRow = (scroller) => + navigationRows(scroller).find( + (row) => row.getAttribute("aria-selected") === "true", + ); + + const reconcileFavoriteSelection = (scroller) => { + const rows = navigationRows(scroller); + const selected = rows.filter((row) => row.getAttribute("aria-selected") === "true"); + if (selected.length <= 1) return; + const favoriteSelected = + selected.find((row) => row.hasAttribute(FAVORITE_CLONE_ATTR)) || selected[0]; + rows.forEach((row) => + row.setAttribute("aria-selected", row === favoriteSelected ? "true" : "false"), + ); + }; + + const selectNavigationRow = (scroller, row, options = {}) => { + if (!(row instanceof HTMLElement)) return; + const menu = scroller.closest(`[${MENU_ATTR}="true"]`); + if (options.inputMode !== false) { + menu?.setAttribute(INPUT_MODE_ATTR, options.inputMode || "keyboard"); + } + navigationRows(scroller).forEach((item) => + item.setAttribute("aria-selected", item === row ? "true" : "false"), + ); + allowSlashRowScrollIntoView(); + row.scrollIntoView({ block: "nearest" }); + updateTopbar(scroller); + }; + + const ensureInitialFavoriteSelection = (scroller) => { + if (scroller.dataset.codexppSlashFavoriteSelectionReady === "true") return; + if (scroller.closest(`[${MENU_ATTR}="true"]`)?.hasAttribute(HOVER_SUPPRESS_ATTR)) return; + const firstFavorite = favoriteRows(scroller)[0]; + if (!(firstFavorite instanceof HTMLElement)) return; + selectNavigationRow(scroller, firstFavorite, { inputMode: false }); + scroller.dataset.codexppSlashFavoriteSelectionReady = "true"; + const keepFavoriteSelected = () => { + if (!scroller.isConnected) return; + if (scroller.closest(`[${MENU_ATTR}="true"]`)?.hasAttribute(HOVER_SUPPRESS_ATTR)) return; + if (scroller.dataset.codexppSlashFavoriteSelectionTouched === "true") return; + const nextFirstFavorite = favoriteRows(scroller)[0]; + if (!(nextFirstFavorite instanceof HTMLElement)) return; + if (selectedNavigationRow(scroller) !== nextFirstFavorite) { + selectNavigationRow(scroller, nextFirstFavorite); + } + }; + requestAnimationFrame(keepFavoriteSelected); + window.setTimeout(keepFavoriteSelected, 80); + }; + + const handleFavoriteNavigationKey = (event, scroller) => { + const rows = navigationRows(scroller); + const favs = favoriteRows(scroller); + if (rows.length === 0 || favs.length === 0) return false; + + if (event.key === "Enter") { + const selected = selectedNavigationRow(scroller); + if (!(selected instanceof HTMLElement)) return false; + scroller.dataset.codexppSlashFavoriteSelectionTouched = "true"; + event.preventDefault(); + event.stopPropagation(); + event.stopImmediatePropagation?.(); + selected.click(); + return true; + } + + if (event.key !== "ArrowDown" && event.key !== "ArrowUp") return false; + scroller.dataset.codexppSlashFavoriteSelectionTouched = "true"; + event.preventDefault(); + event.stopPropagation(); + event.stopImmediatePropagation?.(); + + const selected = selectedNavigationRow(scroller); + const currentIndex = selected instanceof HTMLElement ? rows.indexOf(selected) : -1; + const fallbackIndex = event.key === "ArrowDown" ? 0 : rows.length - 1; + const nextIndex = + currentIndex < 0 + ? fallbackIndex + : event.key === "ArrowDown" + ? Math.min(rows.length - 1, currentIndex + 1) + : Math.max(0, currentIndex - 1); + selectNavigationRow(scroller, rows[nextIndex]); + return true; + }; + + const cleanupMenu = (menu) => { + const overlay = menu.closest('[data-composer-overlay-floating-ui="true"]'); + const scroller = menu.querySelector(".vertical-scroll-fade-mask"); + if (scroller instanceof HTMLElement) { + const scrollHandler = scrollHandlers.get(scroller); + if (scrollHandler) { + scroller.removeEventListener("scroll", scrollHandler); + scrollHandlers.delete(scroller); + } + const animation = scrollAnimations.get(scroller); + if (animation) { + cancelScrollAnimation(scroller); + } + const pointerHandler = pointerHandlers.get(scroller); + if (pointerHandler) { + scroller.removeEventListener("pointermove", pointerHandler); + scroller.removeEventListener("pointerdown", pointerHandler); + pointerHandlers.delete(scroller); + } + const wheelHandler = wheelHandlers.get(scroller); + if (wheelHandler) { + scroller.removeEventListener("wheel", wheelHandler); + wheelHandlers.delete(scroller); + } + const hoverGuardHandler = hoverGuardHandlers.get(scroller); + if (hoverGuardHandler) { + HOVER_GUARD_EVENTS.forEach((type) => + scroller.removeEventListener(type, hoverGuardHandler, true), + ); + hoverGuardHandlers.delete(scroller); + } + } + if (scroller instanceof HTMLElement) removeFavoriteSection(scroller); + menu.removeAttribute(MENU_ATTR); + menu.removeAttribute(INPUT_MODE_ATTR); + menu.removeAttribute(PROGRAM_SCROLL_ATTR); + menu.removeAttribute(HOVER_SUPPRESS_ATTR); + menu.querySelectorAll(`[${TOPBAR_ATTR}]`).forEach((node) => node.remove()); + menu.querySelectorAll(`.${FAVORITE_BUTTON_CLASS}`).forEach((node) => node.remove()); + menu.querySelectorAll(`.${SKILL_COPY_CLASS}`).forEach((copy) => { + if (!(copy instanceof HTMLElement) || !copy.parentElement) return; + while (copy.firstChild) copy.parentElement.insertBefore(copy.firstChild, copy); + copy.remove(); + }); + menu + .querySelectorAll( + `[${FAVORITE_KEY_ATTR}], [${FAVORITE_CLONE_ATTR}], [${FAVORITE_SOURCE_SECTION_ATTR}], [${FAVORITE_DUPLICATE_HIDDEN_ATTR}]`, + ) + .forEach((node) => { + node.removeAttribute(FAVORITE_KEY_ATTR); + node.removeAttribute(FAVORITE_CLONE_ATTR); + node.removeAttribute(FAVORITE_SOURCE_SECTION_ATTR); + node.removeAttribute(FAVORITE_DUPLICATE_HIDDEN_ATTR); + }); + menu + .querySelectorAll(`[${SECTION_ATTR}]`) + .forEach((node) => { + node.removeAttribute(SECTION_ATTR); + node.removeAttribute(SECTION_EMPTY_ATTR); + }); + if (overlay && !overlay.querySelector(`[${MENU_ATTR}="true"]`)) { + overlay.removeAttribute(OVERLAY_ATTR); + markOverlayNoise(overlay); + } + }; + + const isSlashMenu = (menu) => { + if (!menu.closest('[data-composer-overlay-floating-ui="true"]')) return false; + if (isEmptySlashMenu(menu)) return true; + const scroller = menu.querySelector(".vertical-scroll-fade-mask"); + if (!(scroller instanceof HTMLElement)) return false; + if ( + isSlashQueryActive() && + menu.querySelectorAll('[data-list-navigation-item="true"]').length > 0 + ) { + return true; + } + return Array.from(scroller.children).some((group) => { + if (!(group instanceof HTMLElement)) return false; + const rows = group.querySelectorAll('[data-list-navigation-item="true"]'); + if (rows.length < 2) return false; + const header = group.querySelector(":scope > .sticky.top-0"); + const headerText = normText(header); + if (!/^Skills\b/i.test(headerText)) return false; + return Array.from(rows).some((row) => + row.querySelector( + '.text-token-description-foreground, span[class*="text-token-description-foreground"]', + ), + ); + }); + }; + + const isSlashQueryActive = () => + Array.from(document.querySelectorAll('.ProseMirror[contenteditable="true"]')).some( + (editor) => editor instanceof HTMLElement && normText(editor).startsWith("/"), + ); + + const isEmptySlashMenu = (menu) => + isSlashQueryActive() && + menu.querySelectorAll('[data-list-navigation-item="true"]').length === 0 && + /^No commands$/i.test(normText(menu)); + + const buildTopbar = (menu, scroller) => { + let topbar = menu.querySelector(`:scope > [${TOPBAR_ATTR}="true"]`); + if (topbar instanceof HTMLElement) return topbar; + topbar = document.createElement("div"); + topbar.setAttribute(TOPBAR_ATTR, "true"); + topbar.innerHTML = + `
General
` + + '
'; + menu.insertBefore(topbar, scroller); + return topbar; + }; + + const setTopbarTitle = (title, text) => { + if (!(title instanceof HTMLElement) || title.textContent === text) return; + title.textContent = text; + title.setAttribute("data-changing", "true"); + const previousTimer = titleTimers.get(title); + if (previousTimer) window.clearTimeout(previousTimer); + const timer = window.setTimeout(() => { + title.removeAttribute("data-changing"); + titleTimers.delete(title); + }, 190); + titleTimers.set(title, timer); + }; + + const groupSections = (scroller) => + Array.from(scroller.children) + .filter( + (node) => + node instanceof HTMLElement && + !node.hasAttribute(TOPBAR_ATTR) && + node.getAttribute(SECTION_EMPTY_ATTR) !== "true" && + node.querySelector('[data-list-navigation-item="true"]'), + ) + .map((group, index) => { + const header = group.querySelector(":scope > .sticky.top-0"); + const isFavorites = group.hasAttribute(FAVORITES_GROUP_ATTR); + const title = isFavorites ? "Favorites" : normText(header) || "General"; + const key = sectionKey(title); + const color = sectionColor(key, index); + const favorites = readFavorites(); + group.setAttribute(SECTION_ATTR, key); + group.dataset.codexppSlashSectionTitle = title; + group.style.setProperty("--codexpp-section-color", color); + group.querySelectorAll('[data-list-navigation-item="true"]').forEach((button) => { + if (!(button instanceof HTMLElement)) return; + if (button.hasAttribute(FAVORITE_CLONE_ATTR)) stripNativeCommandState(button); + button.setAttribute(SECTION_ATTR, key); + button.style.setProperty("--codexpp-section-color", color); + const visualKey = + button.getAttribute(FAVORITE_SOURCE_SECTION_ATTR) || + button.getAttribute(SECTION_ATTR) || + key; + if (visualKey === "skills") wrapSkillRow(button); + else unwrapSkillRow(button); + const favoriteKey = + button.getAttribute(FAVORITE_KEY_ATTR) || rowFavoriteKey(button, key); + ensureFavoriteControl(button, favoriteKey, favorites); + }); + return { group, title, key, color }; + }); + + const renderTopbarIcons = (topbar, sections) => { + const icons = topbar.querySelector(".codexpp-slash-section-icons"); + if (!(icons instanceof HTMLElement)) return; + const signature = sections.map((s) => `${s.key}:${s.title}`).join("|"); + if (icons.dataset.signature === signature) return; + icons.dataset.signature = signature; + icons.replaceChildren(); + for (const section of sections) { + const button = document.createElement("button"); + button.type = "button"; + button.setAttribute(SECTION_ICON_ATTR, section.key); + button.setAttribute("aria-label", section.title); + button.style.setProperty("--codexpp-section-color", section.color); + button.innerHTML = sectionIconSvg(section.key); + button.addEventListener("click", (event) => { + event.preventDefault(); + event.stopPropagation(); + const scroller = topbar.nextElementSibling; + if (!(scroller instanceof HTMLElement)) return; + scrollToSection(scroller, section, sections); + updateTopbar(scroller, sections); + }); + icons.appendChild(button); + } + }; + + const scrollToSection = (scroller, section, sections = groupSections(scroller)) => { + const menu = scroller.closest(`[${MENU_ATTR}="true"]`); + menu?.setAttribute(INPUT_MODE_ATTR, "keyboard"); + menu?.setAttribute(PROGRAM_SCROLL_ATTR, "true"); + if (menu instanceof HTMLElement) suppressHoverUntilPointerMoves(menu); + scroller.dataset.codexppSlashFavoriteSelectionTouched = "true"; + scroller.scrollLeft = 0; + const targetTop = sectionTop(scroller, section.group); + const adjustedTop = + sections.indexOf(section) > 0 + ? Math.min(targetTop + 1, scroller.scrollHeight - scroller.clientHeight) + : targetTop; + allowProgrammaticScroll(scroller, adjustedTop); + const topbar = scroller.previousElementSibling; + if (topbar instanceof HTMLElement) { + topbar.dataset.forcedActiveSection = section.key; + } + updateTopbar(scroller, sections); + animateScrollTop( + scroller, + adjustedTop, + () => updateTopbar(scroller, sections), + () => { + menu?.removeAttribute(PROGRAM_SCROLL_ATTR); + if (topbar instanceof HTMLElement) delete topbar.dataset.forcedActiveSection; + updateTopbar(scroller, sections); + }, + ); + updateTopbar(scroller, sections); + }; + + const sectionTop = (scroller, group) => { + const target = + scroller.scrollTop + + group.getBoundingClientRect().top - + scroller.getBoundingClientRect().top; + return Math.max(0, Math.min(target, scroller.scrollHeight - scroller.clientHeight)); + }; + + const cancelScrollAnimation = (scroller) => { + const animation = scrollAnimations.get(scroller); + if (!animation) return; + cancelAnimationFrame(animation.frame); + window.clearTimeout(animation.timer); + scrollAnimations.delete(scroller); + }; + + const animateScrollTop = (scroller, targetTop, onStep, onDone) => { + cancelScrollAnimation(scroller); + const startTop = scroller.scrollTop; + const delta = targetTop - startTop; + if (Math.abs(delta) < 1) { + scroller.scrollTop = targetTop; + onStep?.(); + onDone?.(); + return; + } + const start = performance.now(); + const duration = 260; + const scheduleTick = () => { + const animation = { frame: 0, timer: 0 }; + const run = (now = performance.now()) => { + if (scrollAnimations.get(scroller) !== animation) return; + cancelAnimationFrame(animation.frame); + window.clearTimeout(animation.timer); + tick(now); + }; + animation.frame = requestAnimationFrame(run); + animation.timer = window.setTimeout(() => run(performance.now()), 16); + scrollAnimations.set(scroller, animation); + }; + const tick = (now) => { + const progress = Math.min(1, (now - start) / duration); + const eased = 1 - Math.pow(1 - progress, 3); + scroller.scrollTop = startTop + delta * eased; + onStep?.(); + if (progress < 1) { + scheduleTick(); + } else { + scrollAnimations.delete(scroller); + onDone?.(); + } + }; + scheduleTick(); + }; + + const updateTopbar = (scroller, sections = groupSections(scroller)) => { + const topbar = scroller.previousElementSibling; + if (!(topbar instanceof HTMLElement) || !topbar.hasAttribute(TOPBAR_ATTR)) return; + if (!(topbar instanceof HTMLElement) || sections.length === 0) return; + const threshold = scroller.scrollTop + 4; + let active = sections[0]; + for (const section of sections) { + if (sectionTop(scroller, section.group) <= threshold) active = section; + } + if (topbar.dataset.forcedActiveSection) { + active = + sections.find((section) => section.key === topbar.dataset.forcedActiveSection) || + active; + } + const title = topbar.querySelector(`[${SECTION_TITLE_ATTR}]`); + setTopbarTitle(title, active.title); + topbar.dataset.activeSection = active.key; + topbar.style.setProperty("--codexpp-section-color", active.color); + topbar + .querySelectorAll(`[${SECTION_ICON_ATTR}]`) + .forEach((button) => + button.setAttribute( + "data-active", + button.getAttribute(SECTION_ICON_ATTR) === active.key ? "true" : "false", + ), + ); + }; + + const enhanceMenu = (menu) => { + if (!isSlashMenu(menu)) { + cleanupMenu(menu); + return; + } + menu.setAttribute(MENU_ATTR, "true"); + menu.closest('[data-composer-overlay-floating-ui="true"]')?.setAttribute(OVERLAY_ATTR, "true"); + pruneMenuNoise(menu); + if (isEmptySlashMenu(menu)) { + menu.querySelectorAll(`[${TOPBAR_ATTR}]`).forEach((node) => node.remove()); + return; + } + const scroller = menu.querySelector(".vertical-scroll-fade-mask"); + if (!(scroller instanceof HTMLElement)) { + menu.querySelectorAll(`[${TOPBAR_ATTR}]`).forEach((node) => node.remove()); + return; + } + scroller.scrollLeft = 0; + const topbar = buildTopbar(menu, scroller); + groupSections(scroller); + syncFavoritesSection(scroller); + const sections = groupSections(scroller); + renderTopbarIcons(topbar, sections); + updateTopbar(scroller, sections); + ensureInitialFavoriteSelection(scroller); + reconcileFavoriteSelection(scroller); + if (!scrollHandlers.has(scroller)) { + const handler = () => { + enforceHoverScrollFreeze(scroller); + updateTopbar(scroller); + }; + scroller.addEventListener("scroll", handler, { passive: true }); + scrollHandlers.set(scroller, handler); + } + hoverScrollStateFor(scroller).lastTop = scroller.scrollTop; + if (!pointerHandlers.has(scroller)) { + const handler = (event) => { + if (menu.hasAttribute(PROGRAM_SCROLL_ATTR)) return; + if (event.type === "pointermove") { + if (!menu.hasAttribute(HOVER_SUPPRESS_ATTR)) { + menu.setAttribute(INPUT_MODE_ATTR, "pointer"); + } + return; + } + menu.setAttribute(INPUT_MODE_ATTR, "pointer"); + }; + scroller.addEventListener("pointermove", handler, { passive: true }); + scroller.addEventListener("pointerdown", handler, { passive: true }); + pointerHandlers.set(scroller, handler); + } + if (!wheelHandlers.has(scroller)) { + const handler = () => clearHoverScrollFreeze(scroller); + scroller.addEventListener("wheel", handler, { passive: true }); + wheelHandlers.set(scroller, handler); + } + if (!hoverGuardHandlers.has(scroller)) { + const handler = (event) => { + stopHoverSelectionEvent(menu, event); + }; + HOVER_GUARD_EVENTS.forEach((type) => scroller.addEventListener(type, handler, true)); + hoverGuardHandlers.set(scroller, handler); + } + }; + + const activeSlashMenu = () => + Array.from(document.querySelectorAll(`[${MENU_ATTR}="true"]`)).find( + (menu) => + menu instanceof HTMLElement && + menu.isConnected && + menu.querySelector(".vertical-scroll-fade-mask"), + ); + + installDocumentHoverGuard(); + Element.prototype.scrollIntoView = patchedScrollIntoView; + + const keyDigit = (event) => { + const key = String(event.key || ""); + if (/^[1-9]$/.test(key)) return Number(key); + const code = String(event.code || ""); + const match = /^(?:Digit|Numpad)([1-9])$/.exec(code); + return match ? Number(match[1]) : 0; + }; + + const onSectionShortcut = (event) => { + const menu = activeSlashMenu(); + if (!(menu instanceof HTMLElement)) return; + const scroller = menu.querySelector(".vertical-scroll-fade-mask"); + if (!(scroller instanceof HTMLElement)) return; + + if (handleFavoriteNavigationKey(event, scroller)) return; + + if ( + event.key === "ArrowDown" || + event.key === "ArrowUp" || + event.key === "Home" || + event.key === "End" || + event.key === "PageDown" || + event.key === "PageUp" + ) { + allowSlashRowScrollIntoView(); + menu.setAttribute(INPUT_MODE_ATTR, "keyboard"); + return; + } + + if (!(event.metaKey || event.ctrlKey) || event.altKey || event.shiftKey) return; + const digit = keyDigit(event); + activateSectionByDigit(scroller, digit, event); + }; + + const onSectionShortcutBridge = (event) => { + const menu = activeSlashMenu(); + if (!(menu instanceof HTMLElement)) return; + const scroller = menu.querySelector(".vertical-scroll-fade-mask"); + if (!(scroller instanceof HTMLElement)) return; + activateSectionByDigit(scroller, Number(event.detail?.digit) || 0, event); + }; + + const activateSectionByDigit = (scroller, digit, event) => { + if (!digit) return; + const sections = groupSections(scroller); + const section = sections[digit - 1]; + if (!section) return; + event.preventDefault(); + event.stopPropagation(); + event.stopImmediatePropagation?.(); + scrollToSection(scroller, section, sections); + }; + + const scan = () => { + scanFrame = 0; + try { + pruneHomeContentNoise(); + } catch { + // Ignore transient DOM shapes while Codex is replacing the slash panel. + } + document + .querySelectorAll('[data-composer-overlay-floating-ui="true"]') + .forEach((overlay) => { + if (!(overlay instanceof HTMLElement)) return; + try { + pruneOverlayNoise(overlay); + markOverlayNoise(overlay); + } catch { + // Keep the observer alive if Codex swaps the overlay mid-scan. + } + }); + document + .querySelectorAll('[data-composer-overlay-floating-ui="true"] > *') + .forEach((menu) => { + if (!(menu instanceof HTMLElement)) return; + try { + enhanceMenu(menu); + } catch { + // Keep scanning other candidates. + } + }); + }; + + const scheduleScan = () => { + if (scanFrame || scanTimer) return; + const run = () => { + if (scanFrame) cancelAnimationFrame(scanFrame); + if (scanTimer) window.clearTimeout(scanTimer); + scanFrame = 0; + scanTimer = 0; + scan(); + }; + scanFrame = requestAnimationFrame(run); + scanTimer = window.setTimeout(run, 60); + }; + + const scheduleSlashWork = () => { + scheduleHomeContentPrune(); + scheduleHardPruneNoise(); + scheduleScan(); + }; + + scan(); + observer = new MutationObserver(scheduleSlashWork); + observer.observe(document.body, OBSERVER_OPTIONS); + document.addEventListener("input", scheduleSlashWork, true); + document.addEventListener("keyup", scheduleSlashWork, true); + window.addEventListener("codexpp-slash-section-shortcut", onSectionShortcutBridge); + window.addEventListener("keydown", onSectionShortcut, true); + document.addEventListener("keydown", onSectionShortcut, true); + const activeSlashInterval = window.setInterval(() => { + if (isSlashQueryActive()) scheduleSlashWork(); + }, 250); + + return () => { + disposed = true; + observer.disconnect(); + window.clearInterval(activeSlashInterval); + if (scanFrame) cancelAnimationFrame(scanFrame); + if (scanTimer) window.clearTimeout(scanTimer); + if (homePruneFrame) cancelAnimationFrame(homePruneFrame); + if (hardPruneTimer) window.clearTimeout(hardPruneTimer); + document.removeEventListener("input", scheduleSlashWork, true); + document.removeEventListener("keyup", scheduleSlashWork, true); + window.removeEventListener("codexpp-slash-section-shortcut", onSectionShortcutBridge); + window.removeEventListener("keydown", onSectionShortcut, true); + document.removeEventListener("keydown", onSectionShortcut, true); + for (const [scroller, handler] of scrollHandlers) { + scroller.removeEventListener("scroll", handler); + } + scrollHandlers.clear(); + for (const [scroller, handler] of pointerHandlers) { + scroller.removeEventListener("pointermove", handler); + scroller.removeEventListener("pointerdown", handler); + } + pointerHandlers.clear(); + for (const [scroller, handler] of wheelHandlers) { + scroller.removeEventListener("wheel", handler); + } + wheelHandlers.clear(); + for (const [scroller, handler] of hoverGuardHandlers) { + HOVER_GUARD_EVENTS.forEach((type) => + scroller.removeEventListener(type, handler, true), + ); + } + hoverGuardHandlers.clear(); + if (documentHoverGuard) { + HOVER_GUARD_EVENTS.forEach((type) => + window.removeEventListener(type, documentHoverGuard, true), + ); + HOVER_GUARD_EVENTS.forEach((type) => + document.removeEventListener(type, documentHoverGuard, true), + ); + documentHoverGuard = null; + } + if (Element.prototype.scrollIntoView === patchedScrollIntoView) { + Element.prototype.scrollIntoView = nativeScrollIntoView; + } + for (const animation of scrollAnimations.values()) { + cancelAnimationFrame(animation.frame); + window.clearTimeout(animation.timer); + } + scrollAnimations.clear(); + for (const timer of titleTimers.values()) window.clearTimeout(timer); + titleTimers.clear(); + document + .querySelectorAll(`[${FAVORITES_GROUP_ATTR}]`) + .forEach((node) => node.remove()); + document.querySelectorAll(`.${FAVORITE_BUTTON_CLASS}`).forEach((node) => node.remove()); + document.querySelectorAll(`.${SKILL_COPY_CLASS}`).forEach((copy) => { + if (!(copy instanceof HTMLElement) || !copy.parentElement) return; + while (copy.firstChild) copy.parentElement.insertBefore(copy.firstChild, copy); + copy.remove(); + }); + document.querySelectorAll(`[${TOPBAR_ATTR}]`).forEach((node) => node.remove()); + document + .querySelectorAll(`[${MENU_ATTR}]`) + .forEach((node) => node.removeAttribute(MENU_ATTR)); + document + .querySelectorAll(`[${INPUT_MODE_ATTR}]`) + .forEach((node) => node.removeAttribute(INPUT_MODE_ATTR)); + document + .querySelectorAll(`[${PROGRAM_SCROLL_ATTR}]`) + .forEach((node) => node.removeAttribute(PROGRAM_SCROLL_ATTR)); + document + .querySelectorAll(`[${HOVER_SUPPRESS_ATTR}]`) + .forEach((node) => node.removeAttribute(HOVER_SUPPRESS_ATTR)); + document + .querySelectorAll(`[${OVERLAY_ATTR}]`) + .forEach((node) => node.removeAttribute(OVERLAY_ATTR)); + document + .querySelectorAll(`[${OVERLAY_NOISE_ATTR}]`) + .forEach((node) => node.removeAttribute(OVERLAY_NOISE_ATTR)); + document + .querySelectorAll(`[${SECTION_ATTR}]`) + .forEach((node) => { + node.removeAttribute(SECTION_ATTR); + node.removeAttribute(SECTION_EMPTY_ATTR); + }); + document + .querySelectorAll( + `[${FAVORITE_KEY_ATTR}], [${FAVORITE_CLONE_ATTR}], [${FAVORITE_SOURCE_SECTION_ATTR}], [${FAVORITE_DUPLICATE_HIDDEN_ATTR}]`, + ) + .forEach((node) => { + node.removeAttribute(FAVORITE_KEY_ATTR); + node.removeAttribute(FAVORITE_CLONE_ATTR); + node.removeAttribute(FAVORITE_SOURCE_SECTION_ATTR); + node.removeAttribute(FAVORITE_DUPLICATE_HIDDEN_ATTR); + }); + style.remove(); + }; + }, + + /** + * Add a compact search field to the Settings sidebar and filter the + * visible settings tabs in place. This is deliberately a tweak, not core + * Codex++, because it is a reversible UI convenience layer. + */ + "settings-search"(api) { + const STYLE_ID = "codexpp-settings-search-style"; + const ROOT_ATTR = "data-codexpp-settings-search"; + const HIDDEN_ATTR = "data-codexpp-settings-search-hidden"; + const PREV_DISPLAY_ATTR = "codexppSettingsSearchPrevDisplay"; + const SIDEBAR_SELECTOR = ".window-fx-sidebar-surface.w-token-sidebar"; + + document.getElementById(STYLE_ID)?.remove(); + const style = document.createElement("style"); + style.id = STYLE_ID; + style.textContent = ` + [${ROOT_ATTR}] { + padding: 0.75rem 0 0.5rem; + } + + [${ROOT_ATTR}] .codexpp-settings-search-box { + position: relative; + display: flex; + align-items: center; + } + + [${ROOT_ATTR}] svg { + position: absolute; + left: 0.625rem; + height: 1rem; + width: 1rem; + color: var(--color-token-text-secondary); + pointer-events: none; + } + + [${ROOT_ATTR}] input { + width: 100%; + height: 2rem; + min-width: 0; + border-radius: var(--radius-md, 0.375rem); + border: 1px solid color-mix(in srgb, currentColor 13%, transparent); + background: color-mix(in srgb, currentColor 4%, transparent); + color: var(--color-token-text-primary); + font-size: 0.875rem; + line-height: 1.25rem; + padding: 0 0.625rem 0 2rem; + outline: none; + } + + [${ROOT_ATTR}] input::placeholder { + color: var(--color-token-text-secondary); + } + + [${ROOT_ATTR}] input:focus { + border-color: color-mix(in srgb, currentColor 18%, transparent); + box-shadow: none; + } + + [${ROOT_ATTR}] .codexpp-settings-search-empty { + display: none; + padding-top: 1.25rem; + color: var(--color-token-text-secondary); + font-size: 0.75rem; + line-height: 1rem; + text-align: center; + } + + [${ROOT_ATTR}][data-empty="true"] .codexpp-settings-search-empty { + display: block; + } + + [${ROOT_ATTR}] .codexpp-settings-search-results { + display: none; + flex-direction: column; + gap: 0.125rem; + padding-top: 0.375rem; + } + + [${ROOT_ATTR}][data-has-results="true"] .codexpp-settings-search-results { + display: flex; + } + + [${ROOT_ATTR}] .codexpp-settings-search-result { + display: flex; + min-width: 0; + width: 100%; + align-items: center; + justify-content: space-between; + gap: 0.375rem; + border-radius: var(--radius-md, 0.375rem); + padding: 0.25rem 0.5rem; + color: var(--color-token-text-secondary); + font-size: 0.75rem; + line-height: 1rem; + text-align: left; + } + + [${ROOT_ATTR}] .codexpp-settings-search-result:hover, + [${ROOT_ATTR}] .codexpp-settings-search-result:focus-visible { + background: color-mix(in srgb, currentColor 8%, transparent); + color: var(--color-token-text-primary); + outline: none; + } + + [${ROOT_ATTR}] .codexpp-settings-search-result span { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + [data-codexpp-settings-search-highlight="true"] { + outline: 2px solid var(--color-token-focus-border, var(--color-token-border)); + outline-offset: 5px; + border-radius: var(--radius-md, 0.375rem); + transition: + outline-color 220ms ease, + outline-offset 220ms ease; + } + + [data-codexpp-settings-search-highlight="fading"] { + outline: 2px solid transparent; + outline-offset: 9px; + border-radius: var(--radius-md, 0.375rem); + transition: + outline-color 420ms ease, + outline-offset 420ms ease; + } + `; + document.head.appendChild(style); + + const root = document.createElement("div"); + root.setAttribute(ROOT_ATTR, "true"); + + const box = document.createElement("div"); + box.className = "codexpp-settings-search-box"; + box.innerHTML = + '"; + + const input = document.createElement("input"); + input.type = "search"; + input.placeholder = "Search settings"; + input.autocomplete = "off"; + input.spellcheck = false; + input.setAttribute("aria-label", "Search settings"); + box.appendChild(input); + root.appendChild(box); + + const empty = document.createElement("div"); + empty.className = "codexpp-settings-search-empty"; + empty.textContent = "No matching settings"; + root.appendChild(empty); + + const results = document.createElement("div"); + results.className = "codexpp-settings-search-results"; + root.appendChild(results); + + let scheduled = false; + let disposed = false; + let lastSidebar = null; + let highlightTimer = null; + const revealTimers = new Set(); + const pageIndex = new Map(); + + const compact = (value) => + String(value || "").replace(/\s+/g, " ").trim().toLowerCase(); + + const knownContent = [ + { + page: "General", + title: "Work mode", + text: "work mode coding everyday technical detail", + }, + { + page: "General", + title: "Permissions", + text: "permissions default permissions auto-review full access", + }, + { + page: "General", + title: "General", + text: "general default open destination language show in menu bar prevent sleep follow-up behavior import other agent setup", + }, + { + page: "General", + title: "Dictation", + text: "dictation hold-to-dictate hotkey toggle dictation hotkey dictation dictionary recent dictations", + }, + { + page: "General", + title: "Dictation dictionary", + text: "dictation dictionary words phrases dictation should recognize", + }, + { + page: "General", + title: "Notifications", + text: "notifications turn completion notifications permission notifications alerts", + }, + ].map((item) => ({ + ...item, + text: compact(`${item.title} ${item.text}`), + node: null, + })); + + const labelFor = (node) => + compact( + [ + node.getAttribute?.("aria-label"), + node.getAttribute?.("title"), + node.textContent, + ] + .filter(Boolean) + .join(" "), + ); + + const visibleLabelFor = (node) => compact(node?.textContent || ""); + const displayLabelFor = (node) => + String(node?.textContent || "").replace(/\s+/g, " ").trim(); + + const findSettingsSidebar = () => { + const exact = document.querySelector(SIDEBAR_SELECTOR); + if (exact instanceof HTMLElement) return exact; + const candidates = Array.from(document.querySelectorAll("div")).filter( + (node) => { + if (!(node instanceof HTMLElement)) return false; + const rect = node.getBoundingClientRect(); + if (rect.width < 180 || rect.width > 420 || rect.height < 240) return false; + const text = compact(node.textContent); + return ( + text.includes("general") && + text.includes("appearance") && + (text.includes("configuration") || text.includes("account")) + ); + }, + ); + return candidates[0] instanceof HTMLElement ? candidates[0] : null; + }; + + const findMount = (sidebar) => { + const groups = Array.from(sidebar.querySelectorAll("div")).filter( + (node) => + node instanceof HTMLElement && + node.classList.contains("flex") && + node.classList.contains("flex-col") && + node.classList.contains("gap-px") && + Array.from(node.children).some( + (child) => + child instanceof HTMLElement && + child.matches("button, a") && + visibleLabelFor(child) === "general", + ), + ); + const itemsGroup = groups[0]; + const outer = itemsGroup?.parentElement; + if (itemsGroup instanceof HTMLElement && outer instanceof HTMLElement) { + const header = Array.from(outer.children).find( + (child) => + child instanceof HTMLElement && + child !== root && + !child.querySelector("button, a") && + visibleLabelFor(child) === "general", + ); + return { + parent: outer, + before: header instanceof HTMLElement ? header : itemsGroup, + }; + } + const nav = sidebar.querySelector("nav"); + return { + parent: nav instanceof HTMLElement ? nav : sidebar, + before: nav instanceof HTMLElement ? nav.firstElementChild : sidebar.firstElementChild, + }; + }; + + const hide = (node, hidden) => { + if (!(node instanceof HTMLElement) || root.contains(node)) return; + if (hidden) { + if (node.getAttribute(HIDDEN_ATTR) === "true") return; + node.dataset[PREV_DISPLAY_ATTR] = node.style.display || ""; + node.style.display = "none"; + node.setAttribute(HIDDEN_ATTR, "true"); + } else if (node.getAttribute(HIDDEN_ATTR) === "true") { + node.style.display = node.dataset[PREV_DISPLAY_ATTR] || ""; + delete node.dataset[PREV_DISPLAY_ATTR]; + node.removeAttribute(HIDDEN_ATTR); + } + }; + + const navigateToPage = (sidebar, page) => { + const nav = navForPage(sidebar, page); + if (!(nav instanceof HTMLElement)) return false; + hide(nav, false); + nav.click(); + return true; + }; + + const restoreHidden = (scope = document) => { + scope.querySelectorAll(`[${HIDDEN_ATTR}="true"]`).forEach((node) => { + hide(node, false); + }); + }; + + const visibleControlsIn = (node) => + Array.from(node.querySelectorAll("button, a")).filter( + (control) => + control instanceof HTMLElement && + !root.contains(control) && + control.getAttribute(HIDDEN_ATTR) !== "true", + ); + + const navControls = (sidebar) => + Array.from(sidebar.querySelectorAll("button, a")).filter( + (node) => node instanceof HTMLElement && !root.contains(node), + ); + + const activePageLabel = (sidebar) => { + const active = navControls(sidebar).find((node) => { + const className = String(node.className || ""); + return ( + node.getAttribute("aria-current") === "page" || + node.getAttribute("data-state") === "active" || + className.includes("active") || + className.includes("selection") + ); + }); + const activeLabel = displayLabelFor(active); + if (activeLabel) return titleCaseLabel(activeLabel); + + const heading = document.querySelector( + ".main-surface .heading-base, .main-surface .electron\\:heading-lg, .main-surface [role='heading']", + ); + const headingLabel = displayLabelFor(heading); + return headingLabel ? titleCaseLabel(headingLabel) : "Settings"; + }; + + const titleCaseLabel = (value) => { + const raw = String(value || "").replace(/\s+/g, " ").trim(); + return raw || "Settings"; + }; + + const mainSurface = () => { + const surface = document.querySelector(".main-surface"); + return surface instanceof HTMLElement ? surface : null; + }; + + const shortText = (node) => + String(node?.textContent || "") + .replace(/\s+/g, " ") + .trim(); + + const sectionTitleFor = (node) => { + const candidates = [ + ":scope > div:first-child .text-base", + ":scope > div:first-child [class*='heading']", + ":scope > div:first-child [role='heading']", + ".text-base.font-medium", + ".min-w-0.text-sm.text-token-text-primary", + ".text-sm.text-token-text-primary", + "button .text-sm", + "button span", + ]; + for (const selector of candidates) { + const found = node.querySelector(selector); + const text = shortText(found); + if (text && text.length <= 80) return text; + } + const text = shortText(node); + return text.slice(0, 80); + }; + + const contentCandidates = () => { + const surface = mainSurface(); + if (!surface) return []; + const nodes = Array.from( + surface.querySelectorAll( + "section, [class*='p-3'], button[class*='p-3'], button.flex.w-full", + ), + ).filter((node) => node instanceof HTMLElement); + return nodes.filter((node) => { + if (root.contains(node)) return false; + const rect = node.getBoundingClientRect(); + if (rect.width < 120 || rect.height < 18) return false; + const text = shortText(node); + if (!text || text.length < 2) return false; + return !nodes.some( + (other) => + other !== node && + other instanceof HTMLElement && + node.contains(other) && + shortText(other) === text, + ); + }); + }; + + const updateCurrentPageIndex = (sidebar) => { + const page = activePageLabel(sidebar); + const items = []; + const seen = new Set(); + for (const node of contentCandidates()) { + const title = sectionTitleFor(node); + const text = shortText(node); + const key = compact(title); + if (!title || seen.has(key)) continue; + seen.add(key); + items.push({ page, title, text: compact(`${title} ${text}`), node }); + } + if (items.length > 0) pageIndex.set(page, items); + }; + + const contentMatches = (query) => { + if (!query) return []; + const matches = []; + const seen = new Set(); + for (const item of knownContent) { + const key = `${item.page}:${item.title}`; + if (!item.text.includes(query) || seen.has(key)) continue; + seen.add(key); + matches.push(item); + } + for (const [page, items] of pageIndex.entries()) { + for (const item of items) { + const key = `${page}:${item.title}`; + if (!item.text.includes(query) || seen.has(key)) continue; + seen.add(key); + matches.push({ ...item, page }); + if (matches.length >= 8) return matches; + } + } + return matches; + }; + + const navForPage = (sidebar, page) => + navControls(sidebar).find((node) => visibleLabelFor(node) === compact(page)); + + const clearHighlight = () => { + document + .querySelectorAll("[data-codexpp-settings-search-highlight]") + .forEach((node) => node.removeAttribute("data-codexpp-settings-search-highlight")); + if (highlightTimer) { + window.clearTimeout(highlightTimer); + highlightTimer = null; + } + }; + + const fadeHighlight = (target) => { + if (target.getAttribute("data-codexpp-settings-search-highlight") !== "true") return; + target.setAttribute("data-codexpp-settings-search-highlight", "fading"); + highlightTimer = window.setTimeout(clearHighlight, 450); + }; + + const findContentTarget = (match) => { + if (match.node instanceof HTMLElement && document.contains(match.node)) { + return match.node; + } + const title = compact(match.title); + const candidates = contentCandidates(); + return ( + candidates.find((node) => compact(sectionTitleFor(node)) === title) || + candidates.find((node) => compact(shortText(node)).includes(title)) || + null + ); + }; + + const scrollToMatch = (match) => { + const target = findContentTarget(match); + if (!(target instanceof HTMLElement)) return false; + clearHighlight(); + target.setAttribute("data-codexpp-settings-search-highlight", "true"); + target.scrollIntoView({ block: "center", behavior: "smooth" }); + highlightTimer = window.setTimeout(() => fadeHighlight(target), 3000); + return true; + }; + + const clearRevealTimers = () => { + for (const timer of revealTimers) window.clearTimeout(timer); + revealTimers.clear(); + }; + + const revealMatch = (match, attempts = 12) => { + if (disposed) return; + if (lastSidebar) updateCurrentPageIndex(lastSidebar); + if (scrollToMatch(match)) return; + if (attempts <= 0) return; + const timer = window.setTimeout(() => { + revealTimers.delete(timer); + revealMatch(match, attempts - 1); + }, 125); + revealTimers.add(timer); + }; + + const renderResults = (sidebar, matches) => { + results.replaceChildren(); + for (const match of matches.slice(0, 5)) { + const button = document.createElement("button"); + button.type = "button"; + button.className = "codexpp-settings-search-result cursor-interaction"; + button.title = `Reveal ${match.page} > ${match.title}`; + const label = document.createElement("span"); + label.textContent = `${match.page} > ${match.title}`; + button.appendChild(label); + const reveal = (event) => { + event.preventDefault(); + event.stopImmediatePropagation(); + clearRevealTimers(); + const currentSidebar = findSettingsSidebar() || sidebar; + navigateToPage(currentSidebar, match.page); + window.setTimeout(() => revealMatch(match), 0); + }; + button.addEventListener("pointerdown", reveal); + button.addEventListener("click", reveal); + button.addEventListener("keydown", (event) => { + if (event.key !== "Enter" && event.key !== " ") return; + reveal(event); + }); + results.appendChild(button); + } + root.dataset.hasResults = matches.length > 0 ? "true" : "false"; + }; + + const syncGroupVisibility = (parent, query) => { + const children = Array.from(parent.children).filter( + (child) => child instanceof HTMLElement && child !== root, + ); + + for (const child of children) { + if (!(child instanceof HTMLElement)) continue; + if (child.querySelector("button, a")) { + const hasVisibleControl = visibleControlsIn(child).length > 0; + const groupLabelMatches = compact(child.textContent).includes(query); + hide(child, !hasVisibleControl && !groupLabelMatches); + } + } + + for (let i = 0; i < children.length; i++) { + const child = children[i]; + if (!(child instanceof HTMLElement) || child.querySelector("button, a")) continue; + const labelMatches = compact(child.textContent).includes(query); + const nextGroup = children + .slice(i + 1) + .find((candidate) => candidate instanceof HTMLElement && candidate.querySelector("button, a")); + const nextVisible = + nextGroup instanceof HTMLElement && + nextGroup.getAttribute(HIDDEN_ATTR) !== "true" && + visibleControlsIn(nextGroup).length > 0; + hide(child, !labelMatches && !nextVisible); + } + }; + + const applyFilter = () => { + scheduled = false; + if (disposed) return; + + const sidebar = findSettingsSidebar(); + if (!sidebar) { + root.remove(); + restoreHidden(document); + return; + } + lastSidebar = sidebar; + + const mount = findMount(sidebar); + if (!root.isConnected || root.parentElement !== mount.parent) { + mount.parent.insertBefore(root, mount.before); + } else if (root.nextElementSibling !== mount.before && mount.before !== root) { + mount.parent.insertBefore(root, mount.before); + } + + updateCurrentPageIndex(sidebar); + restoreHidden(sidebar); + const query = compact(input.value); + root.dataset.empty = "false"; + root.dataset.hasResults = "false"; + results.replaceChildren(); + if (!query) return; + + const matches = contentMatches(query); + const matchingPages = new Set(matches.map((match) => compact(match.page))); + + const controls = navControls(sidebar); + let visibleCount = 0; + for (const control of controls) { + const matchesNav = + labelFor(control).includes(query) || matchingPages.has(visibleLabelFor(control)); + hide(control, !matchesNav); + if (matchesNav) visibleCount++; + } + + if (root.parentElement instanceof HTMLElement) { + syncGroupVisibility(root.parentElement, query); + } + renderResults(sidebar, matches); + root.dataset.empty = visibleCount === 0 && matches.length === 0 ? "true" : "false"; + }; + + const schedule = () => { + if (scheduled || disposed) return; + scheduled = true; + requestAnimationFrame(applyFilter); + }; + + input.addEventListener("input", schedule); + input.addEventListener("keydown", (event) => { + if (event.key !== "Escape") return; + if (input.value) { + input.value = ""; + schedule(); + } else { + input.blur(); + } + event.stopPropagation(); + }); + + const onDocumentKeydown = (event) => { + if (event.key.toLowerCase() !== "f" || (!event.metaKey && !event.ctrlKey)) return; + const sidebar = findSettingsSidebar(); + if (!sidebar || !document.contains(sidebar)) return; + event.preventDefault(); + event.stopPropagation(); + if (document.activeElement === input) { + input.blur(); + return; + } + schedule(); + window.setTimeout(() => { + input.focus(); + input.select(); + }, 0); + }; + + const observer = new MutationObserver(schedule); + observer.observe(document.documentElement, { childList: true, subtree: true }); + document.addEventListener("keydown", onDocumentKeydown, true); + window.addEventListener("codexpp:settings-surface", schedule); + schedule(); + + api.log.info("settings search active"); + + return () => { + disposed = true; + observer.disconnect(); + document.removeEventListener("keydown", onDocumentKeydown, true); + window.removeEventListener("codexpp:settings-surface", schedule); + clearRevealTimers(); + clearHighlight(); + restoreHidden(document); + root.remove(); + style.remove(); + }; + }, + + /** + * Match settings sidebar width to the main UI sidebar. + * + * Codex's main UI sidebar is `