diff --git a/index.json b/index.json index 2694c08..a111eef 100644 --- a/index.json +++ b/index.json @@ -139,19 +139,22 @@ { "id": "bennett-ui-improvements", "name": "Bennett UI Improvements", - "description": "迁移自 b-nnett Bennett UI 的 renderer-only UI 增强", - "version": "1.0.9-bigpizza.1", + "description": "彩色项目管理界面、额度显示与提示隐藏、Markdown 预览增强,以及仅提高 Codex 原生历史查询上限的轻量功能;由 b-nnett/codex-plusplus-bennett-ui 迁移适配至 BigPizzaV3 Codex++。", + "version": "1.2.4", "author": "bennett; JHees", "tags": [ "codex", "ui", "sidebar", "settings", - "polish" + "polish", + "markdown", + "math", + "history" ], - "homepage": "", + "homepage": "https://github.com/JHees/bennett-ui-improvements-for-codexplusplus", "script_url": "https://raw.githubusercontent.com/BigPizzaV3/CodexPlusPlusScriptMarket/main/scripts/bennett-ui-improvements.js", - "sha256": "2fb8c36ab5a7103d103deb53a55c5f42ab26763479bec253dccbb825c93e1af9" + "sha256": "39145ddb1101d41524f56cb96cfce7d1bc0e26baa8c11bedb32e7a144d2a9a29" }, { "id": "codex-provider-guard", diff --git a/scripts/bennett-ui-improvements.js b/scripts/bennett-ui-improvements.js index 1d0a673..cca7c81 100644 --- a/scripts/bennett-ui-improvements.js +++ b/scripts/bennett-ui-improvements.js @@ -21,7 +21,43 @@ "use strict"; const INSTALL_KEY = "__bennettUiImprovementsBigPizza"; - const VERSION = "1.0.9-bigpizza.1"; + const VERSION = "1.2.4"; + const HISTORY_TARGET_STORAGE_KEY = "__codexListPagebusterTarget"; + const HISTORY_TARGET_DEFAULT = 500; + const HISTORY_TARGET_MIN = 1; + const HISTORY_TARGET_MAX = 2000; + const SCRIPT_LOAD_ID = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; + const lifecycleTimers = new Set(); + const lifecycleSignatures = new Set(); + + function reportLifecycle(event, detail = {}) { + const signature = `${event}:${JSON.stringify(detail)}`; + if (event === "usage-mounted" && lifecycleSignatures.has(signature)) return; + lifecycleSignatures.add(signature); + const payload = { + event: `bennett-ui.${event}`, + version: VERSION, + scriptLoadId: SCRIPT_LOAD_ID, + ...detail, + }; + window.__bennettUiLastLifecycle = payload; + try { + const bridge = window.__codexSessionDeleteBridge; + if (typeof bridge === "function") { + Promise.resolve(bridge("/diagnostics/log", payload)).catch(() => {}); + } + } catch (_) {} + } + + function scheduleLifecycle(callback, delay) { + const timer = window.setTimeout(() => { + lifecycleTimers.delete(timer); + callback(); + }, delay); + lifecycleTimers.add(timer); + return timer; + } + const previous = window[INSTALL_KEY]; if (previous && typeof previous.stop === "function") { try { @@ -61,18 +97,12 @@ * 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. + * • Renderer-first implementation with reversible feature cleanup. * • 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. @@ -82,10 +112,7 @@ module.exports = { start(api) { if (api.process === "main") { - startMainMetricsProvider(api); startMainUsageProvider(api); - startMainProjectLabelProvider(api); - startMainSidebarBatchMenuProvider(api); startMainSlashMenuShortcutBridge(api); return; } @@ -96,15 +123,14 @@ module.exports = { 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, + "render-markdown-preview-math": true, "slash-menu-polish": true, + "hide-usage-alert": true, }, }; this._state = state; @@ -168,7 +194,7 @@ function renderSettings(root, state) { 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.', + 'Hide the Plus/Pro plan upgrade pill and Get Plus button, but keep Codex software-update notices visible.', }, { id: "show-usage-in-sidebar", @@ -176,12 +202,6 @@ function renderSettings(root, state) { 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", @@ -212,24 +232,18 @@ function renderSettings(root, state) { 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.", }, + { + id: "hide-usage-alert", + title: "Hide usage exhaustion alerts", + description: + "Hide Codex usage exhaustion banners and reset prompts.", + }, ]; const section = el("section", "flex flex-col gap-2"); @@ -362,6 +376,12 @@ function activateFeature(state, id) { state.api.log.info("activated", id); } catch (e) { state.api.log.error("activate failed", id, e); + if (typeof reportLifecycle === "function") { + reportLifecycle("feature-activation-failed", { + feature: id, + error: String(e?.stack || e), + }); + } } } @@ -380,298 +400,371 @@ function deactivateFeature(state, id) { 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. + * Render LaTeX inside Codex's right-side Markdown file preview. + * + * The preview is a CodeMirror editor. Formula source ranges are replaced by + * native CodeMirror widgets so math participates in the editor's own layout, + * scrolling, clipping, selection, and lifecycle. */ - "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 */); + "render-markdown-preview-math"(api) { + const STYLE_ID = "bennett-markdown-preview-math-style"; + const FORMULA_ATTR = "data-bennett-markdown-preview-math"; + const TABLE_ATTR = "data-bennett-markdown-preview-math-table"; + const CELL_ATTR = "data-bennett-markdown-preview-math-cell"; + const EDITOR_ATTR = "data-bennett-markdown-preview-math-editor"; + const EDITING_ATTR = "data-bennett-markdown-preview-math-editing"; + const IMAGE_ATTR = "data-bennett-markdown-preview-image"; + const IMAGE_STATUS_ATTR = "data-bennett-markdown-preview-image-status"; + const IMAGE_MAX_BYTES = 20 * 1024 * 1024; + const IMAGE_CACHE_MAX_BYTES = 64 * 1024 * 1024; + const IMAGE_READ_CONCURRENCY = 2; + const MARKDOWN_EXTENSION = /\.(?:md|markdown|mdown|mkd)$/i; + const states = new Map(); + const imageCache = new Map(); + let imageCacheBytes = 0; + const imageReadQueue = []; + let activeImageReads = 0; + let disposed = false; + let scanFrame = 0; + let scanning = false; + let scanRequested = false; + let katexPromise = null; + let mainModuleUrl = null; + let lastError = null; + let imageRequestSequence = 0; - const isInsideOurShell = (el) => { - let n = el; - while (n) { - if (n instanceof HTMLElement && n.dataset?.codexpp) return true; - n = n.parentElement; + const style = document.createElement("style"); + style.id = STYLE_ID; + style.textContent = ` + [${FORMULA_ATTR}] { + box-sizing: border-box; + color: var(--color-token-text-primary, currentColor); + vertical-align: baseline; } - 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(); + [${FORMULA_ATTR}="inline"] { + display: inline-block; + max-width: 100%; + padding-inline: 1px; + white-space: nowrap; + } + [${FORMULA_ATTR}="display-inline"] { + display: inline-block; + width: 100%; + max-width: 100%; + overflow-x: auto; + overflow-y: hidden; + vertical-align: middle; + } + [${FORMULA_ATTR}="display-block"] { + display: block; + width: 100%; + max-width: 100%; + min-height: 1.5em; + margin: 0.35em 0; + overflow-x: auto; + overflow-y: hidden; + text-align: center; + } + [${FORMULA_ATTR}] > .katex-display { + width: 100%; + margin: 0; + } + [${FORMULA_ATTR}] { + cursor: text; + } + [${FORMULA_ATTR}][${EDITING_ATTR}] { + width: 100%; + min-width: 0; + overflow: visible; + } + [${TABLE_ATTR}] { + display: block; + width: 100%; + max-width: 100%; + margin: 0.5em 0; + overflow-x: auto; + } + [${TABLE_ATTR}] table { + width: max-content; + min-width: min(100%, 36rem); + max-width: none; + border-collapse: collapse; + border-spacing: 0; + color: var(--color-token-text-primary, currentColor); + font: inherit; + } + [${TABLE_ATTR}] th, + [${TABLE_ATTR}] td { + min-width: 5em; + padding: 0.45em 0.75em; + border-bottom: 1px solid var( + --color-token-border-default, + color-mix(in srgb, currentColor 12%, transparent) + ); + text-align: left; + vertical-align: top; + white-space: nowrap; + } + [${TABLE_ATTR}] th { + font-weight: 600; + } + [${TABLE_ATTR}] tr:last-child td { + border-bottom-color: transparent; + } + [${TABLE_ATTR}] [${CELL_ATTR}] { + cursor: text; + outline: none; + } + [${TABLE_ATTR}] [${CELL_ATTR}]:hover, + [${TABLE_ATTR}] [${CELL_ATTR}]:focus-visible { + background: color-mix(in srgb, currentColor 5%, transparent); + } + [${TABLE_ATTR}] [${CELL_ATTR}][${EDITING_ATTR}] { + background: color-mix( + in srgb, + var(--color-token-main-surface-primary, Canvas) 88%, + currentColor 12% + ); + box-shadow: 0 0 0 1px var( + --color-token-focus-border, + color-mix(in srgb, currentColor 28%, transparent) + ) inset; + } + [${EDITOR_ATTR}] { + display: block; + box-sizing: border-box; + width: 100%; + min-width: 6em; + margin: 0; + border: 0; + outline: 0; + background: transparent; + color: inherit; + font: inherit; + line-height: inherit; + } + [${TABLE_ATTR}] [${EDITOR_ATTR}] { + min-width: 0; + max-width: 100%; + } + textarea[${EDITOR_ATTR}] { + min-height: 4.5em; + overflow-x: hidden; + overflow-y: hidden; + resize: vertical; + white-space: pre-wrap; + overflow-wrap: anywhere; + } + [${IMAGE_ATTR}] { + display: inline-block; + box-sizing: border-box; + max-width: 100%; + vertical-align: middle; + cursor: text; + } + [${IMAGE_ATTR}="block"] { + display: block; + width: 100%; + margin: 0.5em 0; + } + [${IMAGE_ATTR}] img { + display: block; + max-width: 100%; + height: auto; + border-radius: var(--radius-md, 0.375rem); + } + [${IMAGE_ATTR}="inline"] img { + max-height: 12em; + } + [${IMAGE_STATUS_ATTR}] { + display: inline-flex; + min-height: 2em; + max-width: 100%; + align-items: center; + padding: 0.35em 0.6em; + border: 1px solid var( + --color-token-border-default, + color-mix(in srgb, currentColor 12%, transparent) + ); + border-radius: var(--radius-md, 0.375rem); + color: var(--color-token-text-secondary, currentColor); + font-size: 0.875em; + overflow-wrap: anywhere; + } + `; + document.head.appendChild(style); - 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; - }; + function escapedAt(text, index) { + let slashes = 0; + for (let i = index - 1; i >= 0 && text[i] === "\\"; i -= 1) slashes += 1; + return slashes % 2 === 1; + } - 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 }); + function blankRange(chars, start, end) { + for (let i = start; i < end; i += 1) { + if (chars[i] !== "\n" && chars[i] !== "\r") chars[i] = " "; } - }; + } - scan(); - const obs = new MutationObserver(scan); - obs.observe(document.documentElement, { childList: true, subtree: true }); + function maskCode(text) { + const chars = text.split(""); + const linePattern = /.*(?:\r\n|\n|\r|$)/g; + let fence = null; + for (const match of text.matchAll(linePattern)) { + if (!match[0]) continue; + const start = match.index; + const end = start + match[0].length; + const body = match[0].replace(/(?:\r\n|\n|\r)$/, ""); + const opener = body.match(/^(?: {0,3})(`{3,}|~{3,})/); + if (fence) { + blankRange(chars, start, end); + const closer = body.match(/^(?: {0,3})(`{3,}|~{3,})\s*$/); + if ( + closer && + closer[1][0] === fence.character && + closer[1].length >= fence.length + ) { + fence = null; + } + continue; + } + if (opener) { + fence = { character: opener[1][0], length: opener[1].length }; + blankRange(chars, start, end); + continue; + } + if (/^(?: {4}|\t)/.test(body)) blankRange(chars, start, end); + } - return () => { - obs.disconnect(); - for (const el of hidden) { - if ("codexppPrevDisplay" in el.dataset) { - el.style.display = el.dataset.codexppPrevDisplay; - delete el.dataset.codexppPrevDisplay; + const fencedMasked = chars.join(""); + for (let i = 0; i < chars.length; i += 1) { + if ( + fencedMasked[i] !== "`" || + fencedMasked[i] === " " || + escapedAt(fencedMasked, i) + ) { + continue; } + let ticks = 1; + while (fencedMasked[i + ticks] === "`") ticks += 1; + const delimiter = "`".repeat(ticks); + const close = fencedMasked.indexOf(delimiter, i + ticks); + if (close < 0) break; + blankRange(chars, i, close + ticks); + i = close + ticks - 1; + } + return chars.join(""); + } + + function findClosing(text, delimiter, from, allowNewline) { + for (let i = from; i <= text.length - delimiter.length; i += 1) { + if (!allowNewline && (text[i] === "\n" || text[i] === "\r")) return -1; + if (text.startsWith(delimiter, i) && !escapedAt(text, i)) return i; } - hidden.clear(); - }; - }, + return -1; + } - /** - * 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; + function parseMath(text) { + const masked = maskCode(text); + const formulas = []; + for (let i = 0; i < masked.length; i += 1) { + if (masked[i] === " " || masked[i] === "\n" || escapedAt(masked, i)) continue; + + let opener = null; + let closer = null; + let display = false; + let allowNewline = false; + + if (masked.startsWith("$$", i)) { + opener = "$$"; + closer = "$$"; + display = true; + allowNewline = true; + } else if (masked.startsWith("\\[", i)) { + opener = "\\["; + closer = "\\]"; + display = true; + allowNewline = true; + } else if (masked.startsWith("\\(", i)) { + opener = "\\("; + closer = "\\)"; + } else if (masked[i] === "$" && masked[i + 1] !== "$") { + const next = masked[i + 1]; + if (next == null || /\s/.test(next)) continue; + opener = "$"; + closer = "$"; + } - 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(", "); - const SIDEBAR_CANDIDATE_SELECTOR = [ - ASIDE_SELECTOR, - "aside", - "nav", - "[role='navigation']", - "[data-testid*='sidebar' i]", - "[data-test*='sidebar' i]", - "[class*='sidebar' i]", - ].join(", "); + if (!opener) continue; + const contentStart = i + opener.length; + const close = findClosing(masked, closer, contentStart, allowNewline); + if (close < 0) continue; + if (opener === "$" && (masked[close - 1] == null || /\s/.test(masked[close - 1]))) { + continue; + } - // ── 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 content = text.slice(contentStart, close).trim(); + if (!content) { + i = close + closer.length - 1; + continue; + } + + formulas.push({ + start: i, + end: close + closer.length, + content, + display, + }); + i = close + closer.length - 1; } - const rect = node.getBoundingClientRect(); - return rect.width > 0 && rect.height > 0; - }; + return formulas; + } - 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(); + function dispatchDesktopViewMessage(message) { + let forwarded = false; + let pending = null; + const bridge = window.electronBridge; + if (typeof bridge?.sendMessageFromView === "function") { + forwarded = true; + try { + pending = Promise.resolve(bridge.sendMessageFromView(message)); + } catch (error) { + pending = Promise.reject(error); + } } - 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}`; + const event = new CustomEvent("codex-message-from-view", { + detail: message, + }); + if (forwarded) event.__codexForwardedViaBridge = true; + window.dispatchEvent(event); + return pending; + } + function requestDesktopJson(command, params, timeoutMs = 15_000) { + const requestSequence = ++imageRequestSequence; + const randomSuffix = + typeof window.crypto?.randomUUID === "function" + ? window.crypto.randomUUID() + : typeof window.crypto?.getRandomValues === "function" + ? Array.from(window.crypto.getRandomValues(new Uint32Array(4)), (value) => value.toString(16)).join("-") + : `${Date.now()}-${requestSequence}-${Math.random().toString(36).slice(2)}`; + const requestId = `bennett-preview-${randomSuffix}`; return new Promise((resolve, reject) => { - let done = false; + let finished = false; const cleanup = () => { - done = true; + if (finished) return; + finished = true; window.removeEventListener("message", onMessage); - window.removeEventListener("codexpp-usage-response", onBridgeResponse); window.clearTimeout(timer); }; - const finish = (fn, value) => { - if (done) return; + const finish = (callback, value) => { + if (finished) return; cleanup(); - fn(value); + callback(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 (event.source !== window) return; + const data = event.data; if ( !data || typeof data !== "object" || @@ -680,5242 +773,6593 @@ const FEATURES = { ) { 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); + if (data.responseType !== "success") { + finish(reject, new Error(data.error || `${command} failed`)); + return; + } + 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}`)); } - } else { - finish(reject, new Error(data.error || "fetch failed")); + } catch (error) { + finish(reject, error); } }; const timer = window.setTimeout(() => { - dispatchCodexViewMessage({ type: "cancel-fetch", requestId }); - finish(reject, new Error("usage request timed out")); + dispatchDesktopViewMessage({ type: "cancel-fetch", requestId }); + finish(reject, new Error(`${command} timed out`)); }, timeoutMs); window.addEventListener("message", onMessage); - window.addEventListener("codexpp-usage-response", onBridgeResponse); - dispatchCodexViewMessage({ + const pending = dispatchDesktopViewMessage({ type: "fetch", - hostId, requestId, - method: "GET", - url, + method: "POST", + url: `vscode://codex/${command}`, + headers: { + "content-type": "application/json", + }, + body: JSON.stringify(params), }); + pending?.catch((error) => finish(reject, error)); }); - }; - - 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]; + function pumpImageReadQueue() { + while ( + !disposed && + activeImageReads < IMAGE_READ_CONCURRENCY && + imageReadQueue.length + ) { + const entry = imageReadQueue.shift(); + activeImageReads += 1; + Promise.resolve() + .then(entry.task) + .then(entry.resolve, entry.reject) + .finally(() => { + activeImageReads -= 1; + pumpImageReadQueue(); + }); } - 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); - }; + function enqueueImageRead(task) { + return new Promise((resolve, reject) => { + imageReadQueue.push({ task, resolve, reject }); + pumpImageReadQueue(); + }); + } - const refreshAccountMode = async (force = false) => { - if (accountModeInFlight) return accountMode; - const now = Date.now(); - if (!force && accountModeLastCheckedAt && now - accountModeLastCheckedAt < 10_000) { - return accountMode; + function parseImageTarget(inner) { + const value = inner.trim(); + if (!value) return null; + if (value.startsWith("<")) { + const close = value.indexOf(">"); + if (close <= 1) return null; + const target = value.slice(1, close).trim(); + const remainder = value.slice(close + 1).trim(); + const title = remainder.match(/^(?:"([^"]*)"|'([^']*)'|\(([^)]*)\))$/); + return { + target, + title: title ? title[1] ?? title[2] ?? title[3] ?? "" : "", + }; } - 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"; + const titled = value.match( + /^(.*?)(?:\s+(?:"([^"]*)"|'([^']*)'|\(([^)]*)\)))\s*$/, + ); + if (!titled) return { target: value, title: "" }; + return { + target: titled[1].trim(), + title: titled[2] ?? titled[3] ?? titled[4] ?? "", + }; + } + + function parseMarkdownImages(text) { + const masked = maskCode(text); + const images = []; + for (let start = 0; start < masked.length - 4; start += 1) { + if ( + masked[start] !== "!" || + masked[start + 1] !== "[" || + escapedAt(masked, start) + ) { + continue; } - 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"; + let bracketDepth = 1; + let bracketClose = -1; + for (let index = start + 2; index < masked.length; index += 1) { + if (escapedAt(masked, index)) continue; + if (masked[index] === "[") bracketDepth += 1; + if (masked[index] === "]") { + bracketDepth -= 1; + if (!bracketDepth) { + bracketClose = index; + break; + } } + if (masked[index] === "\n" || masked[index] === "\r") break; } - 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; + if (bracketClose < 0 || masked[bracketClose + 1] !== "(") continue; + + let parenthesisDepth = 1; + let quote = null; + let parenthesisClose = -1; + for (let index = bracketClose + 2; index < masked.length; index += 1) { + const character = masked[index]; + if (escapedAt(masked, index)) continue; + if (quote) { + if (character === quote) quote = null; + continue; + } + if (character === '"' || character === "'") { + quote = character; + continue; + } + if (character === "(") parenthesisDepth += 1; + if (character === ")") { + parenthesisDepth -= 1; + if (!parenthesisDepth) { + parenthesisClose = index; + break; + } } - ensureMounted(true); + if (character === "\n" || character === "\r") break; } - if (!accountModeLogged && accountMode !== "unknown") { - accountModeLogged = true; - log("account mode", accountMode); + if (parenthesisClose < 0) continue; + + const parsed = parseImageTarget( + text.slice(bracketClose + 2, parenthesisClose), + ); + if (!parsed?.target) continue; + images.push({ + start, + end: parenthesisClose + 1, + alt: text.slice(start + 2, bracketClose), + target: parsed.target, + title: parsed.title, + source: text.slice(start, parenthesisClose + 1), + }); + start = parenthesisClose; + } + return images; + } + + function normalizeFilePath(path, separator) { + let value = path; + let prefix = ""; + if (/^[A-Za-z]:[\\/]/.test(value)) { + prefix = `${value.slice(0, 2)}${separator}`; + value = value.slice(3); + } else if (/^[\\/]{2}/.test(value)) { + prefix = separator.repeat(2); + value = value.replace(/^[\\/]+/, ""); + } else if (/^[\\/]/.test(value)) { + prefix = separator; + value = value.replace(/^[\\/]+/, ""); + } + const parts = []; + for (const part of value.split(/[\\/]+/)) { + if (!part || part === ".") continue; + if (part === "..") { + if (parts.length && parts[parts.length - 1] !== "..") parts.pop(); + else if (!prefix) parts.push(part); + continue; } - return accountMode; - } catch (e) { - return accountMode; - } finally { - accountModeInFlight = false; + parts.push(part); } - }; + return `${prefix}${parts.join(separator)}`; + } - const remainingPercent = (usedPercent) => { - const used = Number(usedPercent); - if (!Number.isFinite(used)) return null; - return Math.round(Math.min(Math.max(100 - used, 0), 100)); - }; + function resolveImageTarget(target, filePath) { + let reference = target.trim(); + if (!reference) return null; + if (/^(?:data|blob):/i.test(reference)) return reference; + if (/^https?:\/\//i.test(reference)) return reference; + if (/^file:\/\//i.test(reference)) { + try { + const url = new URL(reference); + reference = decodeURIComponent(url.pathname); + if (/^\/[A-Za-z]:\//.test(reference)) reference = reference.slice(1); + } catch { + return null; + } + } else { + try { + reference = decodeURIComponent(reference); + } catch { + // Keep the literal Markdown destination. + } + } - 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 windowsPath = /^[A-Za-z]:[\\/]/.test(filePath) + || filePath.includes("\\"); + const separator = windowsPath ? "\\" : "/"; + if ( + /^[A-Za-z]:[\\/]/.test(reference) || + /^[\\/]{2}/.test(reference) || + (!windowsPath && reference.startsWith("/")) + ) { + return normalizeFilePath(reference, separator); + } + const lastSlash = Math.max( + filePath.lastIndexOf("/"), + filePath.lastIndexOf("\\"), + ); + const directory = lastSlash >= 0 ? filePath.slice(0, lastSlash) : ""; + return normalizeFilePath( + directory ? `${directory}${separator}${reference}` : reference, + separator, + ); + } - 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; + function imageMimeType(target, provided) { + if (typeof provided === "string" && provided.startsWith("image/")) { + return provided; + } + const extension = target.split(/[?#]/)[0].match(/\.([A-Za-z0-9]+)$/)?.[1]?.toLowerCase(); return { - label, - pct, - resetAt: formatResetAt(window.reset_at, includeResetDay), - }; - }; + png: "image/png", + jpg: "image/jpeg", + jpeg: "image/jpeg", + gif: "image/gif", + webp: "image/webp", + svg: "image/svg+xml", + bmp: "image/bmp", + ico: "image/x-icon", + avif: "image/avif", + }[extension] || null; + } - 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); + function codexLocalMediaUrl(path) { + if ( + !/^[A-Za-z]:[\\/]/.test(path) && + !/^[\\/]{2}/.test(path) && + !path.startsWith("/") + ) { + return null; + } + const normalized = path.replace(/\\/g, "/"); + const encoded = encodeURI(normalized) + .replaceAll("#", "%23") + .replaceAll("?", "%3F"); + return `app://fs/@fs${encoded}`; + } + + function loadImageSource(target, filePath, hostId) { + const resolved = resolveImageTarget(target, filePath); + if (!resolved) return Promise.reject(new Error("图片路径为空")); + if (/^(?:data|https?):/i.test(resolved)) return Promise.resolve(resolved); + if (!hostId || hostId === "local") { + const localMediaUrl = codexLocalMediaUrl(resolved); + if (localMediaUrl) return Promise.resolve(localMediaUrl); + } + const key = `${hostId || "local"}\n${resolved}`; + const cached = imageCache.get(key); + if (cached) { + imageCache.delete(key); + imageCache.set(key, cached); + return cached.promise; + } + const entry = { promise: null, bytes: 0 }; + entry.promise = enqueueImageRead(() => requestDesktopJson("read-file-binary", { + hostId: hostId || "local", + path: resolved, + maxBytes: IMAGE_MAX_BYTES, + }, 60_000)) + .then((result) => { + if (disposed) throw new Error("图片预览已停止"); + if (!result?.contentsBase64) { + if (/^https?:\/\//i.test(resolved)) return resolved; + throw new Error("图片不存在、格式不受支持或超过 20 MB"); + } + const mimeType = imageMimeType(resolved, result.mimeType); + if (!mimeType) throw new Error("文件不是支持的图片格式"); + if (imageCache.get(key) === entry) { + entry.bytes = Math.ceil(result.contentsBase64.length * 0.75); + imageCacheBytes += entry.bytes; + while (imageCacheBytes > IMAGE_CACHE_MAX_BYTES && imageCache.size > 1) { + const oldestKey = imageCache.keys().next().value; + const oldest = imageCache.get(oldestKey); + imageCache.delete(oldestKey); + imageCacheBytes = Math.max(0, imageCacheBytes - (oldest?.bytes || 0)); + } + } + return `data:${mimeType};base64,${result.contentsBase64}`; + }) + .catch((error) => { + if (imageCache.get(key) === entry) { + imageCache.delete(key); + imageCacheBytes = Math.max(0, imageCacheBytes - entry.bytes); + } + throw error; + }); + imageCache.set(key, entry); + return entry.promise; + } + + function currentMainModuleUrl() { + if (mainModuleUrl) return mainModuleUrl; + const scripts = Array.from(document.scripts); + const script = scripts.find((item) => /\/app-initial-[^/]+\.js(?:$|[?#])/.test(item.src)); + if (script?.src) { + mainModuleUrl = script.src; + return mainModuleUrl; + } + const preload = Array.from(document.querySelectorAll('link[rel="modulepreload"][href]')) + .find((item) => /\/app-initial-[^/]+\.js(?:$|[?#])/.test(item.href)); + if (preload?.href) { + mainModuleUrl = preload.href; + return mainModuleUrl; + } + const resource = performance + .getEntriesByType("resource") + .map((entry) => entry.name) + .find((url) => /\/app-initial-[^/]+\.js(?:$|[?#])/.test(url)); + mainModuleUrl = resource || null; + return mainModuleUrl; + } + + async function discoverKatexUrl() { + const loaded = performance + .getEntriesByType("resource") + .map((entry) => entry.name) + .find((url) => /\/katex-[^/]+\.js(?:$|[?#])/.test(url)); + if (loaded) return loaded; + + const mainUrl = currentMainModuleUrl(); + if (!mainUrl) throw new Error("Codex main renderer module was not found"); + const response = await fetch(mainUrl); + if (!response.ok) throw new Error(`Could not inspect Codex renderer (${response.status})`); + const source = await response.text(); + const match = source.match(/import\((?:`|"|')\.\/(katex-[^"'`]+\.js)(?:`|"|')\)/); + if (!match) throw new Error("Codex native KaTeX chunk was not found"); + return new URL(match[1], mainUrl).href; + } + + function loadNativeKatex() { + if (katexPromise) return katexPromise; + if (typeof window.katex?.renderToString === "function") { + katexPromise = Promise.resolve(window.katex); + return katexPromise; + } + katexPromise = discoverKatexUrl() + .then((url) => import(url)) + .then((module) => module?.default || module) + .then((katex) => { + if (typeof katex?.renderToString !== "function") { + throw new Error("Codex KaTeX module has no renderToString export"); + } + return katex; + }) + .catch((error) => { + lastError = String(error?.message || error); + katexPromise = null; + throw error; + }); + return katexPromise; + } + + function markdownFileNameFor(root) { + let current = root; + for (let depth = 0; current && depth < 8; depth += 1, current = current.parentElement) { + const nav = Array.from(current.children || []).find( + (child) => child instanceof HTMLElement && child.tagName === "NAV", + ) || current.querySelector(":scope > nav"); + const lastCrumb = nav?.querySelector("ol > li:last-child"); + const fileName = (lastCrumb?.textContent || "").replace(/\s+/g, " ").trim(); + if (fileName) return fileName; + } + return ""; + } + + function findPreviewEditors() { + const editors = []; + for (const surface of document.querySelectorAll("[data-editor-search-surface]")) { + const editor = surface.querySelector(":scope > .cm-editor, .cm-editor"); + if (!(editor instanceof HTMLElement)) continue; + const fileName = markdownFileNameFor(surface); + if (!MARKDOWN_EXTENSION.test(fileName)) continue; + editors.push(editor); + } + return editors; + } + + function controllerFromValue(value, editor) { + if (!value || (typeof value !== "object" && typeof value !== "function")) return null; + if (value.editorView?.dom === editor) return value; + if (value.current?.editorView?.dom === editor) return value.current; + return null; + } + + function findEditorController(editor) { + const fibers = []; + let node = editor; + for (let depth = 0; node && depth < 8; depth += 1, node = node.parentElement) { + for (const key of Object.getOwnPropertyNames(node)) { + if (key.startsWith("__reactFiber$")) fibers.push(node[key]); + } + } + + const visited = new Set(); + for (const initialFiber of fibers) { + for (let fiber = initialFiber, depth = 0; fiber && depth < 24; fiber = fiber.return, depth += 1) { + if (visited.has(fiber)) continue; + visited.add(fiber); + let hook = fiber.memoizedState; + for (let index = 0; hook && index < 120; hook = hook.next, index += 1) { + const direct = controllerFromValue(hook.memoizedState, editor) + || controllerFromValue(hook.baseState, editor); + if (direct) return direct; + const memo = hook.memoizedState; + if (Array.isArray(memo)) { + for (const item of memo) { + const nested = controllerFromValue(item, editor); + if (nested) return nested; + } + } + } + } + } + return null; + } + + function decorationClassFromValue(value) { + let constructor = value?.constructor; + for (let depth = 0; constructor && depth < 6; depth += 1) { if ( - !best || - distance < bestDistance || - (distance === bestDistance && - minutes > Number(best.limit_window_seconds) / 60) + typeof constructor.replace === "function" && + typeof constructor.set === "function" ) { - best = window; - bestDistance = distance; + return constructor; } + constructor = Object.getPrototypeOf(constructor); } - return best; - }; + return null; + } - 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); - }; + function discoverDecorationClass(view) { + const sets = [ + ...(Array.isArray(view.viewState?.stateDeco) ? view.viewState.stateDeco : []), + ...view.plugins.map((wrapper) => wrapper?.value?.decorations).filter(Boolean), + ]; + let found = null; + const end = view.state.doc.length; + for (const set of sets) { + if (typeof set?.between !== "function") continue; + try { + set.between(0, end, (_from, _to, value) => { + found ||= decorationClassFromValue(value); + }); + } catch { + // Continue with the other decoration providers. + } + if (found) return found; + } + return null; + } - pushLimit(status?.rate_limit); - if (Array.isArray(status?.additional_rate_limits)) { - for (const item of status.additional_rate_limits) { - pushLimit(item?.rate_limit); + function extensionValues(root) { + const values = []; + const queue = [root]; + const visited = new Set(); + while (queue.length && visited.size < 5000) { + const value = queue.shift(); + if ( + value == null || + (typeof value !== "object" && typeof value !== "function") || + visited.has(value) + ) { + continue; + } + visited.add(value); + values.push(value); + if (Array.isArray(value)) { + queue.push(...value); + continue; } + if (value.inner && value.inner !== value) queue.push(value.inner); + if (value.extension && value.extension !== value) queue.push(value.extension); + if (Array.isArray(value.baseExtensions)) queue.push(...value.baseExtensions); } + return values; + } - const five = pickClosestWindow( - limits, - 300, - (minutes) => minutes > 0 && minutes < 1440, - ); - const weekly = pickClosestWindow( - limits, - 7 * 24 * 60, - (minutes) => minutes >= 1440, - ); + function discoverStateFieldClass(view) { + for (const value of extensionValues(view.state.config?.base)) { + const StateField = value?.constructor; + if ( + typeof StateField?.define === "function" && + typeof value?.create === "function" && + typeof value?.slot === "function" && + typeof value?.init === "function" && + value.extension === value + ) { + return StateField; + } + } + return null; + } - return { - fiveHour: normalizeUsageWindow(five, "5h"), - weekly: normalizeUsageWindow(weekly, "Weekly"), - }; - }; + function discoverDecorationsFacet(view) { + for (const wrapper of view.plugins) { + const decorationSet = wrapper?.value?.decorations; + if (!decorationSet) continue; + for (const extension of extensionValues(wrapper.plugin?.baseExtensions)) { + if (!extension?.facet || typeof extension.value !== "function") continue; + try { + if (extension.value(view) === decorationSet) return extension.facet; + } catch { + // This provider belongs to another view-plugin capability. + } + } + } + return null; + } - const collectUsageWindows = (value, out = [], seen = new WeakSet()) => { - if (!value || typeof value !== "object") return out; - if (seen.has(value)) return out; - seen.add(value); + function discoverCodeMirrorRuntime(controller) { + const view = controller?.editorView; + if (!view?.state?.doc || !view.dom) return null; + const Decoration = discoverDecorationClass(view); + const StateField = discoverStateFieldClass(view); + const DecorationsFacet = discoverDecorationsFacet(view); + const existingCompartment = controller.readOnlyCompartment + || controller.selectionEditCompartment + || Array.from(view.state.config?.compartments?.keys?.() || [])[0]; + if (!Decoration || !StateField || !DecorationsFacet || !existingCompartment) return null; + + const Compartment = existingCompartment.constructor; + const StateEffect = existingCompartment.reconfigure([]).constructor; if ( - "used_percent" in value && - "limit_window_seconds" in value && - "reset_at" in value + typeof Compartment !== "function" || + typeof StateEffect?.appendConfig?.of !== "function" ) { - 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 null; } - 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"), + view, + Decoration, + StateField, + DecorationsFacet, + Compartment, + StateEffect, }; - }; + } - 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; - }; + function formulaRange(formula, state) { + if (!formula.display) return { from: formula.start, to: formula.end, block: false }; + const firstLine = state.doc.lineAt(formula.start); + const lastLine = state.doc.lineAt(Math.max(formula.start, formula.end - 1)); + const before = state.sliceDoc(firstLine.from, formula.start); + const after = state.sliceDoc(formula.end, lastLine.to); + if (!before.trim() && !after.trim()) { + return { from: firstLine.from, to: lastLine.to, block: true }; + } + return { from: formula.start, to: formula.end, block: false }; + } - const onUsageMessage = (event) => { - const data = event.data; - if (!data || typeof data !== "object") return; - applyUsageEvent(data); - }; + function imageRange(image, state) { + const firstLine = state.doc.lineAt(image.start); + const lastLine = state.doc.lineAt(Math.max(image.start, image.end - 1)); + const before = state.sliceDoc(firstLine.from, image.start); + const after = state.sliceDoc(image.end, lastLine.to); + if (!before.trim() && !after.trim()) { + return { from: firstLine.from, to: lastLine.to, block: true }; + } + return { from: image.start, to: image.end, block: false }; + } - 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; + function splitTableRow(lineText, lineFrom = 0) { + let contentFrom = 0; + let contentTo = lineText.length; + while (contentFrom < contentTo && /\s/.test(lineText[contentFrom])) { + contentFrom += 1; } - 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; + while (contentTo > contentFrom && /\s/.test(lineText[contentTo - 1])) { + contentTo -= 1; + } + if (!lineText.slice(contentFrom, contentTo).includes("|")) return null; + if (lineText[contentFrom] === "|") contentFrom += 1; + if ( + lineText[contentTo - 1] === "|" && + !escapedAt(lineText, contentTo - 1) + ) { + contentTo -= 1; + } + + const cells = []; + let start = contentFrom; + let codeTicks = 0; + let mathDelimiter = null; + const pushCell = (from, to) => { + while (from < to && /\s/.test(lineText[from])) from += 1; + while (to > from && /\s/.test(lineText[to - 1])) to -= 1; + cells.push({ + text: lineText.slice(from, to), + from: lineFrom + from, + to: lineFrom + to, + }); + }; + for (let i = contentFrom; i < contentTo; i += 1) { + if (lineText[i] === "`" && !escapedAt(lineText, i)) { + let ticks = 1; + while (lineText[i + ticks] === "`") ticks += 1; + if (!codeTicks) codeTicks = ticks; + else if (codeTicks === ticks) codeTicks = 0; + i += ticks - 1; + continue; } - return false; - } catch (e) { - if (!directUsageFailureLogged) { - directUsageFailureLogged = true; - api.log.warn("[usage] /wham/usage unavailable; falling back to DOM", e); + if (codeTicks) continue; + if (lineText[i] === "$" && !escapedAt(lineText, i)) { + const delimiter = lineText[i + 1] === "$" ? "$$" : "$"; + if (!mathDelimiter) mathDelimiter = delimiter; + else if (mathDelimiter === delimiter) mathDelimiter = null; + i += delimiter.length - 1; + continue; + } + if (lineText[i] === "|" && !mathDelimiter && !escapedAt(lineText, i)) { + pushCell(start, i); + start = i + 1; } - return false; - } finally { - directUsageInFlight = false; } - }; + pushCell(start, contentTo); + return cells.length >= 2 ? cells : null; + } - /** - * 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(); + function delimiterAlignment(cell) { + const value = cell.text.trim(); + if (!/^:?-{3,}:?$/.test(value)) return null; + const left = value.startsWith(":"); + const right = value.endsWith(":"); + return left && right ? "center" : right ? "right" : left ? "left" : ""; + } + + function parseMathTables(state) { + const tables = []; + for (let lineNumber = 1; lineNumber < state.doc.lines; lineNumber += 1) { + const headerLine = state.doc.line(lineNumber); + const delimiterLine = state.doc.line(lineNumber + 1); + const header = splitTableRow(headerLine.text, headerLine.from); + const delimiter = splitTableRow(delimiterLine.text, delimiterLine.from); if ( - (txt.includes("5h") || txt.includes("hourly")) && - txt.includes("week") - ) - return g; + !header || + !delimiter || + header.length !== delimiter.length + ) { + continue; + } + const alignments = delimiter.map(delimiterAlignment); + if (alignments.some((alignment) => alignment == null)) continue; + + const rows = [header]; + let lastLine = delimiterLine; + let nextLineNumber = lineNumber + 2; + while (nextLineNumber <= state.doc.lines) { + const line = state.doc.line(nextLineNumber); + const cells = splitTableRow(line.text, line.from); + if (!cells || cells.length !== header.length) break; + rows.push(cells); + lastLine = line; + nextLineNumber += 1; + } + + const from = headerLine.from; + const to = lastLine.to; + const source = state.sliceDoc(from, to); + if (parseMath(source).length) { + tables.push({ + from, + to, + source, + rows, + alignments, + }); + } + lineNumber = nextLineNumber - 1; } - return null; - }; + return tables; + } - /** - * 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 }; - }; + function selectionTouches(range, state) { + return state.selection.ranges.some((selection) => ( + selection.empty + ? selection.from >= range.from && selection.from <= range.to + : selection.from < range.to && selection.to > range.from + )); + } - const parseValue = (span) => { - const txt = (span.textContent || "").replace(/\s+/g, " ").trim(); - return parseValueText(txt, span); - }; + function createFormulaWidgetClass(katex) { + return class FormulaWidget { + constructor(content, display, block, source, editFrom, editTo) { + this.content = content; + this.display = display; + this.block = block; + this.source = source; + this.editFrom = editFrom; + this.editTo = editTo; + } - 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 }; + eq(other) { + return ( + other instanceof this.constructor && + other.content === this.content && + other.display === this.display && + other.block === this.block && + other.source === this.source && + other.editFrom === this.editFrom && + other.editTo === this.editTo + ); } - } - 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; + updateDOM() { + return false; + } - 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 } }; - }; + compare(other) { + return this === other || ( + this.constructor === other?.constructor && + this.eq(other) + ); + } - 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"); - } - }; + get estimatedHeight() { + if (!this.block) return -1; + const contentLines = this.content + .split(/\r\n|\n|\r/) + .filter((line) => line.trim()).length; + return Math.max(40, contentLines * 18 + 16); + } - // ── 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(); + get lineBreaks() { + return 0; + } - 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; - }; + ignoreEvent() { + return true; + } - 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); - }; + coordsAt() { + return null; + } - const quickControlText = (node) => - [ - node.getAttribute?.("aria-label"), - node.getAttribute?.("title"), - node.textContent, - ] - .filter(Boolean) - .join(" ") - .replace(/\s+/g, " ") - .trim() - .toLowerCase(); + get isHidden() { + return false; + } - const isSidebarGeometry = (node) => { - if (!(node instanceof HTMLElement) || !isVisibleElement(node)) return false; - const rect = node.getBoundingClientRect(); - const viewportHeight = window.innerHeight || document.documentElement.clientHeight || 0; - const viewportWidth = window.innerWidth || document.documentElement.clientWidth || 0; - return ( - rect.width >= 180 && - rect.width <= Math.min(520, Math.max(320, viewportWidth * 0.55)) && - rect.height >= Math.max(360, viewportHeight * 0.45) && - rect.left <= Math.max(96, viewportWidth * 0.12) && - rect.top <= Math.max(96, viewportHeight * 0.18) - ); - }; + get editable() { + return false; + } - const hasBottomControl = (sidebar) => { - const sidebarRect = sidebar.getBoundingClientRect(); - const controls = Array.from(sidebar.querySelectorAll('button, a, [role="button"]')); - return controls.some((control) => { - if (!(control instanceof HTMLElement) || !isVisibleElement(control)) return false; - const rect = control.getBoundingClientRect(); - const text = quickControlText(control); - const nearBottom = rect.bottom >= sidebarRect.bottom - 260; - const compact = rect.width > 0 && rect.width <= 64 && rect.height > 0 && rect.height <= 64; - return nearBottom && (compact || /\bmobile\b|\bphone\b|\bdevice\b|\bsettings?\b|手机|移动|设备|连接|设置/.test(text)); - }); - }; + destroy() {} - const addSidebarAncestorsForBottomControls = (candidates) => { - const viewportHeight = window.innerHeight || document.documentElement.clientHeight || 0; - const controls = Array.from(document.querySelectorAll('button, a, [role="button"]')); - for (const control of controls) { - if (!(control instanceof HTMLElement) || !isVisibleElement(control)) continue; - const rect = control.getBoundingClientRect(); - if (rect.left > 560 || rect.bottom < viewportHeight - 280) continue; - const text = quickControlText(control); - const compact = rect.width > 0 && rect.width <= 64 && rect.height > 0 && rect.height <= 64; - if (!compact && !/\bmobile\b|\bphone\b|\bdevice\b|\bsettings?\b|手机|移动|设备|连接|设置/.test(text)) continue; - let node = control.parentElement; - while (node && node !== document.body) { - if (isSidebarGeometry(node) && !looksLikeSettingsSidebar(node)) candidates.add(node); - node = node.parentElement; + toDOM(view) { + const ownerDocument = view.dom.ownerDocument; + const element = ownerDocument.createElement(this.block ? "div" : "span"); + element.setAttribute( + FORMULA_ATTR, + this.block ? "display-block" : this.display ? "display-inline" : "inline", + ); + element.setAttribute("contenteditable", "false"); + element.setAttribute("role", "math"); + element.setAttribute("aria-label", this.content); + element.tabIndex = 0; + element.title = "单击编辑公式,Enter 或 Ctrl+Enter 提交,Esc 取消"; + + const renderFormula = () => { + element.removeAttribute(EDITING_ATTR); + element.replaceChildren(); + try { + element.innerHTML = katex.renderToString(this.content, { + displayMode: this.display, + strict: "ignore", + throwOnError: false, + }); + } catch { + element.textContent = this.content; + } + }; + const beginEdit = (event) => { + if (event?.button != null && event.button !== 0) return; + if (element.hasAttribute(EDITING_ATTR)) return; + event?.preventDefault(); + event?.stopPropagation(); + + const multiline = this.display || this.block || /[\r\n]/.test(this.source); + const editor = ownerDocument.createElement(multiline ? "textarea" : "input"); + if (!multiline) editor.type = "text"; + editor.value = this.source; + editor.setAttribute(EDITOR_ATTR, ""); + editor.setAttribute("aria-label", "公式 LaTeX 源码"); + editor.spellcheck = false; + element.setAttribute(EDITING_ATTR, ""); + element.replaceChildren(editor); + + const resizeEditor = () => { + if (!(editor instanceof HTMLTextAreaElement)) return; + editor.style.height = "auto"; + editor.style.height = `${Math.max(editor.scrollHeight, 72)}px`; + view.requestMeasure?.(); + }; + let finished = false; + const finish = (commit) => { + if (finished) return; + finished = true; + const nextSource = editor.value; + if ( + commit && + nextSource !== this.source && + !view.destroyed + ) { + view.dispatch({ + changes: { + from: this.editFrom, + to: this.editTo, + insert: nextSource, + }, + }); + return; + } + renderFormula(); + }; + editor.addEventListener("mousedown", (inputEvent) => { + inputEvent.stopPropagation(); + }); + editor.addEventListener("input", resizeEditor); + editor.addEventListener("keydown", (inputEvent) => { + if (inputEvent.key === "Escape") { + inputEvent.preventDefault(); + finish(false); + return; + } + if ( + inputEvent.key === "Enter" && + (!multiline || inputEvent.ctrlKey || inputEvent.metaKey) + ) { + inputEvent.preventDefault(); + finish(true); + } + }); + editor.addEventListener("blur", () => finish(true), { once: true }); + ownerDocument.defaultView?.setTimeout(() => { + resizeEditor(); + editor.focus(); + editor.select(); + }, 0); + }; + element.addEventListener("mousedown", beginEdit); + element.addEventListener("keydown", (event) => { + if (event.key === "Enter" || event.key === " ") beginEdit(event); + }); + renderFormula(); + return element; } - } - }; + }; + } - const sidebarScore = (sidebar) => { - if (!isSidebarGeometry(sidebar) || looksLikeSettingsSidebar(sidebar)) return -Infinity; - const rect = sidebar.getBoundingClientRect(); - let score = 0; - if (rect.left <= 16) score += 4; - else if (rect.left <= 80) score += 2; - if (rect.width >= 220 && rect.width <= 420) score += 3; - if (rect.height >= (window.innerHeight || 0) * 0.75) score += 3; - if (looksLikeMainAppSidebar(sidebar)) score += 6; - if (hasBottomControl(sidebar)) score += 5; - if (/^(aside|nav)$/i.test(sidebar.tagName) || sidebar.getAttribute("role") === "navigation") score += 2; - return score; - }; + function createImageWidgetClass(context) { + return class ImageWidget { + constructor(image, range) { + this.alt = image.alt; + this.target = image.target; + this.title = image.title; + this.source = image.source; + this.editFrom = image.start; + this.editTo = image.end; + this.block = range.block; + this.filePath = context.filePath; + this.hostId = context.hostId; + } - const findUsageSidebar = () => { - const candidates = new Set( - Array.from(document.querySelectorAll(SIDEBAR_CANDIDATE_SELECTOR)) - .filter((node) => node instanceof HTMLElement), - ); - addSidebarAncestorsForBottomControls(candidates); - return Array.from(candidates) - .map((sidebar) => ({ sidebar, score: sidebarScore(sidebar) })) - .filter((item) => item.score > 0) - .sort((a, b) => b.score - a.score || a.sidebar.getBoundingClientRect().left - b.sidebar.getBoundingClientRect().left)[0]?.sidebar || null; - }; + eq(other) { + return ( + other instanceof this.constructor && + other.alt === this.alt && + other.target === this.target && + other.title === this.title && + other.source === this.source && + other.editFrom === this.editFrom && + other.editTo === this.editTo && + other.block === this.block && + other.filePath === this.filePath && + other.hostId === this.hostId + ); + } - const controlText = (node) => - [ - node.getAttribute?.("aria-label"), - node.getAttribute?.("title"), - node.textContent, - ] - .filter(Boolean) - .join(" ") - .replace(/\s+/g, " ") - .trim() - .toLowerCase(); + updateDOM() { + return false; + } - const controlLabelText = (node) => - [node.getAttribute?.("aria-label"), node.getAttribute?.("title")] - .filter(Boolean) - .join(" ") - .replace(/\s+/g, " ") - .trim() - .toLowerCase(); + compare(other) { + return this === other || ( + this.constructor === other?.constructor && + this.eq(other) + ); + } - const isDeviceButton = (button) => { - const label = controlLabelText(button); - if (/\bmobile\b|\bphone\b|\bdevice\b|手机|移动|设备|连接/.test(label)) return true; - const text = controlText(button); - return text.length <= 28 && /\bmobile\b|\bphone\b|\bdevice\b|手机|移动|设备|连接/.test(text); - }; + get estimatedHeight() { + return this.block ? 240 : -1; + } - const isSettingsButton = (button) => { - const text = controlText(button); - return /\bsettings?\b|preferences?|设置|偏好/.test(text); - }; - - const isNearSidebarBottom = (sidebar, node) => { - if (!(sidebar instanceof HTMLElement) || !(node instanceof HTMLElement)) return false; - const sidebarRect = sidebar.getBoundingClientRect(); - const rect = node.getBoundingClientRect(); - const bottomBand = Math.min(Math.max(sidebarRect.height * 0.22, 120), 240); - return rect.bottom >= sidebarRect.bottom - bottomBand; - }; - - const isCompactIconControl = (control) => { - const rect = control.getBoundingClientRect(); - const text = controlText(control); - return rect.width > 0 && rect.width <= 56 && rect.height > 0 && rect.height <= 56 && text.length <= 32; - }; - - const isUsageControlNode = (node) => - node.closest?.('[data-codexpp="usage-slot"], [data-codexpp="usage-box"], [data-codexpp="usage-boxes"]'); - - const rowControls = (row) => - Array.from(row.querySelectorAll('button, a, [role="button"]')) - .filter((control) => - control instanceof HTMLElement && - isVisibleElement(control) && - !isUsageControlNode(control), - ); + get lineBreaks() { + return 0; + } - const isSlotAfterRightmostControl = (slot) => { - const row = slot.parentElement; - if (!(row instanceof HTMLElement)) return false; - const controls = rowControls(row); - if (!controls.length) return true; - const slotRect = slot.getBoundingClientRect(); - const rightmostControl = Math.max(...controls.map((control) => control.getBoundingClientRect().right)); - return slotRect.right >= rightmostControl - 2; - }; + ignoreEvent() { + return true; + } - 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 nearBottom = isNearSidebarBottom(sidebar, row); - const looksLikeControlLayer = - insideSidebar && - nearBottom && - rect.height > 0 && - rect.height <= 88 && - (style.display === "flex" || style.display === "grid" || buttonCount >= 2); - if (looksLikeControlLayer) return row; - row = row.parentElement; - } - return null; - }; + coordsAt() { + return 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.nextSibling); - } - else row.appendChild(slot); - return slot; - }; + get isHidden() { + return false; + } - const findSidebarSlot = () => { - const sidebar = findUsageSidebar(); - if (!sidebar) return null; - for (const slot of sidebar.querySelectorAll('[data-codexpp="usage-slot"]')) { - const row = slot.parentElement; - if ( - !(slot instanceof HTMLElement) || - !(row instanceof HTMLElement) || - !isNearSidebarBottom(sidebar, row) || - !isSlotAfterRightmostControl(slot) - ) { - slot.remove(); + get editable() { + return false; } - } - const existingSlot = Array.from(sidebar.querySelectorAll('[data-codexpp="usage-slot"]')) - .find((slot) => - slot instanceof HTMLElement && - slot.parentElement instanceof HTMLElement && - isNearSidebarBottom(sidebar, slot.parentElement) && - isSlotAfterRightmostControl(slot), - ); - if (existingSlot instanceof HTMLElement) return existingSlot; - const controls = Array.from(sidebar.querySelectorAll('button, a, [role="button"]')) - .filter((button) => - button instanceof HTMLElement && - isVisibleElement(button) && - isNearSidebarBottom(sidebar, button) && - !isUsageControlNode(button), - ); - const deviceControls = controls.filter(isDeviceButton); - const settingsControls = controls.filter(isSettingsButton); - const compactControls = controls.filter(isCompactIconControl); - const preferredControls = deviceControls.length - ? deviceControls - : compactControls.length - ? compactControls - : settingsControls; - const ordered = (preferredControls.length ? preferredControls : controls).sort((a, b) => { - const ar = a.getBoundingClientRect(); - const br = b.getBoundingClientRect(); - return br.right - ar.right || br.bottom - ar.bottom; - }); + destroy(dom) { + if (!dom) return; + dom.__bennettImageActive = false; + dom.__bennettImageObserver?.disconnect(); + } - for (const button of ordered) { - const row = nearestControlRow(sidebar, button); - if (row) return createInlineSlot(row, button); - } + toDOM(view) { + const ownerDocument = view.dom.ownerDocument; + const element = ownerDocument.createElement(this.block ? "div" : "span"); + element.__bennettImageActive = true; + element.setAttribute(IMAGE_ATTR, this.block ? "block" : "inline"); + element.setAttribute("contenteditable", "false"); + element.setAttribute("role", "img"); + element.setAttribute("aria-label", this.alt || this.title || this.target); + element.tabIndex = 0; + element.title = "单击编辑图片语法,Enter 提交,Esc 取消"; + const loadingMessage = this.alt.trim() + ? `正在加载${this.alt.trim()}图片` + : "正在加载图片"; + + let imageObserver = null; + let loadingStarted = false; + + const renderStatus = (status, message, measure = true) => { + element.removeAttribute(EDITING_ATTR); + const statusElement = ownerDocument.createElement("span"); + statusElement.setAttribute(IMAGE_STATUS_ATTR, status); + statusElement.textContent = message; + element.replaceChildren(statusElement); + if (measure) view.requestMeasure?.(); + }; - return null; - }; + const renderImage = () => { + if (!element.__bennettImageActive) return; + renderStatus("loading", loadingMessage); + loadImageSource(this.target, this.filePath, this.hostId) + .then((source) => { + if ( + !element.__bennettImageActive || + element.hasAttribute(EDITING_ATTR) + ) { + return; + } + const image = ownerDocument.createElement("img"); + image.alt = this.alt; + if (this.title) image.title = this.title; + image.addEventListener("load", () => view.requestMeasure?.(), { + once: true, + }); + image.addEventListener("error", () => { + if (!element.__bennettImageActive) return; + renderStatus( + "error", + `无法显示图片:${this.alt || this.target}`, + ); + }, { once: true }); + image.src = source; + element.replaceChildren(image); + view.requestMeasure?.(); + }) + .catch((error) => { + if ( + !element.__bennettImageActive || + element.hasAttribute(EDITING_ATTR) + ) { + return; + } + const detail = String(error?.message || error || "").trim(); + renderStatus( + "error", + `无法加载图片:${this.alt || this.target}${detail ? `(${detail})` : ""}`, + ); + }); + }; - 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 beginEdit = (event) => { + if (event?.button != null && event.button !== 0) return; + if (element.hasAttribute(EDITING_ATTR)) return; + event?.preventDefault(); + event?.stopPropagation(); + imageObserver?.disconnect(); + + const editor = ownerDocument.createElement("input"); + editor.type = "text"; + editor.value = this.source; + editor.setAttribute(EDITOR_ATTR, ""); + editor.setAttribute("aria-label", "Markdown 图片语法"); + editor.spellcheck = false; + element.setAttribute(EDITING_ATTR, ""); + element.replaceChildren(editor); + + let finished = false; + const finish = (commit) => { + if (finished) return; + finished = true; + const nextSource = editor.value; + if ( + commit && + nextSource !== this.source && + !view.destroyed + ) { + view.dispatch({ + changes: { + from: this.editFrom, + to: this.editTo, + insert: nextSource, + }, + }); + return; + } + loadingStarted = true; + renderImage(); + }; + editor.addEventListener("mousedown", (inputEvent) => { + inputEvent.stopPropagation(); + }); + editor.addEventListener("keydown", (inputEvent) => { + if (inputEvent.key === "Escape") { + inputEvent.preventDefault(); + finish(false); + return; + } + if (inputEvent.key === "Enter") { + inputEvent.preventDefault(); + finish(true); + } + }); + editor.addEventListener("blur", () => finish(true), { once: true }); + ownerDocument.defaultView?.setTimeout(() => { + editor.focus(); + editor.select(); + }, 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; + element.addEventListener("mousedown", beginEdit); + element.addEventListener("keydown", (event) => { + if (event.key === "Enter" || event.key === " ") beginEdit(event); + }); + const startLoading = () => { + if (loadingStarted || !element.__bennettImageActive) return; + loadingStarted = true; + imageObserver?.disconnect(); + imageObserver = null; + element.__bennettImageObserver = null; + renderImage(); + }; + renderStatus( + "waiting", + loadingMessage, + false, + ); + const IntersectionObserverClass = ownerDocument.defaultView?.IntersectionObserver; + if (typeof IntersectionObserverClass === "function") { + imageObserver = new IntersectionObserverClass((entries) => { + if (entries.some((entry) => entry.isIntersecting)) startLoading(); + }, { + root: view.scrollDOM instanceof Element ? view.scrollDOM : null, + rootMargin: "400px 0px", + threshold: 0, + }); + element.__bennettImageObserver = imageObserver; + imageObserver.observe(element); + } else { + startLoading(); + } + return element; } - for (const stale of document.querySelectorAll( - '[data-codexpp="usage-box"], [data-codexpp="usage-boxes"]', - )) { - stale.remove(); + }; + } + + function appendMathContent(ownerDocument, parent, text, katex) { + const formulas = parseMath(text); + let offset = 0; + for (const formula of formulas) { + if (formula.start > offset) { + parent.appendChild(ownerDocument.createTextNode(text.slice(offset, formula.start))); } - if (!ensureMounted._warned) { - log("ensureMounted: no sidebar slot found yet"); - ensureMounted._warned = true; + const math = ownerDocument.createElement("span"); + math.setAttribute(FORMULA_ATTR, formula.display ? "display-inline" : "inline"); + math.setAttribute("role", "math"); + math.setAttribute("aria-label", formula.content); + try { + math.innerHTML = katex.renderToString(formula.content, { + displayMode: formula.display, + strict: "ignore", + throwOnError: false, + }); + } catch { + math.textContent = formula.content; } - return; + parent.appendChild(math); + offset = formula.end; } - - // 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 (offset < text.length) { + parent.appendChild(ownerDocument.createTextNode(text.slice(offset))); } + } - if (mounted && slot.contains(mounted) && !forceRebuild) { - mounted._refresh?.(visibleSnapshot); - return; + function closingMarker(text, marker, from) { + let index = text.indexOf(marker, from); + while (index >= 0) { + if (!escapedAt(text, index)) return index; + index = text.indexOf(marker, index + marker.length); } - 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"; + return -1; + } + + function appendTableCellContent(ownerDocument, cell, text, katex) { + let offset = 0; + while (offset < text.length) { + const candidates = [ + { marker: "**", tag: "strong" }, + { marker: "__", tag: "strong" }, + { marker: "~~", tag: "del" }, + { marker: "`", tag: "code" }, + ] + .map((candidate) => ({ + ...candidate, + index: text.indexOf(candidate.marker, offset), + })) + .filter((candidate) => ( + candidate.index >= 0 && !escapedAt(text, candidate.index) + )) + .sort((left, right) => left.index - right.index); + const token = candidates[0]; + if (!token) { + appendMathContent(ownerDocument, cell, text.slice(offset), katex); + return; + } + if (token.index > offset) { + appendMathContent( + ownerDocument, + cell, + text.slice(offset, token.index), + katex, + ); + } + const close = closingMarker( + text, + token.marker, + token.index + token.marker.length, + ); + if (close < 0) { + appendMathContent(ownerDocument, cell, text.slice(token.index), katex); + return; + } + const node = ownerDocument.createElement(token.tag); + const content = text.slice(token.index + token.marker.length, close); + if (token.tag === "code") { + node.textContent = content; + } else { + appendTableCellContent(ownerDocument, node, content, katex); + } + cell.appendChild(node); + offset = close + token.marker.length; } - 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); + function createMathTableWidgetClass(katex) { + return class MathTableWidget { + constructor(table) { + this.table = table; + } - // ── 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(); + eq(other) { + return ( + other instanceof this.constructor && + other.table.source === this.table.source + ); } - 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); + updateDOM() { + return false; + } - log("active", { snapshot }); + compare(other) { + return this === other || ( + this.constructor === other?.constructor && + this.eq(other) + ); + } - 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(); - } - }; - }, + get estimatedHeight() { + return Math.max(72, this.table.rows.length * 38 + 12); + } - /** - * 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(); + get lineBreaks() { + return 0; + } - 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; + ignoreEvent() { + return true; + } + + coordsAt() { + return null; + } + + get isHidden() { + return false; + } + + get editable() { + return false; + } + + destroy() {} + + toDOM(view) { + const ownerDocument = view.dom.ownerDocument; + const wrapper = ownerDocument.createElement("div"); + wrapper.setAttribute(TABLE_ATTR, ""); + wrapper.setAttribute("contenteditable", "false"); + + const tableElement = ownerDocument.createElement("table"); + const head = ownerDocument.createElement("thead"); + const body = ownerDocument.createElement("tbody"); + const headRow = ownerDocument.createElement("tr"); + const renderCell = (cell, cellData) => { + cell.setAttribute(CELL_ATTR, ""); + cell.setAttribute("contenteditable", "false"); + cell.setAttribute("aria-label", cellData.text || "空单元格"); + cell.tabIndex = 0; + cell.title = "单击编辑此单元格,Enter 提交,Esc 取消"; + appendTableCellContent(ownerDocument, cell, cellData.text, katex); + + const beginEdit = (event) => { + if (event?.button != null && event.button !== 0) return; + if (cell.hasAttribute(EDITING_ATTR)) return; + event?.preventDefault(); + event?.stopPropagation(); + + const cellStyle = ownerDocument.defaultView?.getComputedStyle(cell); + const horizontalPadding = cellStyle + ? (Number.parseFloat(cellStyle.paddingLeft) || 0) + + (Number.parseFloat(cellStyle.paddingRight) || 0) + : 0; + const contentWidth = Math.max( + 1, + Math.floor(cell.clientWidth - horizontalPadding), + ); + const editor = ownerDocument.createElement("input"); + editor.type = "text"; + editor.value = cellData.text; + editor.setAttribute(EDITOR_ATTR, ""); + editor.setAttribute("aria-label", "Markdown 表格单元格源码"); + editor.spellcheck = false; + editor.style.width = `${contentWidth}px`; + editor.style.maxWidth = `${contentWidth}px`; + editor.style.minWidth = "0"; + cell.setAttribute(EDITING_ATTR, ""); + cell.replaceChildren(editor); + + let finished = false; + const restore = () => { + cell.removeAttribute(EDITING_ATTR); + cell.replaceChildren(); + appendTableCellContent(ownerDocument, cell, cellData.text, katex); + }; + const finish = (commit) => { + if (finished) return; + finished = true; + const nextText = editor.value; + if ( + commit && + nextText !== cellData.text && + !view.destroyed + ) { + view.dispatch({ + changes: { + from: cellData.from, + to: cellData.to, + insert: nextText, + }, + }); + return; + } + restore(); + }; + editor.addEventListener("mousedown", (inputEvent) => { + inputEvent.stopPropagation(); + }); + editor.addEventListener("keydown", (inputEvent) => { + if (inputEvent.key === "Escape") { + inputEvent.preventDefault(); + finish(false); + return; + } + if (inputEvent.key === "Enter") { + inputEvent.preventDefault(); + finish(true); + } + }); + editor.addEventListener("blur", () => finish(true), { once: true }); + ownerDocument.defaultView?.setTimeout(() => { + editor.focus(); + editor.select(); + }, 0); + }; + cell.addEventListener("mousedown", beginEdit); + cell.addEventListener("keydown", (event) => { + if (event.key === "Enter" || event.key === " ") beginEdit(event); + }); + }; + + this.table.rows[0].forEach((cellData, index) => { + const cell = ownerDocument.createElement("th"); + cell.scope = "col"; + if (this.table.alignments[index]) { + cell.style.textAlign = this.table.alignments[index]; + } + renderCell(cell, cellData); + headRow.appendChild(cell); + }); + head.appendChild(headRow); + + for (const row of this.table.rows.slice(1)) { + const rowElement = ownerDocument.createElement("tr"); + row.forEach((cellData, index) => { + const cell = ownerDocument.createElement("td"); + if (this.table.alignments[index]) { + cell.style.textAlign = this.table.alignments[index]; + } + renderCell(cell, cellData); + rowElement.appendChild(cell); + }); + body.appendChild(rowElement); + } + + tableElement.append(head, body); + wrapper.appendChild(tableElement); + return wrapper; + } + }; + } + + function createMathExtension(runtime, katex, context) { + const { Decoration, StateField, DecorationsFacet } = runtime; + const FormulaWidget = createFormulaWidgetClass(katex); + const ImageWidget = createImageWidgetClass(context); + const MathTableWidget = createMathTableWidgetClass(katex); + + function buildDecorations(state) { + const source = state.doc.toString(); + const ranges = []; + const mathTables = parseMathTables(state); + const images = parseMarkdownImages(source); + const formulas = parseMath(source); + for (const table of mathTables) { + ranges.push( + Decoration.replace({ + widget: new MathTableWidget(table), + block: true, + }).range(table.from, table.to), + ); + } + for (const image of images) { + if ( + mathTables.some((table) => ( + image.start >= table.from && image.end <= table.to + )) + ) { + continue; + } + const range = imageRange(image, state); + const widget = new ImageWidget(image, range); + let decoration; + try { + decoration = Decoration.replace({ + widget, + block: range.block, + }).range(range.from, range.to); + } catch { + decoration = Decoration.replace({ widget }).range(image.start, image.end); + } + ranges.push(decoration); + } + for (const formula of formulas) { + if ( + mathTables.some((table) => ( + formula.start >= table.from && formula.end <= table.to + )) || + images.some((image) => ( + formula.start < image.end && formula.end > image.start + )) + ) { + continue; + } + const range = formulaRange(formula, state); + if (selectionTouches(range, state)) continue; + const widget = new FormulaWidget( + formula.content, + formula.display, + range.block, + state.sliceDoc(formula.start, formula.end), + formula.start, + formula.end, + ); + let decoration; + try { + decoration = Decoration.replace({ + widget, + block: range.block, + }).range(range.from, range.to); + } catch { + decoration = Decoration.replace({ widget }).range(formula.start, formula.end); + } + ranges.push(decoration); + } + return Decoration.set(ranges, true); } - `; - document.head.appendChild(style); + + return StateField.define({ + create(state) { + return buildDecorations(state); + }, + update(decorations, transaction) { + if (transaction.docChanged || transaction.selection) { + return buildDecorations(transaction.state); + } + return decorations; + }, + provide(field) { + return DecorationsFacet.from(field); + } + }); + } + + function removeState(state) { + states.delete(state.editor); + if (!state.view.destroyed) { + try { + state.view.dispatch({ + effects: state.compartment.reconfigure([]), + }); + } catch (error) { + api.log.warn("Could not remove Markdown preview math extension", error); + } + } + } + + async function installForEditor(editor, katex) { + if (states.has(editor) || !editor.isConnected || disposed) return; + const controller = findEditorController(editor); + const runtime = discoverCodeMirrorRuntime(controller); + if (!runtime) return; + + const compartment = new runtime.Compartment(); + const extension = createMathExtension(runtime, katex, { + filePath: typeof controller.filePath === "string" + ? controller.filePath + : markdownFileNameFor(editor), + hostId: typeof controller.hostId === "string" && controller.hostId + ? controller.hostId + : "local", + }); + runtime.view.dispatch({ + effects: runtime.StateEffect.appendConfig.of(compartment.of(extension)), + }); + states.set(editor, { + editor, + view: runtime.view, + compartment, + }); + } + + async function scanEditors() { + scanFrame = 0; + if (disposed) return; + if (scanning) { + scanRequested = true; + return; + } + scanning = true; + scanRequested = false; + try { + const editors = findPreviewEditors(); + const liveEditors = new Set(editors); + for (const state of Array.from(states.values())) { + if ( + !liveEditors.has(state.editor) || + !state.editor.isConnected || + state.view.destroyed + ) { + removeState(state); + } + } + + let katex; + try { + katex = await loadNativeKatex(); + } catch (error) { + api.log.warn("Markdown preview math unavailable", error); + return; + } + for (const editor of editors) await installForEditor(editor, katex); + } finally { + scanning = false; + if (scanRequested && !disposed) scheduleScan(); + } + } + + function scheduleScan() { + if (disposed || scanFrame) return; + scanFrame = requestAnimationFrame(() => void scanEditors()); + } + + const observer = new MutationObserver(scheduleScan); + observer.observe(document.documentElement, { + childList: true, + subtree: true, + }); + + window.__bennettMarkdownPreviewMath = { + getStats() { + return { + enabled: !disposed, + previewEditors: states.size, + renderedFormulas: Array.from(states.values()).reduce( + (count, state) => ( + count + state.editor.querySelectorAll(`[${FORMULA_ATTR}]`).length + ), + 0, + ), + renderedImages: Array.from(states.values()).reduce( + (count, state) => ( + count + state.editor.querySelectorAll(`[${IMAGE_ATTR}] img`).length + ), + 0, + ), + cachedImages: imageCache.size, + cachedImageBytes: imageCacheBytes, + queuedImageReads: imageReadQueue.length, + activeImageReads, + nativeKatexLoaded: !!katexPromise && !lastError, + implementation: "CodeMirror formula, table, and image replacement widgets", + lastError, + scope: "right-side Markdown file preview only", + }; + }, + refresh: scheduleScan, + }; + scheduleScan(); return () => { + disposed = true; + if (scanFrame) cancelAnimationFrame(scanFrame); + observer.disconnect(); + for (const state of Array.from(states.values())) removeState(state); + while (imageReadQueue.length) { + imageReadQueue.shift().reject(new Error("图片预览已停止")); + } + imageCache.clear(); + imageCacheBytes = 0; style.remove(); + delete window.__bennettMarkdownPreviewMath; }; }, /** - * 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 + * Hide the Plus/Pro plan "Upgrade" / "Get Plus" buttons while keeping Codex software-update notices visible. 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. */ - "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(); + "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 APP_UPDATE_CONTEXT_RE = + /(?:\b(?:app|application|desktop|codex)\s+(?:update|upgrade|version|release)\b|\b(?:update|updated|updating|new version|latest version|release|download|restart|install)\b|软件升级|应用升级|版本升级|新版本|软件更新|应用更新|更新可用|下载更新|重启更新|安装包)/i; + const hidden = new Set(/* HTMLElement */); - const style = document.createElement("style"); - style.id = STYLE_ID; - style.textContent = ` - [data-composer-overlay-floating-ui="true"] { - isolation: isolate; + const isInsideOurShell = (el) => { + let n = el; + while (n) { + if (n instanceof HTMLElement && n.dataset?.codexpp) return true; + n = n.parentElement; } + return false; + }; - [data-composer-overlay-floating-ui="true"][${OVERLAY_ATTR}="true"] - > :not([${MENU_ATTR}="true"]) { - display: none !important; + // Codex may split a label across icon + text spans, so use textContent + // for the button itself and semantic attributes for update banners. + const normText = (el) => + (el.textContent || "").replace(/\s+/g, " ").trim().toLowerCase(); + const semanticText = (el) => + [ + el.textContent, + el.getAttribute("aria-label"), + el.getAttribute("title"), + el.getAttribute("data-testid"), + el.getAttribute("data-test"), + el.id, + typeof el.className === "string" ? el.className : "", + ] + .filter(Boolean) + .join(" ") + .replace(/\s+/g, " ") + .trim() + .toLowerCase(); + + const isAppUpdateControl = (el) => { + let n = el; + for (let depth = 0; n && depth < 5; depth += 1, n = n.parentElement) { + const text = semanticText(n); + // Avoid matching against the entire document body, which could contain + // an unrelated update message elsewhere in the page. + if (text.length <= 240 && APP_UPDATE_CONTEXT_RE.test(text)) return true; } + return false; + }; - [data-composer-overlay-floating-ui="true"] - > [${OVERLAY_NOISE_ATTR}="true"] { - display: none !important; + const matches = (el, text) => { + if (!text || isAppUpdateControl(el)) return false; + if (EXACT.has(text)) return true; + for (const c of CONTAINS) if (text.includes(c)) return true; + return false; + }; + const CANDIDATE_SELECTOR = 'button, a, [role="button"], [role="menuitem"]'; + const scanRoot = (root) => { + const candidates = []; + if (root instanceof Element && root.matches(CANDIDATE_SELECTOR)) candidates.push(root); + if (root?.querySelectorAll) candidates.push(...root.querySelectorAll(CANDIDATE_SELECTOR)); + for (const el of candidates) { + if (isInsideOurShell(el)) continue; + const t = normText(el); + if (t.length === 0 || t.length > 80) continue; + if (!matches(el, t)) continue; + const host = el.closest('[class*="rounded"], [class*="badge"]') || el; + if (!(host instanceof HTMLElement)) continue; + if (hidden.has(host)) continue; + host.dataset.codexppPrevDisplay = host.style.display || ""; + host.style.display = "none"; + hidden.add(host); + api.log.info("hid upgrade element", { text: t }); } + }; - [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; + let scanTimer = 0; + const pendingRoots = new Set(); + const flushPendingRoots = () => { + scanTimer = 0; + const roots = Array.from(pendingRoots); + pendingRoots.clear(); + for (const root of roots) scanRoot(root); + }; + const scheduleRoots = (records) => { + for (const record of records) { + for (const node of record.addedNodes) { + const root = node instanceof Element ? node : node.parentElement; + if (root instanceof Element) pendingRoots.add(root); + } + } + if (!pendingRoots.size) return; + if (pendingRoots.size > 40) { + pendingRoots.clear(); + pendingRoots.add(document.documentElement); } + if (scanTimer) window.clearTimeout(scanTimer); + scanTimer = window.setTimeout(flushPendingRoots, 100); + }; - [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; + scanRoot(document); + const obs = new MutationObserver(scheduleRoots); + obs.observe(document.documentElement, { childList: true, subtree: true }); + + return () => { + obs.disconnect(); + if (scanTimer) window.clearTimeout(scanTimer); + pendingRoots.clear(); + for (const el of hidden) { + if ("codexppPrevDisplay" in el.dataset) { + el.style.display = el.dataset.codexppPrevDisplay; + delete el.dataset.codexppPrevDisplay; + } } + hidden.clear(); + }; + }, - [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; - } + /** + * Surface 5h + Weekly rate limits and points balance 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, + * points: {label,value} | 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 = readSnapshot(api); + 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 disposed = false; + let lastMountedMode = null; + let accountMode = "unknown"; // "official" | "api" | "unknown" + let accountModeInFlight = false; + let accountModeLastCheckedAt = 0; + let accountModeLogged = false; + let accountModeCandidate = "unknown"; + let accountModeCandidateCount = 0; + let accountModeCandidateAt = 0; - [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; - } + 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(", "); + const SIDEBAR_CANDIDATE_SELECTOR = [ + ASIDE_SELECTOR, + "aside", + "nav", + "[role='navigation']", + "[data-testid*='sidebar' i]", + "[data-test*='sidebar' i]", + "[class*='sidebar' i]", + ].join(", "); - [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; + // ── 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; + }; - [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; + const applySnapshot = (partial, source) => { + if (disposed) return false; + if ( + !partial?.fiveHour && + !partial?.weekly && + !Object.prototype.hasOwnProperty.call(partial || {}, "points") + ) { + return false; } - - [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; + const next = { + fiveHour: partial.fiveHour || snapshot?.fiveHour || null, + weekly: partial.weekly || snapshot?.weekly || null, + points: Object.prototype.hasOwnProperty.call(partial, "points") + ? partial.points + : snapshot?.points || null, + at: Date.now(), + }; + const changed = + JSON.stringify(next.fiveHour) !== JSON.stringify(snapshot?.fiveHour) || + JSON.stringify(next.weekly) !== JSON.stringify(snapshot?.weekly) || + JSON.stringify(next.points) !== JSON.stringify(snapshot?.points); + snapshot = next; + writeSnapshot(api, snapshot); + if (changed) { + log(`parsed snapshot from ${source}`, snapshot); + ensureMounted(); } + return changed; + }; - [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; - } + 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", + }, + })); + const onRequest = (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); + }; + const onMessage = (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, + }, "*"); + }; + const stop = () => { + window.removeEventListener("codexpp-usage-request", onRequest); + window.removeEventListener("message", onMessage); + window.__codexppUsageBridgeInstalled = false; + }; + window.addEventListener("codexpp-usage-request", onRequest); + window.addEventListener("message", onMessage); + window.addEventListener("codexpp-usage-bridge-stop", stop, { once: true }); + })();`; + (document.head || document.documentElement).appendChild(script); + script.remove(); + }; - [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; + const dispatchCodexViewMessage = (message) => { + const bridge = window.electronBridge; + if (typeof bridge?.sendMessageFromView === "function") { + bridge.sendMessageFromView(message).catch((e) => { + if (!directUsageFailureLogged) { + directUsageFailureLogged = true; + api.log.warn("[usage] bridge send failed", e); + } + }); + return; } + ensureUsageBridgeScript(); + window.dispatchEvent( + new CustomEvent("codexpp-usage-request", { detail: message }), + ); + }; - [data-composer-overlay-floating-ui="true"] - [${MENU_ATTR}="true"] - [${SECTION_TITLE_ATTR}][data-changing="true"] { - animation: codexpp-slash-title-change 180ms ease !important; + 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. } - @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; - } + const hostId = + new URL(window.location.href).searchParams.get("hostId")?.trim() || + "local"; + const requestId = `codexpp-usage-${Date.now()}-${++bridgeRequestSeq}`; - [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; - } + 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, + }); + }); + }; - [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; + const bridgePostJson = async (path, payload = {}, timeoutMs = 2_500) => { + const bridge = window.__codexSessionDeleteBridge; + if (disposed || typeof bridge !== "function") return null; + let timeout = 0; + try { + return await Promise.race([ + bridge(path, payload), + new Promise((resolve) => { + timeout = window.setTimeout(() => resolve(null), timeoutMs); + }), + ]); + } finally { + if (timeout) window.clearTimeout(timeout); } + }; - [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; - } + 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; + }; - [data-composer-overlay-floating-ui="true"] - [${MENU_ATTR}="true"] - [${SECTION_ICON_ATTR}][data-active="true"]::before { - opacity: 1 !important; - transform: scale(1) !important; + 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; + }; - [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; - } + 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); + }; - [data-composer-overlay-floating-ui="true"] - [${MENU_ATTR}="true"] - [${SECTION_ICON_ATTR}][data-active="true"]:hover { - color: #fff !important; + 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 explicitMode = false; + const settings = await bridgePostJson("/settings/get", {}); + if (disposed) return accountMode; + 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"; + explicitMode = true; + } else if (relayMode === "pureApi" || relayMode === "pure_api") { + nextMode = "api"; + explicitMode = true; + } else if (relayMode === "mixedApi" || relayMode === "mixed_api" || officialMixApiKey) { + nextMode = "api"; + explicitMode = true; + } else if (!relayMode && legacyApiConfigured) { + nextMode = "api"; + explicitMode = true; + } - [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; - } + if (nextMode === "unknown") { + const catalog = await bridgePostJson("/codex-model-catalog", {}); + if (disposed) return accountMode; + if (catalogLooksLikeApiMode(catalog)) nextMode = "api"; + else if (catalog?.model_provider === "openai" || catalog?.provider_name === "openai") { + nextMode = "official"; + } + } + if (nextMode === "unknown") return accountMode; + + // Catalog responses can briefly reflect the previous provider while + // Codex is switching accounts. Require two matching non-explicit + // observations before changing the visible mode. + if (nextMode === accountMode) { + accountModeCandidate = "unknown"; + accountModeCandidateCount = 0; + accountModeCandidateAt = 0; + return accountMode; + } + if (accountMode !== "unknown" || !explicitMode) { + if ( + accountModeCandidate === nextMode && + now - accountModeCandidateAt < 45_000 + ) { + accountModeCandidateCount += 1; + } else { + accountModeCandidate = nextMode; + accountModeCandidateCount = 1; + accountModeCandidateAt = now; + } + if (accountModeCandidateCount < 2) return accountMode; + } - [data-composer-overlay-floating-ui="true"] - [${MENU_ATTR}="true"] - [${SECTION_ICON_ATTR}][data-active="true"] - svg path { - stroke-width: 1.8 !important; + accountModeCandidate = "unknown"; + accountModeCandidateCount = 0; + accountModeCandidateAt = 0; + accountMode = nextMode; + if (accountMode === "api") { + snapshot = { + fiveHour: { label: "API", pct: null, resetAt: null, apiMode: true }, + weekly: null, + at: Date.now(), + apiMode: true, + }; + } else { + // Keep the last stable value visible while the official snapshot + // refreshes. Clearing here caused the control to flash "—". + directUsageAvailable = false; + directUsageLastAttemptAt = 0; + } + ensureMounted(true); + if (!accountModeLogged) { + accountModeLogged = true; + log("account mode", accountMode); + } + return accountMode; + } catch (e) { + return accountMode; + } finally { + accountModeInFlight = false; } + }; - [data-composer-overlay-floating-ui="true"] - [${MENU_ATTR}="true"] - [data-list-navigation-item="true"][${FAVORITE_DUPLICATE_HIDDEN_ATTR}="true"] { - display: none !important; - } + const remainingPercent = (usedPercent) => { + const used = Number(usedPercent); + if (!Number.isFinite(used)) return null; + return Math.round(Math.min(Math.max(100 - used, 0), 100)); + }; - [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; - } + 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", + }); + }; - [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; - } + 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), + }; + }; - [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; + const pointValue = (value) => { + if (value == null) return null; + if (typeof value === "string" || typeof value === "number") { + const text = String(value).trim(); + return text || null; + } + if (typeof value !== "object") return null; + if (value.unlimited === true) return "无限"; + for (const key of [ + "balance", + "remaining", + "available", + "amount", + "points", + "credits", + "value", + ]) { + const result = pointValue(value[key]); + if (result != null) return result; } + return null; + }; - [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; + const normalizePoints = (status) => { + if (!status || typeof status !== "object") return null; + const candidates = [ + status.points, + status.point_balance, + status.pointBalance, + status.credits, + status.credit, + status.account?.points, + status.account?.credits, + ]; + for (const candidate of candidates) { + const value = pointValue(candidate); + if (value != null) return { label: "Credit", value, kind: "points" }; } + return null; + }; - [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; + 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; + }; - [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; - } + 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); + }; - [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; + pushLimit(status?.rate_limit); + if (Array.isArray(status?.additional_rate_limits)) { + for (const item of status.additional_rate_limits) { + pushLimit(item?.rate_limit); + } } - [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; - } + const five = pickClosestWindow( + limits, + 300, + (minutes) => minutes > 0 && minutes < 1440, + ); + const weekly = pickClosestWindow( + limits, + 7 * 24 * 60, + (minutes) => minutes >= 1440, + ); - [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; - } + return { + fiveHour: normalizeUsageWindow(five, "5h"), + weekly: normalizeUsageWindow(weekly, "Weekly"), + points: normalizePoints(status), + }; + }; - [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; + 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); } - - [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; + 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; + }; - [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; - } + 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"), + }; + }; - [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; - } + const applyUsageEvent = (message) => { + if (!message || typeof message !== "object") return false; + const windows = collectUsageWindows(message); + const points = normalizePoints(message); + if (!windows.length && !points) return false; + const partial = snapshotFromUsageWindows(windows); + if (points) partial.points = points; + if (!partial.fiveHour && !partial.weekly && !partial.points) return false; + directUsageAvailable = true; + applySnapshot(partial, "rate-limit-event"); + return true; + }; - [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; - } + const onUsageMessage = (event) => { + const data = event.data; + if (!data || typeof data !== "object") return; + applyUsageEvent(data); + }; - [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; + const refreshUsageFromApi = async () => { + if (disposed) return false; + if ((await refreshAccountMode()) === "api" || disposed) return false; + if (directUsageInFlight) return false; + const now = Date.now(); + if (directUsageLastAttemptAt && now - directUsageLastAttemptAt < 15_000) { + return false; } - - [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; + directUsageLastAttemptAt = now; + directUsageInFlight = true; + try { + const status = await fetchCodexAppServerJson("/wham/usage"); + if (disposed) return false; + 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; } + }; - [data-composer-overlay-floating-ui="true"] - [${MENU_ATTR}="true"] - .${FAVORITE_BUTTON_CLASS}[data-favorite="true"] { - color: #f4c95d !important; + /** + * 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; + }; - [data-composer-overlay-floating-ui="true"] - [${MENU_ATTR}="true"] - .${FAVORITE_BUTTON_CLASS}:hover { - color: #ffd76a !important; + /** + * 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 }; + }; - [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; - } + const parseValue = (span) => { + const txt = (span.textContent || "").replace(/\s+/g, " ").trim(); + return parseValueText(txt, span); + }; - [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; + 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; + }; - [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; - } + 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; - [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; - } + 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 } }; + }; - [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; + 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"); } + }; - [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; - } + // ── 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(); - [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; + 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; } - `; - 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, + 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; }; - 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 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 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); + const quickControlText = (node) => + [ + node.getAttribute?.("aria-label"), + node.getAttribute?.("title"), + node.textContent, + ] + .filter(Boolean) + .join(" ") + .replace(/\s+/g, " ") + .trim() + .toLowerCase(); + + const isSidebarGeometry = (node) => { + if (!(node instanceof HTMLElement) || !isVisibleElement(node)) return false; + const rect = node.getBoundingClientRect(); + const viewportHeight = window.innerHeight || document.documentElement.clientHeight || 0; + const viewportWidth = window.innerWidth || document.documentElement.clientWidth || 0; return ( - /^Codex\+\+\b/.test(text) || - /^Tweaks\b/.test(text) || - /\bTweak Store\b/.test(text) || - /Better TerminalKeyboard ShortcutsDatabase Explorer/.test(text) + rect.width >= 180 && + rect.width <= Math.min(520, Math.max(320, viewportWidth * 0.55)) && + rect.height >= Math.max(360, viewportHeight * 0.45) && + rect.left <= Math.max(96, viewportWidth * 0.12) && + rect.top <= Math.max(96, viewportHeight * 0.18) ); }; - const stopHoverSelectionEvent = (menu, event) => { - if (!(menu instanceof HTMLElement)) return; - trackPointerPosition(menu, event); - if (shouldBlockSuppressedHover(menu, event)) { - event.stopPropagation(); - event.stopImmediatePropagation?.(); - return; + const hasBottomControl = (sidebar) => { + const sidebarRect = sidebar.getBoundingClientRect(); + const controls = Array.from(sidebar.querySelectorAll('button, a, [role="button"], [role="status"], [aria-live], span, div')); + return controls.some((control) => { + if (!(control instanceof HTMLElement) || !isVisibleElement(control)) return false; + const rect = control.getBoundingClientRect(); + const text = quickControlText(control); + const nearBottom = rect.bottom >= sidebarRect.bottom - 260; + const compact = rect.width > 0 && rect.width <= 64 && rect.height > 0 && rect.height <= 64; + const downloadStatus = text.length <= 80 && /\bdownloading\b|\bdownload\b|\bupdating\b|\binstalling\b|正在下载|下载中|更新中|正在更新|安装中/.test(text); + return nearBottom && (compact || downloadStatus || /\bmobile\b|\bphone\b|\bdevice\b|\bsettings?\b|手机|移动|设备|连接|设置/.test(text)); + }); + }; + + const addSidebarAncestorsForBottomControls = (candidates) => { + const viewportHeight = window.innerHeight || document.documentElement.clientHeight || 0; + const controls = Array.from(document.querySelectorAll('button, a, [role="button"], [role="status"], [aria-live], span, div')); + for (const control of controls) { + if (!(control instanceof HTMLElement) || !isVisibleElement(control)) continue; + const rect = control.getBoundingClientRect(); + if (rect.left > 560 || rect.bottom < viewportHeight - 280) continue; + const text = quickControlText(control); + const compact = rect.width > 0 && rect.width <= 64 && rect.height > 0 && rect.height <= 64; + const downloadStatus = text.length <= 80 && /\bdownloading\b|\bdownload\b|\bupdating\b|\binstalling\b|正在下载|下载中|更新中|正在更新|安装中/.test(text); + if (!compact && !downloadStatus && !/\bmobile\b|\bphone\b|\bdevice\b|\bsettings?\b|手机|移动|设备|连接|设置/.test(text)) continue; + let node = control.parentElement; + while (node && node !== document.body) { + if (isSidebarGeometry(node) && !looksLikeSettingsSidebar(node)) candidates.add(node); + node = node.parentElement; + } } - 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 sidebarScore = (sidebar) => { + if (!isSidebarGeometry(sidebar) || looksLikeSettingsSidebar(sidebar)) return -Infinity; + const rect = sidebar.getBoundingClientRect(); + let score = 0; + if (rect.left <= 16) score += 4; + else if (rect.left <= 80) score += 2; + if (rect.width >= 220 && rect.width <= 420) score += 3; + if (rect.height >= (window.innerHeight || 0) * 0.75) score += 3; + if (looksLikeMainAppSidebar(sidebar)) score += 6; + if (hasBottomControl(sidebar)) score += 5; + if (/^(aside|nav)$/i.test(sidebar.tagName) || sidebar.getAttribute("role") === "navigation") score += 2; + return score; }; - const allowSlashRowScrollIntoView = (duration = 220) => { - slashRowScrollAllowedUntil = Math.max( - slashRowScrollAllowedUntil, - performance.now() + duration, + const findUsageSidebar = () => { + const primarySidebar = Array.from(document.querySelectorAll(ASIDE_SELECTOR)) + .find((sidebar) => { + if (!(sidebar instanceof HTMLElement) || !isVisibleElement(sidebar)) return false; + if (looksLikeSettingsSidebar(sidebar)) return false; + const rect = sidebar.getBoundingClientRect(); + return rect.width >= 150 && rect.height >= 300; + }); + if (primarySidebar instanceof HTMLElement) return primarySidebar; + const candidates = new Set( + Array.from(document.querySelectorAll(SIDEBAR_CANDIDATE_SELECTOR)) + .filter((node) => node instanceof HTMLElement), ); + addSidebarAncestorsForBottomControls(candidates); + return Array.from(candidates) + .map((sidebar) => ({ sidebar, score: sidebarScore(sidebar) })) + .filter((item) => item.score > 0) + .sort((a, b) => b.score - a.score || a.sidebar.getBoundingClientRect().left - b.sidebar.getBoundingClientRect().left)[0]?.sidebar || null; }; - const isSlashMenuRow = (node) => - node instanceof HTMLElement && - node.matches('[data-list-navigation-item="true"]') && - !!node.closest(`[${MENU_ATTR}="true"]`); + const controlText = (node) => + [ + node.getAttribute?.("aria-label"), + node.getAttribute?.("title"), + node.textContent, + ] + .filter(Boolean) + .join(" ") + .replace(/\s+/g, " ") + .trim() + .toLowerCase(); - 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 controlLabelText = (node) => + [node.getAttribute?.("aria-label"), node.getAttribute?.("title")] + .filter(Boolean) + .join(" ") + .replace(/\s+/g, " ") + .trim() + .toLowerCase(); + + const isDeviceButton = (button) => { + const label = controlLabelText(button); + if (/\bmobile\b|\bphone\b|\bdevice\b|手机|移动|设备|连接/.test(label)) return true; + const text = controlText(button); + return text.length <= 28 && /\bmobile\b|\bphone\b|\bdevice\b|手机|移动|设备|连接/.test(text); }; - const eventPointerPosition = (event) => { - if (!event || typeof event.clientX !== "number" || typeof event.clientY !== "number") { - return null; - } - return { x: event.clientX, y: event.clientY }; + const isDownloadStatusNode = (node) => { + const text = controlText(node); + if (!text || text.length > 80) return false; + return /\bdownloading\b|\bdownload\b|\bupdating\b|\binstalling\b|正在下载|下载中|更新中|正在更新|安装中/.test(text); }; - 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 isSettingsButton = (button) => { + const text = controlText(button); + return /\bsettings?\b|preferences?|设置|偏好/.test(text); }; - 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 isNearSidebarBottom = (sidebar, node) => { + if (!(sidebar instanceof HTMLElement) || !(node instanceof HTMLElement)) return false; + const sidebarRect = sidebar.getBoundingClientRect(); + const rect = node.getBoundingClientRect(); + const bottomBand = Math.min(Math.max(sidebarRect.height * 0.22, 120), 240); + const visibleBottom = Math.min( + sidebarRect.bottom, + window.innerHeight || document.documentElement.clientHeight || sidebarRect.bottom, + ); + return ( + rect.top < visibleBottom && + rect.bottom <= visibleBottom + 8 && + rect.bottom >= visibleBottom - bottomBand ); - 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 isCompactIconControl = (control) => { + const rect = control.getBoundingClientRect(); + const text = controlText(control); + return rect.width > 0 && rect.width <= 56 && rect.height > 0 && rect.height <= 56 && text.length <= 32; }; - 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); + const isUsageControlNode = (node) => + node.closest?.('[data-codexpp="usage-slot"], [data-codexpp="usage-box"], [data-codexpp="usage-boxes"]'); - 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; + 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 nearBottom = isNearSidebarBottom(sidebar, row); + const looksLikeControlLayer = + insideSidebar && + nearBottom && + rect.height > 0 && + rect.height <= 128 && + (style.display === "flex" || style.display === "grid" || buttonCount >= 2); + if (looksLikeControlLayer) return row; + row = row.parentElement; } - return true; + return null; }; - 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); + const nearestBottomStatusRow = (sidebar, node) => { + const sidebarRect = sidebar.getBoundingClientRect(); + let row = node.parentElement; + while (row && row !== document.body && row !== sidebar.parentElement) { + if (!(row instanceof HTMLElement)) break; + const rect = row.getBoundingClientRect(); + const style = window.getComputedStyle(row); + const insideSidebar = + rect.left >= sidebarRect.left - 8 && + rect.right <= sidebarRect.right + 8; + const nearBottom = isNearSidebarBottom(sidebar, row); + const compactStatusLayer = + insideSidebar && + nearBottom && + rect.height > 0 && + rect.height <= 96 && + (style.display === "flex" || style.display === "grid" || isDownloadStatusNode(row)); + if (compactStatusLayer) return row; + row = row.parentElement; } - return state; + return null; }; - 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); + 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.nextSibling); } + else row.appendChild(slot); + return slot; + }; - state.lastTop = scroller.scrollTop; - return false; + const createFallbackSlot = (sidebar) => { + const existing = sidebar.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 = "sidebar-floating-fallback"; + slot.className = "flex items-center"; + slot.style.position = "absolute"; + slot.style.right = "0.75rem"; + slot.style.bottom = "0.75rem"; + slot.style.zIndex = "30"; + sidebar.appendChild(slot); + return slot; }; - 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); + const findSidebarSlot = () => { + const sidebar = findUsageSidebar(); + if (!sidebar) return null; + for (const slot of sidebar.querySelectorAll('[data-codexpp="usage-slot"]')) { + if ( + !(slot instanceof HTMLElement) || + !(slot.parentElement instanceof HTMLElement) || + !slot.isConnected + ) { + slot.remove(); } - }; - state.restoreFrame = requestAnimationFrame(tick); - }; + } + const existingSlot = Array.from(sidebar.querySelectorAll('[data-codexpp="usage-slot"]')) + .find((slot) => + slot instanceof HTMLElement && + slot.parentElement instanceof HTMLElement && + slot.isConnected, + ); + if (existingSlot instanceof HTMLElement) return existingSlot; - const queueHoverScrollFreezeChecks = (scroller) => { - [0, 16, 80, 180, 360].forEach((delay) => { - window.setTimeout(() => enforceHoverScrollFreeze(scroller), delay); + const controls = Array.from(sidebar.querySelectorAll('button, a, [role="button"]')) + .filter((button) => + button instanceof HTMLElement && + isVisibleElement(button) && + isNearSidebarBottom(sidebar, button) && + !isUsageControlNode(button), + ); + const deviceControls = controls.filter(isDeviceButton); + const settingsControls = controls.filter(isSettingsButton); + const compactControls = controls.filter(isCompactIconControl); + const preferredControls = deviceControls.length + ? deviceControls + : compactControls.length + ? compactControls + : settingsControls; + const ordered = (preferredControls.length ? preferredControls : controls).sort((a, b) => { + const ar = a.getBoundingClientRect(); + const br = b.getBoundingClientRect(); + return br.right - ar.right || br.bottom - ar.bottom; }); - }; - 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; + for (const button of ordered) { + const row = nearestControlRow(sidebar, button); + if (row) return createInlineSlot(row, button); } - 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 statusAnchors = Array.from( + sidebar.querySelectorAll('[role="status"], [aria-live], [aria-label], [title], span, div'), + ) + .filter((node) => + node instanceof HTMLElement && + isVisibleElement(node) && + isNearSidebarBottom(sidebar, node) && + !isUsageControlNode(node) && + isDownloadStatusNode(node), + ) + .sort((a, b) => { + const ar = a.getBoundingClientRect(); + const br = b.getBoundingClientRect(); + return br.bottom - ar.bottom || br.right - ar.right; + }); - const allowProgrammaticScroll = (scroller, targetTop, duration = 900) => { - const state = hoverScrollStateFor(scroller); - if (targetTop < scroller.scrollTop - 1) { - state.freezeUntil = 0; - state.freezeTop = targetTop; + for (const anchor of statusAnchors) { + const row = nearestBottomStatusRow(sidebar, anchor); + if (row) return createInlineSlot(row, anchor); } - state.programmaticTarget = targetTop; - state.programmaticUntil = performance.now() + duration; - }; - const patchedScrollIntoView = function (...args) { - if (isSlashMenuRow(this) && performance.now() > slashRowScrollAllowedUntil) { - return; - } - return nativeScrollIntoView.apply(this, args); + return createFallbackSlot(sidebar); }; - 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 displaySnapshot = () => + accountMode === "api" + ? { + fiveHour: { label: "API", pct: null, resetAt: null, apiMode: true }, + weekly: null, + points: 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 }, + points: null, + at: 0, + }; - 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 ensureMounted = (forceRebuild = false) => { + if (disposed) return; + const visibleSnapshot = displaySnapshot(); + const slot = findSidebarSlot(); + 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"); + if (typeof reportLifecycle === "function") { + reportLifecycle("usage-slot-missing", { + asideCount: document.querySelectorAll("aside").length, + }); + } + ensureMounted._warned = true; + } + return; + } - 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(); + // 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, }); + if (typeof reportLifecycle === "function") { + reportLifecycle("usage-mounted", { + mode: slot.dataset.codexppUsageSlot || "unknown", + }); + } }; - const pruneMenuNoise = (menu) => { - menu.querySelectorAll(NAV_NOISE_SELECTOR).forEach((node) => node.remove()); - }; + // Initial render from persisted snapshot (so first paint isn't empty + // even before the user opens the popover). + ensureMounted(true); - 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()); + // ── observers ───────────────────────────────────────────────────── + // React can emit hundreds of mutations while restoring the history list. + // Ignore our own UI and editor churn, then coalesce the rest into one scan. + let scheduled = false; + let scheduleTimer = 0; + const runMutationScan = async () => { + if (disposed) return; + const mode = await refreshAccountMode(); + if (disposed) return; + if (mode !== "api") await refreshUsageFromApi(); + if (disposed) return; + if (accountMode !== "api" && !directUsageAvailable) { + const grid = findBreakdownGrid(); + if (grid) scanBreakdown(grid); + scanCompactUsage(); + } + ensureMounted(); + }; + const onMutate = () => { + if (scheduled) return; + scheduled = true; + scheduleTimer = window.setTimeout(() => { + scheduleTimer = 0; + scheduled = false; + void runMutationScan(); + }, 1000); }; - 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(); + onMutate(); + const IGNORED_MUTATION_SELECTOR = + "[data-codexpp], [data-codex-composer-root], [data-codex-composer='true'], [contenteditable='true'], textarea, [data-composer-overlay-floating-ui]"; + const obs = new MutationObserver((records) => { + const relevant = records.some((record) => { + const target = record.target; + const element = target instanceof Element ? target : target?.parentElement; + return !(element instanceof Element && element.closest(IGNORED_MUTATION_SELECTOR)); }); - }; + if (relevant) 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); - 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. + log("active", { snapshot }); + + return () => { + disposed = true; + if (usageBridgeScriptInjected) { + window.dispatchEvent(new CustomEvent("codexpp-usage-bridge-stop")); + } + obs.disconnect(); + window.clearInterval(interval); + if (scheduleTimer) window.clearTimeout(scheduleTimer); + 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(); } }; + }, - 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(); + /** Hide exhaustion banners and reset prompts without touching conversation content. */ + "hide-usage-alert"() { + const STYLE_ID = "codex-plus-hide-usage-alert-style"; + const HIDDEN_ATTR = "data-codex-plus-hidden-usage-alert"; + const quotaRe = /(Codex\s*消息限额已用尽|消息限额已用尽|message\s+limit|usage\s+limit|out\s+of\s+Codex\s+messages|额度|限额|quota|rate\s+limit)/i; + const resetRe = /(额度将于|继续使用\s*Codex|升级至\s*Plus|quota\s+will\s+reset|limit\s+will\s+reset|rate\s+limit\s+resets|reset|重置|upgrade\s+to\s+plus)/i; + const usageCardRe = /(剩余\s*\d+%\s*使用量|remaining\s+\d+%\s+usage|usage\s+remaining|reset\s+frequency|next\s+reset)/i; + const actionRe = /(升级|Plus|upgrade|pricing|重置|reset|限额|额度|限制|limit|quota)/i; + const quotaDialogSelector = [ + "[role='dialog']", + "[aria-modal='true']", + "[data-radix-dialog-content]", + "[data-slot='dialog-content']", + "[data-testid*='pricing' i]", + ].join(","); + const quotaSurfaceSelector = [ + "[role='alert']", + "[role='status']", + "[aria-live]", + quotaDialogSelector, + "[data-testid*='quota' i]", + "[data-testid*='usage' i]", + "[data-test*='quota' i]", + "[data-test*='usage' i]", + "[class*='toast' i]", + "[class*='alert' i]", + "[class*='banner' i]", + "[class*='modal' i]", + "aside:has(h3):has(button)", + ].join(","); + const hidden = new Set(); + let observer = null; + let timer = 0; + const guestPending = new WeakSet(); + const guestListeners = new Map(); + + const textOf = (node) => String(node?.innerText || node?.textContent || "").replace(/\s+/g, " ").trim(); + const visibleBox = (node) => { + if (!(node instanceof HTMLElement)) return false; + const rect = node.getBoundingClientRect(); + return rect.width >= 160 && rect.height >= 16 && rect.bottom > 0 && rect.top < (window.innerHeight || 900); + }; + const hasAction = (node, text) => actionRe.test(`${text} ${Array.from(node.querySelectorAll("button, a, [role='button']")).slice(0, 8).map((item) => textOf(item)).join(" ")}`); + const hasEditable = (node) => Boolean(node.querySelector("input, textarea, [contenteditable='true'], [role='textbox']")); + const touchesUsageControl = (node) => Boolean(node.closest("[data-codexpp='usage-slot'], [data-codexpp='usage-box'], [data-codexpp='usage-boxes']")); + const shouldHide = (node) => { + if (!(node instanceof HTMLElement) || node.closest("[data-message-author-role], article")) return false; + if (!node.matches(quotaSurfaceSelector)) return false; + if (!visibleBox(node)) return false; + if (hasEditable(node) || touchesUsageControl(node)) return false; + const text = textOf(node); + if (text.length < 12 || text.length > (node.matches(quotaDialogSelector) ? 4_000 : 500)) return false; + const rect = node.getBoundingClientRect(); + const bannerLike = rect.width >= 300 && rect.height >= 30 && rect.height <= 240 && quotaRe.test(text) && resetRe.test(text); + const cardLike = rect.width >= 160 && rect.width <= 560 && rect.height >= 70 && rect.height <= 340 && usageCardRe.test(text) && hasAction(node, text); + const dialogLike = node.matches(quotaDialogSelector) && quotaRe.test(text) && (resetRe.test(text) || actionRe.test(text)); + return (bannerLike || cardLike || dialogLike) && (hasAction(node, text) || dialogLike); + }; + const findHideTarget = (node) => { + if (!node.matches(quotaDialogSelector)) return node; + const viewportWidth = window.innerWidth || document.documentElement.clientWidth || 1; + const viewportHeight = window.innerHeight || document.documentElement.clientHeight || 1; + const nodeRect = node.getBoundingClientRect(); + if (nodeRect.width >= viewportWidth * 0.8 && nodeRect.height >= viewportHeight * 0.8) return node; + let current = node; + for (let depth = 0; depth < 4; depth += 1) { + const parent = current.parentElement; + if (!parent || parent === document.body || parent === document.documentElement) break; + const protectedContent = parent.querySelector("[data-message-author-role], article, input, textarea, [contenteditable='true'], [role='textbox'], [data-codexpp='usage-slot'], [data-codexpp='usage-box']"); + const compactSurface = textOf(parent).length <= 800 && !protectedContent; + const rect = parent.getBoundingClientRect(); + const position = window.getComputedStyle(parent).position; + const coversViewport = rect.width >= viewportWidth * 0.8 && rect.height >= viewportHeight * 0.8; + const portalLike = parent.matches("[data-radix-portal], [data-portal], [class*='portal' i]") || Boolean(parent.querySelector("[data-radix-dialog-overlay], [data-slot='dialog-overlay'], [class*='backdrop' i], [class*='overlay' i]")); + if (compactSurface && (portalLike || (coversViewport && (position === "fixed" || position === "absolute")))) return parent; + current = parent; + } + return node; + }; + const scanGuestUsage = () => { + const STYLE_ID = "codex-plus-hide-usage-alert-style"; + const HIDDEN_ATTR = "data-codex-plus-hidden-usage-alert"; + const STATE_KEY = "__codexPlusUsageAlertGuestState"; + const state = window[STATE_KEY] || { observer: null, timer: 0 }; + window[STATE_KEY] = state; + const quotaRe = /(Codex\s*消息限额已用尽|消息限额已用尽|message\s+limit|usage\s+limit|out\s+of\s+Codex\s+messages|额度|限额|quota|rate\s+limit)/i; + const resetRe = /(额度将于|继续使用\s*Codex|升级至\s*Plus|quota\s+will\s+reset|limit\s+will\s+reset|rate\s+limit\s+resets|reset|重置|upgrade\s+to\s+plus)/i; + const usageCardRe = /(剩余\s*\d+%\s*使用量|remaining\s+\d+%\s+usage|usage\s+remaining|reset\s+frequency|next\s+reset)/i; + const actionRe = /(升级|Plus|upgrade|pricing|重置|reset|限额|额度|限制|limit|quota)/i; + const quotaDialogSelector = [ + "[role='dialog']", + "[aria-modal='true']", + "[data-radix-dialog-content]", + "[data-slot='dialog-content']", + "[data-testid*='pricing' i]", + ].join(","); + const quotaSurfaceSelector = [ + "[role='alert']", + "[role='status']", + "[aria-live]", + quotaDialogSelector, + "[data-testid*='quota' i]", + "[data-testid*='usage' i]", + "[data-test*='quota' i]", + "[data-test*='usage' i]", + "[class*='toast' i]", + "[class*='alert' i]", + "[class*='banner' i]", + "[class*='modal' i]", + "aside:has(h3):has(button)", + ].join(","); + const textOf = (node) => String(node?.innerText || node?.textContent || "").replace(/\s+/g, " ").trim(); + const visibleBox = (node) => { + const rect = node.getBoundingClientRect(); + return rect.width >= 160 && rect.height >= 16 && rect.bottom > 0 && rect.top < (window.innerHeight || 900); + }; + const hasAction = (node, text) => actionRe.test(`${text} ${Array.from(node.querySelectorAll("button, a, [role='button']")).slice(0, 8).map(textOf).join(" ")}`); + const shouldHide = (node) => { + if (!(node instanceof HTMLElement) || node.closest("[data-message-author-role], article")) return false; + if (!node.matches(quotaSurfaceSelector) || !visibleBox(node)) return false; + if (node.querySelector("input, textarea, [contenteditable='true'], [role='textbox']")) return false; + const text = textOf(node); + if (text.length < 12 || text.length > (node.matches(quotaDialogSelector) ? 4_000 : 500)) return false; + const rect = node.getBoundingClientRect(); + const bannerLike = rect.width >= 300 && rect.height >= 30 && rect.height <= 240 && quotaRe.test(text) && resetRe.test(text); + const cardLike = rect.width >= 160 && rect.width <= 560 && rect.height >= 70 && rect.height <= 340 && usageCardRe.test(text) && hasAction(node, text); + const dialogLike = node.matches(quotaDialogSelector) && quotaRe.test(text) && (resetRe.test(text) || actionRe.test(text)); + return (bannerLike || cardLike || dialogLike) && (hasAction(node, text) || dialogLike); + }; + const findHideTarget = (node) => { + const viewportWidth = window.innerWidth || document.documentElement.clientWidth || 1; + const viewportHeight = window.innerHeight || document.documentElement.clientHeight || 1; + const nodeRect = node.getBoundingClientRect(); + if (nodeRect.width >= viewportWidth * 0.8 && nodeRect.height >= viewportHeight * 0.8) return node; + let current = node; + for (let depth = 0; depth < 4; depth += 1) { + const parent = current.parentElement; + if (!parent || parent === document.body || parent === document.documentElement) break; + const protectedContent = parent.querySelector("[data-message-author-role], article, input, textarea, [contenteditable='true'], [role='textbox']"); + const compactSurface = textOf(parent).length <= 800 && !protectedContent; + const rect = parent.getBoundingClientRect(); + const position = window.getComputedStyle(parent).position; + const coversViewport = rect.width >= viewportWidth * 0.8 && rect.height >= viewportHeight * 0.8; + const portalLike = parent.matches("[data-radix-portal], [data-portal], [class*='portal' i]") || Boolean(parent.querySelector("[data-radix-dialog-overlay], [data-slot='dialog-overlay'], [class*='backdrop' i], [class*='overlay' i]")); + if (compactSurface && (portalLike || (coversViewport && (position === "fixed" || position === "absolute")))) return parent; + current = parent; + } + return node; + }; + if (!document.documentElement) return 0; + let style = document.getElementById(STYLE_ID); + if (!style) { + style = document.createElement("style"); + style.id = STYLE_ID; + style.textContent = `[${HIDDEN_ATTR}="true"] { display: none !important; visibility: hidden !important; pointer-events: none !important; }`; + document.documentElement.appendChild(style); + } + const scan = () => { + let hiddenCount = 0; + for (const node of document.body?.querySelectorAll(quotaSurfaceSelector) || []) { + if (!node.hasAttribute(HIDDEN_ATTR) && shouldHide(node)) { + findHideTarget(node).setAttribute(HIDDEN_ATTR, "true"); + hiddenCount += 1; } + } + return hiddenCount; + }; + if (!state.observer) { + state.observer = new MutationObserver(() => { + if (!state.timer) state.timer = window.setTimeout(() => { state.timer = 0; scan(); }, 80); }); - }, 60); + state.observer.observe(document.documentElement, { childList: true, subtree: true, characterData: true }); + } + return scan(); + }; + const cleanupGuestUsage = () => { + const HIDDEN_ATTR = "data-codex-plus-hidden-usage-alert"; + const state = window.__codexPlusUsageAlertGuestState; + if (state?.timer) window.clearTimeout(state.timer); + state?.observer?.disconnect(); + delete window.__codexPlusUsageAlertGuestState; + document.querySelectorAll(`[${HIDDEN_ATTR}="true"]`).forEach((node) => node.removeAttribute(HIDDEN_ATTR)); + document.getElementById("codex-plus-hide-usage-alert-style")?.remove(); + }; + const scanGuests = () => { + for (const guest of document.querySelectorAll("webview")) { + if (typeof guest.executeJavaScript !== "function" || guestPending.has(guest)) continue; + guestPending.add(guest); + try { + Promise.resolve(guest.executeJavaScript(`(${scanGuestUsage.toString()})()`, true)).catch(() => {}).finally(() => guestPending.delete(guest)); + } catch { + guestPending.delete(guest); + } + } }; - - 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 attachGuest = (guest) => { + if (guestListeners.has(guest)) return; + const onGuestReady = () => window.setTimeout(scanGuests, 80); + guest.addEventListener("dom-ready", onGuestReady); + guest.addEventListener("did-stop-loading", onGuestReady); + guestListeners.set(guest, onGuestReady); }; - - const sectionIconSvg = (key) => { - if (key === "favorites") { - return ( - '" - ); - } - if (key === "skills") { - return ( - '" - ); + const hide = (node) => { + if (!(node instanceof HTMLElement) || node === document.body || node === document.documentElement) return; + const target = findHideTarget(node); + target.setAttribute(HIDDEN_ATTR, "true"); + hidden.add(target); + }; + const scan = () => { + timer = 0; + if (!document.body) return; + for (const node of document.body.querySelectorAll(quotaSurfaceSelector)) { + if (!node.hasAttribute(HIDDEN_ATTR) && shouldHide(node)) hide(node); } - return ( - '" - ); + for (const guest of document.querySelectorAll("webview")) attachGuest(guest); + scanGuests(); + }; + const schedule = () => { + if (!timer) timer = window.setTimeout(scan, 80); }; + const style = document.createElement("style"); + style.id = STYLE_ID; + style.textContent = `[${HIDDEN_ATTR}="true"] { display: none !important; visibility: hidden !important; pointer-events: none !important; }`; + document.documentElement.appendChild(style); + observer = new MutationObserver(schedule); + observer.observe(document.documentElement, { childList: true, subtree: true, characterData: true }); + scan(); - 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(); - } + return () => { + if (timer) window.clearTimeout(timer); + observer?.disconnect(); + for (const [guest, listener] of guestListeners) { + guest.removeEventListener("dom-ready", listener); + guest.removeEventListener("did-stop-loading", listener); + if (typeof guest.executeJavaScript === "function") guest.executeJavaScript(`(${cleanupGuestUsage.toString()})()`, true).catch(() => {}); + } + guestListeners.clear(); + for (const node of hidden) node.removeAttribute(HIDDEN_ATTR); + style.remove(); + hidden.clear(); }; + }, - 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. + /** + * 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); - 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}` : ""; + return () => { + style.remove(); }; + }, - 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); - }); - }; + /** + * 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 toggleFavorite = (key) => { - if (!key) return; - const favorites = readFavorites(); - if (favorites.has(key)) favorites.delete(key); - else favorites.add(key); - writeFavorites(favorites); - refreshFavoriteViews(); - scheduleScan(); - }; + const style = document.createElement("style"); + style.id = STYLE_ID; + style.textContent = ` + [data-composer-overlay-floating-ui="true"] { + isolation: isolate; + } - 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); - }; + [data-composer-overlay-floating-ui="true"][${OVERLAY_ATTR}="true"] + > :not([${MENU_ATTR}="true"]) { + display: none !important; + } - 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); + [data-composer-overlay-floating-ui="true"] + > [${OVERLAY_NOISE_ATTR}="true"] { + display: none !important; } - 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(); - }; + [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; + } - 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); - }; + [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; + } - 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); - }); - }; + [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; + } - 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; - }; + [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; + } - 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; + [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; } - 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); + [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; } - 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(); + [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; + } - const header = document.createElement("div"); - header.className = "sticky top-0"; - header.textContent = "Favorites"; - group.appendChild(header); + [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; + } - 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); - }); - }; + [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; + } - const navigationRows = (scroller) => - Array.from(scroller.querySelectorAll('[data-list-navigation-item="true"]')).filter( - (row) => row instanceof HTMLElement && row.offsetParent !== null, - ); + [data-composer-overlay-floating-ui="true"] + [${MENU_ATTR}="true"] + [${SECTION_TITLE_ATTR}][data-changing="true"] { + animation: codexpp-slash-title-change 180ms ease !important; + } - const favoriteRows = (scroller) => - Array.from( - scroller.querySelectorAll( - `[${FAVORITES_GROUP_ATTR}="true"] [data-list-navigation-item="true"]`, - ), - ).filter((row) => row instanceof HTMLElement && row.offsetParent !== null); + @keyframes codexpp-slash-title-change { + 0% { + opacity: 0; + transform: translateY(0.25rem); + } + 100% { + opacity: 1; + transform: translateY(0); + } + } - const selectedNavigationRow = (scroller) => - navigationRows(scroller).find( - (row) => row.getAttribute("aria-selected") === "true", - ); + [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; + } - 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"), - ); - }; + [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; + } - 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"); + [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; } - 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); - }; + [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; + } - const handleFavoriteNavigationKey = (event, scroller) => { - const rows = navigationRows(scroller); - const favs = favoriteRows(scroller); - if (rows.length === 0 || favs.length === 0) return false; + [data-composer-overlay-floating-ui="true"] + [${MENU_ATTR}="true"] + [${SECTION_ICON_ATTR}][data-active="true"]::before { + opacity: 1 !important; + transform: scale(1) !important; + } - 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; + [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; } - if (event.key !== "ArrowDown" && event.key !== "ArrowUp") return false; - scroller.dataset.codexppSlashFavoriteSelectionTouched = "true"; - event.preventDefault(); - event.stopPropagation(); - event.stopImmediatePropagation?.(); + [data-composer-overlay-floating-ui="true"] + [${MENU_ATTR}="true"] + [${SECTION_ICON_ATTR}][data-active="true"]:hover { + color: #fff !important; + } - 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; - }; + [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; + } - 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); - } + [data-composer-overlay-floating-ui="true"] + [${MENU_ATTR}="true"] + [${SECTION_ICON_ATTR}][data-active="true"] + svg path { + stroke-width: 1.8 !important; } - 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); + + [data-composer-overlay-floating-ui="true"] + [${MENU_ATTR}="true"] + [data-list-navigation-item="true"][${FAVORITE_DUPLICATE_HIDDEN_ATTR}="true"] { + display: none !important; } - }; - 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; + [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; } - 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("/"), - ); + [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; + } - const isEmptySlashMenu = (menu) => - isSlashQueryActive() && - menu.querySelectorAll('[data-list-navigation-item="true"]').length === 0 && - /^No commands$/i.test(normText(menu)); + [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; + } - 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; - }; + [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; + } - 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); - }; + [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; + } - 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 }; - }); + [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; + } - 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); + [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; } - }; - 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; + [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; } - 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)); - }; + [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; + } - const cancelScrollAnimation = (scroller) => { - const animation = scrollAnimations.get(scroller); - if (!animation) return; - cancelAnimationFrame(animation.frame); - window.clearTimeout(animation.timer); - scrollAnimations.delete(scroller); - }; + [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; + } - 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; + [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; } - 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(); + + [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 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 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 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 text = normText(node); + return ( + /^Codex\+\+\b/.test(text) || + /^Tweaks\b/.test(text) || + /\bTweak Store\b/.test(text) || + /Better TerminalKeyboard ShortcutsDatabase Explorer/.test(text) + ); }; - 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()); + const stopHoverSelectionEvent = (menu, event) => { + if (!(menu instanceof HTMLElement)) return; + trackPointerPosition(menu, event); + if (shouldBlockSuppressedHover(menu, event)) { + event.stopPropagation(); + event.stopImmediatePropagation?.(); 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); - } + freezeHoverScroll(menu); + event.stopPropagation(); + event.stopImmediatePropagation?.(); }; - const activeSlashMenu = () => - Array.from(document.querySelectorAll(`[${MENU_ATTR}="true"]`)).find( - (menu) => - menu instanceof HTMLElement && - menu.isConnected && - menu.querySelector(".vertical-scroll-fade-mask"), + 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), ); - - 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; + const allowSlashRowScrollIntoView = (duration = 220) => { + slashRowScrollAllowedUntil = Math.max( + slashRowScrollAllowedUntil, + performance.now() + duration, + ); + }; - if (handleFavoriteNavigationKey(event, scroller)) return; + const isSlashMenuRow = (node) => + node instanceof HTMLElement && + node.matches('[data-list-navigation-item="true"]') && + !!node.closest(`[${MENU_ATTR}="true"]`); - 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; + const hoverSuppressStateFor = (menu) => { + let state = hoverSuppressStates.get(menu); + if (!state) { + state = { + active: false, + pointerX: null, + pointerY: null, + releaseAfter: 0, + }; + hoverSuppressStates.set(menu, state); } - - if (!(event.metaKey || event.ctrlKey) || event.altKey || event.shiftKey) return; - const digit = keyDigit(event); - activateSectionByDigit(scroller, digit, event); + return state; }; - 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 eventPointerPosition = (event) => { + if (!event || typeof event.clientX !== "number" || typeof event.clientY !== "number") { + return null; + } + return { x: event.clientX, y: event.clientY }; }; - 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 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 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 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 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 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 scheduleSlashWork = () => { - scheduleHomeContentPrune(); - scheduleHardPruneNoise(); - scheduleScan(); + 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"); + } }; - 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); + 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); - 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; + 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; } - for (const animation of scrollAnimations.values()) { - cancelAnimationFrame(animation.frame); - window.clearTimeout(animation.timer); + 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; } - 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(); + return true; }; - }, - - /** - * 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; + 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; + }; - [${ROOT_ATTR}] .codexpp-settings-search-box { - position: relative; - display: flex; - align-items: center; - } + 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; - [${ROOT_ATTR}] svg { - position: absolute; - left: 0.625rem; - height: 1rem; - width: 1rem; - color: var(--color-token-text-secondary); - pointer-events: none; + 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); } - [${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; - } + state.lastTop = scroller.scrollTop; + return false; + }; - [${ROOT_ATTR}] input::placeholder { - color: var(--color-token-text-secondary); - } + 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); + }; - [${ROOT_ATTR}] input:focus { - border-color: color-mix(in srgb, currentColor 18%, transparent); - box-shadow: none; - } + const queueHoverScrollFreezeChecks = (scroller) => { + [0, 16, 80, 180, 360].forEach((delay) => { + window.setTimeout(() => enforceHoverScrollFreeze(scroller), delay); + }); + }; - [${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; + 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); + }; - [${ROOT_ATTR}][data-empty="true"] .codexpp-settings-search-empty { - display: block; - } + const clearHoverScrollFreeze = (scroller) => { + const state = hoverScrollStateFor(scroller); + state.freezeUntil = 0; + state.freezeTop = scroller.scrollTop; + state.lastTop = scroller.scrollTop; + }; - [${ROOT_ATTR}] .codexpp-settings-search-results { - display: none; - flex-direction: column; - gap: 0.125rem; - padding-top: 0.375rem; + 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; + }; - [${ROOT_ATTR}][data-has-results="true"] .codexpp-settings-search-results { - display: flex; + const patchedScrollIntoView = function (...args) { + if (isSlashMenuRow(this) && performance.now() > slashRowScrollAllowedUntil) { + return; } + return nativeScrollIntoView.apply(this, args); + }; - [${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; - } + 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"]') + ); + }; - [${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; - } + const isOverlaySlashActive = (overlay) => + isSlashQueryActive() || + Array.from(overlay.children).some((child) => looksLikeSlashPanel(child)); - [${ROOT_ATTR}] .codexpp-settings-search-result span { - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } + 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); + } + }); + }; - [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; - } + 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(); + }); + }; - [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 pruneMenuNoise = (menu) => { + menu.querySelectorAll(NAV_NOISE_SELECTOR).forEach((node) => node.remove()); + }; - const root = document.createElement("div"); - root.setAttribute(ROOT_ATTR, "true"); + 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 box = document.createElement("div"); - box.className = "codexpp-settings-search-box"; - box.innerHTML = - '"; + const shouldPruneHomeContentNoise = () => + isSlashQueryActive() || + !!document.querySelector( + '[data-composer-overlay-floating-ui="true"], [data-codexpp-slash-menu="true"]', + ); - 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 scheduleHomeContentPrune = () => { + if (homePruneFrame) return; + homePruneFrame = requestAnimationFrame(() => { + homePruneFrame = 0; + if (shouldPruneHomeContentNoise()) pruneHomeContentNoise(); + }); + }; - const empty = document.createElement("div"); - empty.className = "codexpp-settings-search-empty"; - empty.textContent = "No matching settings"; - root.appendChild(empty); + 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 results = document.createElement("div"); - results.className = "codexpp-settings-search-results"; - root.appendChild(results); + 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]; + }; - let scheduled = false; - let disposed = false; - let lastSidebar = null; - let highlightTimer = null; - const revealTimers = new Set(); - const pageIndex = new Map(); + const sectionIconSvg = (key) => { + if (key === "favorites") { + return ( + '" + ); + } + if (key === "skills") { + return ( + '" + ); + } + return ( + '" + ); + }; - const compact = (value) => - String(value || "").replace(/\s+/g, " ").trim().toLowerCase(); + const starIconSvg = (filled) => + filled + ? '' + : ''; - 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 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 labelFor = (node) => - compact( - [ - node.getAttribute?.("aria-label"), - node.getAttribute?.("title"), - node.textContent, - ] - .filter(Boolean) - .join(" "), - ); + 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 visibleLabelFor = (node) => compact(node?.textContent || ""); - const displayLabelFor = (node) => - String(node?.textContent || "").replace(/\s+/g, " ").trim(); + 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 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")) - ); + 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; }, ); - return candidates[0] instanceof HTMLElement ? candidates[0] : null; + + 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 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 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 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 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 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 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 visibleControlsIn = (node) => - Array.from(node.querySelectorAll("button, a")).filter( - (control) => - control instanceof HTMLElement && - !root.contains(control) && - control.getAttribute(HIDDEN_ATTR) !== "true", + 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 navControls = (sidebar) => - Array.from(sidebar.querySelectorAll("button, a")).filter( - (node) => node instanceof HTMLElement && !root.contains(node), + 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 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 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 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 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, ); - const headingLabel = displayLabelFor(heading); - return headingLabel ? titleCaseLabel(headingLabel) : "Settings"; + if (favorite instanceof HTMLElement) selectNavigationRow(scroller, favorite); }; - const titleCaseLabel = (value) => { - const raw = String(value || "").replace(/\s+/g, " ").trim(); - return raw || "Settings"; + 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 mainSurface = () => { - const surface = document.querySelector(".main-surface"); - return surface instanceof HTMLElement ? surface : null; + 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 shortText = (node) => - String(node?.textContent || "") - .replace(/\s+/g, " ") - .trim(); + 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); - 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; + 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 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 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 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 }); + 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"); } - if (items.length > 0) pageIndex.set(page, items); + navigationRows(scroller).forEach((item) => + item.setAttribute("aria-selected", item === row ? "true" : "false"), + ); + allowSlashRowScrollIntoView(); + row.scrollIntoView({ block: "nearest" }); + updateTopbar(scroller); }; - 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; + 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); } - } - return matches; + }; + requestAnimationFrame(keepFavoriteSelected); + window.setTimeout(keepFavoriteSelected, 80); }; - const navForPage = (sidebar, page) => - navControls(sidebar).find((node) => visibleLabelFor(node) === compact(page)); + const handleFavoriteNavigationKey = (event, scroller) => { + const rows = navigationRows(scroller); + const favs = favoriteRows(scroller); + if (rows.length === 0 || favs.length === 0) return false; - 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; + 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; } - }; - 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); + 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 findContentTarget = (match) => { - if (match.node instanceof HTMLElement && document.contains(match.node)) { - return match.node; + 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 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 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 clearRevealTimers = () => { - for (const timer of revealTimers) window.clearTimeout(timer); - revealTimers.clear(); + 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 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 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 renderResults = (sidebar, matches) => { - results.replaceChildren(); - for (const match of matches.slice(0, 5)) { + 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.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) => { + 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.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); + event.stopPropagation(); + const scroller = topbar.nextElementSibling; + if (!(scroller instanceof HTMLElement)) return; + scrollToSection(scroller, section, sections); + updateTopbar(scroller, sections); }); - results.appendChild(button); + icons.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); - } + 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); + }; - 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 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 applyFilter = () => { - scheduled = false; - if (disposed) return; + const cancelScrollAnimation = (scroller) => { + const animation = scrollAnimations.get(scroller); + if (!animation) return; + cancelAnimationFrame(animation.frame); + window.clearTimeout(animation.timer); + scrollAnimations.delete(scroller); + }; - const sidebar = findSettingsSidebar(); - if (!sidebar) { - root.remove(); - restoreHidden(document); + 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; } - 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 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 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++; + 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 (root.parentElement instanceof HTMLElement) { - syncGroupVisibility(root.parentElement, query); + if (topbar.dataset.forcedActiveSection) { + active = + sections.find((section) => section.key === topbar.dataset.forcedActiveSection) || + active; } - renderResults(sidebar, matches); - root.dataset.empty = visibleCount === 0 && matches.length === 0 ? "true" : "false"; - }; - - const schedule = () => { - if (scheduled || disposed) return; - scheduled = true; - requestAnimationFrame(applyFilter); + 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", + ), + ); }; - input.addEventListener("input", schedule); - input.addEventListener("keydown", (event) => { - if (event.key !== "Escape") return; - if (input.value) { - input.value = ""; - schedule(); - } else { - input.blur(); + const enhanceMenu = (menu) => { + if (!isSlashMenu(menu)) { + cleanupMenu(menu); + return; } - 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(); + 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; } - schedule(); - window.setTimeout(() => { - input.focus(); - input.select(); - }, 0); + 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 observer = new MutationObserver(schedule); - observer.observe(document.documentElement, { childList: true, subtree: true }); - document.addEventListener("keydown", onDocumentKeydown, true); - window.addEventListener("codexpp:settings-surface", schedule); - schedule(); + const activeSlashMenu = () => + Array.from(document.querySelectorAll(`[${MENU_ATTR}="true"]`)).find( + (menu) => + menu instanceof HTMLElement && + menu.isConnected && + menu.querySelector(".vertical-scroll-fade-mask"), + ); - api.log.info("settings search active"); + installDocumentHoverGuard(); + Element.prototype.scrollIntoView = patchedScrollIntoView; - return () => { - disposed = true; - observer.disconnect(); - document.removeEventListener("keydown", onDocumentKeydown, true); - window.removeEventListener("codexpp:settings-surface", schedule); - clearRevealTimers(); - clearHighlight(); - restoreHidden(document); - root.remove(); - style.remove(); + 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; }; - }, - - /** - * Match settings sidebar width to the main UI sidebar. - * - * Codex's main UI sidebar is `