From c3d4732707ba7648dc1dcd58c33af4f5a3b1b60c Mon Sep 17 00:00:00 2001
From: win9 <19194309279@163.com>
Date: Tue, 26 May 2026 16:47:40 +0800
Subject: [PATCH 1/3] Add Codex Context Ring Restore
---
index.json | 19 +-
scripts/codex-context-ring-restore.js | 852 ++++++++++++++++++++++++++
2 files changed, 870 insertions(+), 1 deletion(-)
create mode 100644 scripts/codex-context-ring-restore.js
diff --git a/index.json b/index.json
index f037443..ea64a5f 100644
--- a/index.json
+++ b/index.json
@@ -1,7 +1,24 @@
{
"version": 1,
- "updated_at": "2026-05-25T08:46:56Z",
+ "updated_at": "2026-05-26T00:00:00Z",
"scripts": [
+ {
+ "id": "codex-context-ring-restore",
+ "name": "Codex Context Ring Restore",
+ "description": "一款尽量还原官方圆环上下文体验的插件:贴近官方圆环的样式、位置与读数逻辑,尽可能保持原生使用感。",
+ "version": "0.1.2",
+ "author": "win9",
+ "tags": [
+ "context",
+ "ring",
+ "ui",
+ "restore",
+ "native"
+ ],
+ "homepage": "https://github.com/win9zhx/codex-context-ring-restore",
+ "script_url": "https://raw.githubusercontent.com/BigPizzaV3/CodexPlusPlusScriptMarket/main/scripts/codex-context-ring-restore.js",
+ "sha256": "e3d9409442f17112fe41542fefab8cd62b2776fc8808d8ac2d841630f127d676"
+ },
{
"id": "codex-context-used-meter",
"name": "Codex Context Used Meter",
diff --git a/scripts/codex-context-ring-restore.js b/scripts/codex-context-ring-restore.js
new file mode 100644
index 0000000..a8688a5
--- /dev/null
+++ b/scripts/codex-context-ring-restore.js
@@ -0,0 +1,852 @@
+/*
+@codex-plus-script
+name: Codex Context Ring Restore
+description: Restore the original official context-usage ring near the composer controls as much as possible, and inject a lightweight fallback ring only when the official ring is unavailable.
+version: 0.1.2
+author: jby
+*/
+
+(() => {
+ const INSTALL_KEY = "__codexContextRingRestoreInstalled";
+ const STYLE_ID = "codex-context-ring-restore-style";
+ const FALLBACK_ATTR = "data-codex-context-ring-restore";
+ const PANEL_VERSION = "context-ring-restore-3";
+ const CACHE_TTL_MS = 1500;
+ const CAPTURE_TEXT_HINT_RE = /context|token|tokens|usage|window|budget|remaining|compress this conversation|压缩此对话的上下文|上下文|令牌|使用|窗口/i;
+ const MAX_CAPTURE_TEXT_LENGTH = 800000;
+
+ let cachedContextUsage = { at: 0, value: null };
+ const officialMenuUsageByConversationId = new Map();
+ const capturedUsageByConversationId = new Map();
+ let captureInstalled = false;
+
+ if (window[INSTALL_KEY]) return;
+ window[INSTALL_KEY] = true;
+
+ function classNameText(node) {
+ return typeof node?.className === "string" ? node.className : "";
+ }
+
+ function isReasoningControl(node) {
+ if (!(node instanceof Element)) return false;
+ return node.matches("[data-codex-intelligence-trigger]") || !!node.querySelector("[data-codex-intelligence-trigger]");
+ }
+
+ function isModelControl(node) {
+ if (!(node instanceof Element)) return false;
+ if (!(node.matches(".h-token-button-composer") || node.querySelector(".h-token-button-composer"))) return false;
+ if (node.querySelector("[data-codex-intelligence-trigger]")) return false;
+ const label = [node.getAttribute("aria-label") || "", node.getAttribute("title") || ""].join(" ");
+ if (/隐藏边栏|显示边栏|hide sidebar|show sidebar/i.test(label)) return false;
+ return true;
+ }
+
+ function directChildContaining(parent, child) {
+ if (!(parent instanceof Element) || !(child instanceof Element)) return null;
+ return Array.from(parent.children).find((node) => node instanceof Element && (node === child || node.contains(child))) || null;
+ }
+
+ function findStructuralContextGroup(footer) {
+ if (!(footer instanceof Element)) return null;
+ const triggers = Array.from(footer.querySelectorAll("[data-codex-intelligence-trigger]"));
+ for (const trigger of triggers) {
+ let node = trigger.parentElement;
+ while (node && node !== footer) {
+ const className = classNameText(node);
+ if (className.includes("items-center") && node.querySelector(".h-token-button-composer")) {
+ const reasoningItem = directChildContaining(node, trigger);
+ const children = Array.from(node.children);
+ const modelItem = children.slice(0, Math.max(0, children.indexOf(reasoningItem))).reverse().find(isModelControl) || null;
+ if (modelItem && reasoningItem) return { group: node, modelItem, reasoningItem };
+ }
+ node = node.parentElement;
+ }
+ }
+ return null;
+ }
+
+ function findInlineContextGroup() {
+ const footer = document.querySelector(".composer-footer");
+ if (!(footer instanceof Element)) return null;
+ const structural = findStructuralContextGroup(footer);
+ if (structural) return structural.group;
+ const groups = Array.from(footer.querySelectorAll("div")).filter((node) => {
+ const className = classNameText(node);
+ if (!className.includes("items-center")) return false;
+ if (!node.querySelector(".h-token-button-composer")) return false;
+ return !!node.querySelector("button, [role='button'], [aria-haspopup='menu']");
+ });
+ return groups.find((node) => Array.from(node.children).some(isReasoningControl) && Array.from(node.children).some(isModelControl)) || null;
+ }
+
+ function findContextModelItem(group) {
+ if (!(group instanceof Element)) return null;
+ const structural = findStructuralContextGroup(document.querySelector(".composer-footer"));
+ if (structural?.group === group) return structural.modelItem;
+ return Array.from(group.children).find(isModelControl) || null;
+ }
+
+ function findContextReasoningItem(group) {
+ if (!(group instanceof Element)) return null;
+ const structural = findStructuralContextGroup(document.querySelector(".composer-footer"));
+ if (structural?.group === group) return structural.reasoningItem;
+ return Array.from(group.children).find((node) => node instanceof Element && isReasoningControl(node)) || null;
+ }
+
+ function normalizeSvgPathData(value) {
+ return String(value || "").replace(/[\s,]+/g, "").trim().toLowerCase();
+ }
+
+ function isOfficialContextRingVisual(node) {
+ if (!(node instanceof Element)) return false;
+ const className = classNameText(node);
+ if (!className.includes("size-token-button-composer")) return false;
+ if (!className.includes("items-center")) return false;
+ if (!className.includes("justify-center")) return false;
+ if (!className.includes("text-token-description-foreground")) return false;
+
+ const svg = node.querySelector("svg[aria-hidden='true']");
+ if (!svg) return false;
+ const circles = Array.from(svg.querySelectorAll("circle"));
+ if (circles.length !== 2) return false;
+ if (!circles.some((circle) => circle.hasAttribute("stroke-dasharray"))) return false;
+ if (!circles.some((circle) => circle.hasAttribute("stroke-dashoffset"))) return false;
+
+ const paths = Array.from(svg.querySelectorAll("path"));
+ if (!paths.length) return true;
+ const pathData = paths.map((path) => normalizeSvgPathData(path.getAttribute("d"))).join(" ");
+ return !pathData || (pathData.includes("a") && pathData.includes("0"));
+ }
+
+ function looksLikeContextRing(node) {
+ if (!(node instanceof Element)) return false;
+ if (node.closest("button[type='submit'], button[aria-label*='send' i], button[aria-label*='发送'], [data-testid*='send' i]")) return false;
+ const visual = node.matches(".size-token-button-composer, [class*='size-token-button-composer']")
+ ? node
+ : node.querySelector(".size-token-button-composer, [class*='size-token-button-composer']");
+ if (!(visual instanceof Element)) return false;
+ const text = (node.textContent || "").trim();
+ if (text.length > 12) return false;
+ return isOfficialContextRingVisual(visual);
+ }
+
+ function findContextRingHost(scope) {
+ const footer = document.querySelector(".composer-footer");
+ const root = scope instanceof Element ? scope : footer || document;
+ const candidates = Array.from(root.querySelectorAll(".size-token-button-composer, [class*='size-token-button-composer']"));
+ const ring = candidates.find((candidate) => looksLikeContextRing(candidate));
+ if (!(ring instanceof Element)) return null;
+ return ring.closest("span.flex.items-center, span") || ring;
+ }
+
+ function getOwnKeyByPrefix(target, prefix) {
+ if (!target || (typeof target !== "object" && typeof target !== "function")) return null;
+ return Object.keys(target).find((key) => key.startsWith(prefix)) || null;
+ }
+
+ function getReactFiber(target) {
+ const fiberKey = getOwnKeyByPrefix(target, "__reactFiber$");
+ if (fiberKey) return target[fiberKey];
+ const containerKey = getOwnKeyByPrefix(target, "__reactContainer$");
+ return containerKey ? target[containerKey] : null;
+ }
+
+ function getReactProps(target) {
+ const propsKey = getOwnKeyByPrefix(target, "__reactProps$");
+ return propsKey ? target[propsKey] : null;
+ }
+
+ function enqueueGraphValue(queue, seen, value, depth) {
+ if (!value || (typeof value !== "object" && typeof value !== "function")) return;
+ if (seen.has(value)) return;
+ seen.add(value);
+ queue.push({ value, depth });
+ }
+
+ function collectSearchRoots() {
+ const seeds = [];
+ const seenNodes = new Set();
+
+ function addNode(node) {
+ if (!(node instanceof Node) || seenNodes.has(node)) return;
+ seenNodes.add(node);
+ seeds.push(node);
+ }
+
+ const footer = document.querySelector(".composer-footer");
+ const editor = document.querySelector(".ProseMirror");
+
+ [
+ document.activeElement,
+ editor,
+ editor?.parentElement,
+ footer,
+ footer?.parentElement,
+ document.querySelector(".size-token-button-composer"),
+ document.querySelector("[data-codex-intelligence-trigger]"),
+ document.body,
+ ].forEach(addNode);
+
+ for (const start of Array.from(seenNodes)) {
+ let node = start;
+ let hops = 0;
+ while (node && hops < 6) {
+ addNode(node);
+ node = node.parentNode || (node instanceof ShadowRoot ? node.host : null);
+ hops += 1;
+ }
+ }
+
+ return seeds.flatMap((node) => [node, getReactFiber(node), getReactProps(node), node.pmViewDesc]).filter(Boolean);
+ }
+
+ function searchObjectGraph(roots, matcher, options = {}) {
+ const maxNodes = options.maxNodes ?? 9000;
+ const maxDepth = options.maxDepth ?? 10;
+ const seen = new WeakSet();
+ const queue = [];
+
+ roots.forEach((root) => enqueueGraphValue(queue, seen, root, 0));
+
+ let visited = 0;
+ while (queue.length && visited < maxNodes) {
+ const { value, depth } = queue.shift();
+ visited += 1;
+
+ try {
+ if (matcher(value)) return value;
+ } catch (_) {
+ // Ignore probing errors from host objects.
+ }
+
+ if (depth >= maxDepth) continue;
+
+ if (value instanceof Node) {
+ enqueueGraphValue(queue, seen, getReactFiber(value), depth + 1);
+ enqueueGraphValue(queue, seen, getReactProps(value), depth + 1);
+ enqueueGraphValue(queue, seen, value.pmViewDesc, depth + 1);
+ continue;
+ }
+
+ if (Array.isArray(value)) {
+ value.slice(0, 50).forEach((item) => enqueueGraphValue(queue, seen, item, depth + 1));
+ continue;
+ }
+
+ if (value instanceof Map) {
+ Array.from(value.values()).slice(0, 50).forEach((item) => enqueueGraphValue(queue, seen, item, depth + 1));
+ continue;
+ }
+
+ if (value instanceof Set) {
+ Array.from(value.values()).slice(0, 50).forEach((item) => enqueueGraphValue(queue, seen, item, depth + 1));
+ continue;
+ }
+
+ for (const key of Object.keys(value).slice(0, 80)) {
+ let nextValue;
+ try {
+ nextValue = value[key];
+ } catch (_) {
+ continue;
+ }
+ enqueueGraphValue(queue, seen, nextValue, depth + 1);
+ }
+ }
+
+ return null;
+ }
+
+ function findReactBackedValue(matcher) {
+ return searchObjectGraph(collectSearchRoots(), matcher);
+ }
+
+ function firstFiniteNumber(...values) {
+ for (const value of values) {
+ const number = Number(value);
+ if (Number.isFinite(number)) return number;
+ }
+ return null;
+ }
+
+ function normalizeConversationId(value) {
+ if (value == null) return null;
+ if (typeof value !== "string" && typeof value !== "number") return null;
+ const text = String(value).trim();
+ if (!text) return null;
+ const uuidMatch = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i.exec(text);
+ if (uuidMatch) return uuidMatch[0].toLowerCase();
+ return text.replace(/^[a-z]+:/i, "").toLowerCase();
+ }
+
+ function conversationIdsMatch(left, right) {
+ const normalizedLeft = normalizeConversationId(left);
+ const normalizedRight = normalizeConversationId(right);
+ return !!normalizedLeft && !!normalizedRight && normalizedLeft === normalizedRight;
+ }
+
+ function getElementConversationId(element) {
+ for (let node = element; node && node.nodeType === Node.ELEMENT_NODE; node = node.parentElement) {
+ const attrValue =
+ node.getAttribute("data-app-action-sidebar-thread-id") ||
+ node.getAttribute("data-thread-id") ||
+ node.getAttribute("data-conversation-id");
+ const normalized = normalizeConversationId(attrValue);
+ if (normalized) return normalized;
+ }
+ return null;
+ }
+
+ function readActiveConversationId() {
+ const selectors = [
+ `[aria-current="page"][data-app-action-sidebar-thread-id]`,
+ `[data-app-action-sidebar-thread-active="true"][data-app-action-sidebar-thread-id]`,
+ `[aria-selected="true"][data-app-action-sidebar-thread-id]`,
+ `[aria-current="page"]`,
+ `[data-app-action-sidebar-thread-active="true"]`,
+ `[aria-selected="true"]`,
+ ];
+
+ for (const selector of selectors) {
+ const element = document.querySelector(selector);
+ const conversationId = getElementConversationId(element);
+ if (conversationId) return conversationId;
+ }
+
+ const threadSurface = document.querySelector("[data-thread-id], [data-conversation-id], [data-app-action-sidebar-thread-id]");
+ return getElementConversationId(threadSurface);
+ }
+
+ function parseContextUsageShape(value) {
+ if (!value || typeof value !== "object") return null;
+
+ const modelContextWindow = firstFiniteNumber(
+ value.model_context_window,
+ value.modelContextWindow,
+ value.context_window,
+ value.contextWindow,
+ value.window_tokens,
+ value.windowTokens,
+ );
+
+ const lastUsage =
+ value.last_token_usage ||
+ value.lastTokenUsage ||
+ value.last_usage ||
+ value.lastUsage ||
+ value.last;
+
+ const totalTokens = firstFiniteNumber(
+ lastUsage && lastUsage.total_tokens,
+ lastUsage && lastUsage.totalTokens,
+ value.total_tokens,
+ value.totalTokens,
+ value.tokens_used,
+ value.tokensUsed,
+ value.used_tokens,
+ value.usedTokens,
+ );
+
+ if (!Number.isFinite(modelContextWindow) || modelContextWindow <= 0) return null;
+ if (!Number.isFinite(totalTokens) || totalTokens < 0) return null;
+
+ return {
+ modelContextWindow,
+ totalTokens,
+ };
+ }
+
+ function makeUsageReading(percent, usedTokens, contextWindow, exact = true) {
+ if (!Number.isFinite(percent)) return null;
+ const safePercent = Math.max(0, Math.min(100, Number(percent)));
+ const roundedPercent = Math.max(0, Math.min(100, Math.round(safePercent)));
+ const hasRatio = Number.isFinite(usedTokens) && Number.isFinite(contextWindow) && contextWindow > 0;
+ const normalizedUsedTokens = hasRatio ? Math.min(Number(usedTokens), Number(contextWindow)) : null;
+ const normalizedContextWindow = hasRatio ? Number(contextWindow) : null;
+ const remainingTokens = hasRatio ? Math.max(normalizedContextWindow - normalizedUsedTokens, 0) : null;
+
+ return {
+ exact,
+ percent: safePercent,
+ usedTokens: normalizedUsedTokens,
+ contextWindow: normalizedContextWindow,
+ remainingTokens,
+ summary: `已使用 ${roundedPercent}%`,
+ label: hasRatio
+ ? `${formatTokenCount(normalizedUsedTokens)} / ${formatTokenCount(normalizedContextWindow)}`
+ : `${roundedPercent}%`,
+ detail: hasRatio
+ ? `已用 ${formatTokenCount(normalizedUsedTokens)} / ${formatTokenCount(normalizedContextWindow)} tokens(${roundedPercent}%),剩余 ${formatTokenCount(remainingTokens)}。`
+ : `已使用 ${roundedPercent}%。`,
+ };
+ }
+
+ function isContextUsageInfo(value) {
+ return !!parseContextUsageShape(value);
+ }
+
+ function formatTokenCount(value) {
+ return Math.round(value).toLocaleString("en-US");
+ }
+
+ function buildExactContextUsage(info) {
+ const parsed = parseContextUsageShape(info);
+ if (!parsed) return null;
+ const contextWindow = Number(parsed.modelContextWindow);
+ const usedTokens = Math.min(Number(parsed.totalTokens), contextWindow);
+ const remainingTokens = Math.max(contextWindow - usedTokens, 0);
+ const percent = (usedTokens / contextWindow) * 100;
+ if (!Number.isFinite(percent)) return null;
+ return makeUsageReading(percent, usedTokens, contextWindow, true) || {
+ exact: true,
+ percent,
+ usedTokens,
+ contextWindow,
+ remainingTokens,
+ };
+ }
+
+ function buildExactContextUsageFromCandidate(value) {
+ if (!value || typeof value !== "object") return null;
+
+ const directReading = buildExactContextUsage(value);
+ if (directReading) return directReading;
+
+ if (
+ value.method === "thread/tokenUsage/updated" ||
+ value.type === "thread/tokenUsage/updated" ||
+ value.event === "thread/tokenUsage/updated"
+ ) {
+ const paramsReading =
+ buildExactContextUsage(value.params && value.params.tokenUsage) ||
+ buildExactContextUsage(value.params);
+ if (paramsReading) return paramsReading;
+ }
+
+ if (value.type === "token_count" || value.event === "token_count") {
+ const infoReading = buildExactContextUsage(value.info);
+ if (infoReading) return infoReading;
+ }
+
+ if (value.payload && (value.payload.type === "token_count" || value.payload.event === "token_count")) {
+ const payloadReading = buildExactContextUsage(value.payload.info);
+ if (payloadReading) return payloadReading;
+ }
+
+ const nestedReading = buildExactContextUsage(
+ value.contextUsage || value.context_usage || value.tokenUsage || value.token_usage || value.usage,
+ );
+ if (nestedReading) return nestedReading;
+
+ const infoReading = buildExactContextUsage(value.info);
+ if (infoReading) return infoReading;
+
+ return null;
+ }
+
+ function findExactContextUsage() {
+ const exactCandidate = findReactBackedValue(isContextUsageInfo);
+ if (exactCandidate) {
+ return buildExactContextUsage(exactCandidate);
+ }
+
+ const candidate = findReactBackedValue((value) => !!buildExactContextUsageFromCandidate(value));
+ return candidate ? buildExactContextUsageFromCandidate(candidate) : null;
+ }
+
+ function rememberCapturedUsage(usage, conversationId) {
+ if (!usage) return null;
+ const activeConversationId = normalizeConversationId(conversationId) || readActiveConversationId();
+ if (activeConversationId) {
+ capturedUsageByConversationId.set(activeConversationId, usage);
+ }
+ return usage;
+ }
+
+ function getRememberedCapturedUsage(conversationId) {
+ const activeConversationId = normalizeConversationId(conversationId) || readActiveConversationId();
+ return activeConversationId ? capturedUsageByConversationId.get(activeConversationId) || null : null;
+ }
+
+ function rememberOfficialMenuUsage(usage, conversationId) {
+ if (!usage) return null;
+ const activeConversationId = normalizeConversationId(conversationId) || readActiveConversationId();
+ if (activeConversationId) {
+ officialMenuUsageByConversationId.set(activeConversationId, usage);
+ }
+ return usage;
+ }
+
+ function getRememberedOfficialMenuUsage(conversationId) {
+ const activeConversationId = normalizeConversationId(conversationId) || readActiveConversationId();
+ return activeConversationId ? officialMenuUsageByConversationId.get(activeConversationId) || null : null;
+ }
+
+ function parseStructuredText(text) {
+ if (!text || !CAPTURE_TEXT_HINT_RE.test(text)) return null;
+
+ const percentFields = [
+ /["']?(?:context|token|usage|window)[A-Za-z0-9_$-]{0,36}(?:percent|percentage)["']?\s*[:=]\s*(\d{1,3}(?:\.\d+)?)/i,
+ /["']?(?:percent|percentage)[A-Za-z0-9_$-]{0,36}(?:context|token|usage|window)["']?\s*[:=]\s*(\d{1,3}(?:\.\d+)?)/i,
+ /["']?(?:context|token|usage|window)[A-Za-z0-9_$-]{0,36}(?:ratio)["']?\s*[:=]\s*(0?\.\d+|1(?:\.0+)?)/i,
+ ];
+
+ for (const pattern of percentFields) {
+ const match = pattern.exec(text);
+ if (!match) continue;
+ let percent = Number(match[1]);
+ if (pattern.source.includes("ratio")) percent *= 100;
+ if (Number.isFinite(percent) && percent >= 0 && percent <= 100) {
+ return makeUsageReading(percent, null, null, true);
+ }
+ }
+
+ const usedMatch =
+ /["']?(?:context|token|tokens)[A-Za-z0-9_$-]{0,36}(?:used|current|total|input)["']?\s*[:=]\s*(\d+(?:\.\d+)?)/i.exec(
+ text,
+ );
+ const limitMatch =
+ /["']?(?:context|token|tokens)[A-Za-z0-9_$-]{0,36}(?:limit|max|window|capacity|budget)["']?\s*[:=]\s*(\d+(?:\.\d+)?)/i.exec(
+ text,
+ );
+
+ if (usedMatch && limitMatch) {
+ const used = Number(usedMatch[1]);
+ const limit = Number(limitMatch[1]);
+ if (Number.isFinite(used) && Number.isFinite(limit) && limit > 0) {
+ return makeUsageReading((used / limit) * 100, used, limit, true);
+ }
+ }
+
+ return null;
+ }
+
+ function parsePayloadText(text) {
+ const clipped = String(text || "").slice(0, MAX_CAPTURE_TEXT_LENGTH);
+ if (!CAPTURE_TEXT_HINT_RE.test(clipped)) return null;
+
+ try {
+ const parsed = JSON.parse(clipped);
+ return buildExactContextUsageFromCandidate(parsed) || parseStructuredText(clipped);
+ } catch (_) {
+ return parseStructuredText(clipped);
+ }
+ }
+
+ function inspectCandidateText(text, conversationId) {
+ if (!text || text.length > MAX_CAPTURE_TEXT_LENGTH) return null;
+ if (!CAPTURE_TEXT_HINT_RE.test(text)) return null;
+ return rememberCapturedUsage(parsePayloadText(text), conversationId);
+ }
+
+ function inspectCandidateValue(value, conversationId) {
+ if (!value || typeof value !== "object") return null;
+ return rememberCapturedUsage(buildExactContextUsageFromCandidate(value), conversationId);
+ }
+
+ function installFetchCapture() {
+ const patchedFlag = "__codexContextRingRestoreFetchPatched";
+ if (window[patchedFlag] || typeof window.fetch !== "function") return;
+
+ const nativeFetch = window.fetch.bind(window);
+ window.fetch = function codexContextRingRestoreFetch(...args) {
+ return nativeFetch(...args).then((response) => {
+ try {
+ const urlText = String(args[0] && (args[0].url || args[0].href || args[0]) || response.url || "");
+ if (urlText && !CAPTURE_TEXT_HINT_RE.test(urlText)) return response;
+
+ const contentType = response.headers && response.headers.get("content-type");
+ const contentLength = response.headers && Number(response.headers.get("content-length"));
+ const isTextLike = !contentType || /json|text|event-stream|x-ndjson/i.test(contentType);
+ if (isTextLike && (!Number.isFinite(contentLength) || contentLength <= MAX_CAPTURE_TEXT_LENGTH)) {
+ response.clone().text().then((text) => {
+ inspectCandidateText(text, readActiveConversationId());
+ schedule();
+ }).catch(() => {});
+ }
+ } catch (_) {
+ return response;
+ }
+ return response;
+ });
+ };
+
+ window[patchedFlag] = true;
+ }
+
+ function installWebSocketCapture() {
+ const patchedFlag = "__codexContextRingRestoreWebSocketPatched";
+ if (window[patchedFlag] || typeof window.WebSocket !== "function") return;
+
+ const NativeWebSocket = window.WebSocket;
+ function ContextRingRestoreWebSocket(...args) {
+ const socket = new NativeWebSocket(...args);
+ socket.addEventListener("message", (event) => {
+ try {
+ if (typeof event.data === "string") {
+ inspectCandidateText(event.data, readActiveConversationId());
+ } else if (event.data instanceof Blob && event.data.size <= MAX_CAPTURE_TEXT_LENGTH) {
+ event.data.text().then((text) => {
+ inspectCandidateText(text, readActiveConversationId());
+ schedule();
+ }).catch(() => {});
+ return;
+ }
+ schedule();
+ } catch (_) {
+ return;
+ }
+ });
+ return socket;
+ }
+
+ ContextRingRestoreWebSocket.prototype = NativeWebSocket.prototype;
+ ContextRingRestoreWebSocket.CONNECTING = NativeWebSocket.CONNECTING;
+ ContextRingRestoreWebSocket.OPEN = NativeWebSocket.OPEN;
+ ContextRingRestoreWebSocket.CLOSING = NativeWebSocket.CLOSING;
+ ContextRingRestoreWebSocket.CLOSED = NativeWebSocket.CLOSED;
+ window.WebSocket = ContextRingRestoreWebSocket;
+ window[patchedFlag] = true;
+ }
+
+ function installPostMessageCapture() {
+ const listenerKey = "__codexContextRingRestorePostMessageListener";
+ if (window[listenerKey]) return;
+
+ const listener = (event) => {
+ try {
+ if (typeof event.data === "string") {
+ inspectCandidateText(event.data, readActiveConversationId());
+ } else {
+ inspectCandidateValue(event.data, readActiveConversationId());
+ }
+ schedule();
+ } catch (_) {
+ return;
+ }
+ };
+
+ window.addEventListener("message", listener, true);
+ window[listenerKey] = listener;
+ }
+
+ function installCaptureHooks() {
+ if (captureInstalled) return;
+ captureInstalled = true;
+ installFetchCapture();
+ installWebSocketCapture();
+ installPostMessageCapture();
+ }
+
+ function findOfficialMenuContextUsage() {
+ const items = Array.from(document.querySelectorAll("button, [role='button'], [cmdk-item], [data-command], li, div"));
+ for (const item of items) {
+ if (!(item instanceof Element)) continue;
+ const text = item.textContent?.replace(/\s+/g, " ").trim() || "";
+ if (!text) continue;
+ if (!/压缩此对话的上下文|compress this conversation/i.test(text)) continue;
+ const percentMatch = /已使用\s*(\d{1,3}(?:\.\d+)?)%|used\s*(\d{1,3}(?:\.\d+)?)%/i.exec(text);
+ if (!percentMatch) continue;
+ const percent = firstFiniteNumber(percentMatch[1], percentMatch[2]);
+ if (!Number.isFinite(percent)) continue;
+ return makeUsageReading(percent, null, null, true);
+ }
+ return null;
+ }
+
+ function getContextUsage() {
+ const now = Date.now();
+ if (cachedContextUsage.value && now - cachedContextUsage.at < CACHE_TTL_MS) {
+ return cachedContextUsage.value;
+ }
+ installCaptureHooks();
+ const activeConversationId = readActiveConversationId();
+ const runtimeUsage =
+ rememberCapturedUsage(findExactContextUsage(), activeConversationId) ||
+ getRememberedCapturedUsage(activeConversationId);
+ const officialMenuUsage =
+ rememberOfficialMenuUsage(findOfficialMenuContextUsage(), activeConversationId) ||
+ getRememberedOfficialMenuUsage(activeConversationId);
+ const usage = runtimeUsage || officialMenuUsage || approximateContextUsage();
+ cachedContextUsage = { at: now, value: usage };
+ return usage;
+ }
+
+ function stabilizeInlineContextRing() {
+ const group = findInlineContextGroup();
+ const host = findContextRingHost(document);
+ if (!(group instanceof Element) || !(host instanceof Element)) return false;
+ if (!looksLikeContextRing(host)) return false;
+ host.hidden = false;
+ host.removeAttribute("hidden");
+ host.style.removeProperty("display");
+ host.style.removeProperty("visibility");
+ host.style.removeProperty("opacity");
+ if (group.contains(host)) return true;
+ const modelItem = findContextModelItem(group);
+ if (!modelItem) return false;
+ group.insertBefore(host, modelItem.nextSibling);
+ return true;
+ }
+
+ function cleanupBrokenContextRingSlot() {
+ document.querySelectorAll(`[${FALLBACK_ATTR}='slot']`).forEach((node) => node.remove());
+ }
+
+ function contextColor(percent) {
+ if (percent >= 85) return "#e25555";
+ if (percent >= 65) return "#d98f28";
+ return "#339cff";
+ }
+
+ function updateFallbackContextUsageRing(button, usage) {
+ const percent = Math.max(0, Math.min(100, Number(usage?.percent || 0)));
+ button.style.setProperty("--codex-context-offset", String(100 - percent));
+ button.style.setProperty("--codex-context-color", contextColor(percent));
+ const summary = usage?.summary || `已使用 ${percent}%`;
+ const detail = usage?.detail || "上下文使用情况为粗略估算。";
+ button.dataset.exact = usage?.exact ? "1" : "0";
+ button.setAttribute("aria-label", `上下文使用情况:${summary}`);
+ button.setAttribute("title", `${summary}\n${detail}`);
+ const label = button.querySelector(`[${FALLBACK_ATTR}='label']`);
+ if (label) {
+ label.textContent = usage?.label || `${Math.round(percent)}%`;
+ }
+ }
+
+ function approximateContextUsage() {
+ const text = document.body?.innerText || "";
+ const normalized = text.replace(/\s+/g, " ").trim();
+ const size = normalized.length;
+ const estimated = Math.min(92, Math.max(4, Math.round(size / 180)));
+ const inferredWindow = Math.max(1, size * 2);
+ const inferredUsed = Math.max(1, Math.round((estimated / 100) * inferredWindow));
+ return {
+ exact: false,
+ percent: estimated,
+ summary: `已使用约 ${estimated}%`,
+ label: `${formatTokenCount(inferredUsed)} / ${formatTokenCount(inferredWindow)}`,
+ detail: `已用约 ${formatTokenCount(inferredUsed)} / ${formatTokenCount(inferredWindow)} tokens(${estimated}%)。`,
+ };
+ }
+
+ function ensureFallbackContextUsageRing() {
+ const group = findInlineContextGroup();
+ if (!(group instanceof Element)) return false;
+ if (findContextRingHost(group)) return false;
+ const modelItem = findContextModelItem(group);
+ if (!modelItem) return false;
+ let container = group.querySelector(`[${FALLBACK_ATTR}='container']`);
+ if (!(container instanceof HTMLSpanElement)) {
+ container = document.createElement("span");
+ container.className = "codex-context-ring-restore-container";
+ container.setAttribute(FALLBACK_ATTR, "container");
+ container.innerHTML = `
+
+
+
+
+ `;
+ }
+ const button = container.querySelector(`[${FALLBACK_ATTR}='button']`);
+ if (!(button instanceof HTMLElement)) return false;
+ const reasoningItem = findContextReasoningItem(group);
+ if (reasoningItem) group.insertBefore(container, reasoningItem);
+ else group.insertBefore(container, modelItem.nextSibling);
+ updateFallbackContextUsageRing(button, getContextUsage());
+ return true;
+ }
+
+ function repairContextRingLayout() {
+ cleanupBrokenContextRingSlot();
+ if (stabilizeInlineContextRing()) return;
+ ensureFallbackContextUsageRing();
+ }
+
+ function installStyle() {
+ const existing = document.getElementById(STYLE_ID);
+ if (existing?.dataset.version === PANEL_VERSION) return;
+ existing?.remove();
+
+ const style = document.createElement("style");
+ style.id = STYLE_ID;
+ style.dataset.version = PANEL_VERSION;
+ style.textContent = `
+ .codex-context-ring-restore-container {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ min-width: 0;
+ flex: 0 0 auto;
+ color: #b8c0cc;
+ }
+ .codex-context-ring-restore-button {
+ position: relative;
+ display: inline-flex;
+ width: 28px;
+ height: 28px;
+ min-width: 28px;
+ align-items: center;
+ justify-content: center;
+ border-radius: 999px;
+ background: transparent;
+ padding: 0;
+ flex: 0 0 28px;
+ }
+ .codex-context-ring-restore-svg {
+ width: 20px;
+ height: 20px;
+ transform: translateZ(0);
+ }
+ .codex-context-ring-restore-label {
+ max-width: 112px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ font-size: 12px;
+ line-height: 1;
+ color: #b8c0cc;
+ font-variant-numeric: tabular-nums;
+ }
+ .codex-context-ring-restore-track {
+ fill: none;
+ stroke: rgba(184,192,204,.24);
+ stroke-width: 4;
+ }
+ .codex-context-ring-restore-progress {
+ fill: none;
+ stroke: var(--codex-context-color, #339cff);
+ stroke-width: 4;
+ stroke-linecap: round;
+ stroke-dasharray: 100;
+ stroke-dashoffset: var(--codex-context-offset, 100);
+ transition: stroke-dashoffset .18s ease, stroke .18s ease;
+ }
+ `;
+ document.documentElement.appendChild(style);
+ }
+
+ function scan() {
+ installStyle();
+ requestAnimationFrame(() => {
+ repairContextRingLayout();
+ });
+ }
+
+ function schedule() {
+ if (window.__codexContextRingRestoreQueued) return;
+ window.__codexContextRingRestoreQueued = true;
+ requestAnimationFrame(() => {
+ window.__codexContextRingRestoreQueued = false;
+ scan();
+ });
+ }
+
+ schedule();
+ window.__codexContextRingRestoreObserver?.disconnect?.();
+ window.__codexContextRingRestoreObserver = new MutationObserver(schedule);
+ window.__codexContextRingRestoreObserver.observe(document.documentElement, { childList: true, subtree: true });
+})();
From 1ad5af8f80f54306cf93e7b0daa9daf8cda419ca Mon Sep 17 00:00:00 2001
From: win9 <19194309279@163.com>
Date: Tue, 26 May 2026 16:54:16 +0800
Subject: [PATCH 2/3] Update author and description
---
index.json | 2 +-
scripts/codex-context-ring-restore.js | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/index.json b/index.json
index ea64a5f..813858b 100644
--- a/index.json
+++ b/index.json
@@ -7,7 +7,7 @@
"name": "Codex Context Ring Restore",
"description": "一款尽量还原官方圆环上下文体验的插件:贴近官方圆环的样式、位置与读数逻辑,尽可能保持原生使用感。",
"version": "0.1.2",
- "author": "win9",
+ "author": "win9zhx",
"tags": [
"context",
"ring",
diff --git a/scripts/codex-context-ring-restore.js b/scripts/codex-context-ring-restore.js
index a8688a5..8ce8a19 100644
--- a/scripts/codex-context-ring-restore.js
+++ b/scripts/codex-context-ring-restore.js
@@ -3,7 +3,7 @@
name: Codex Context Ring Restore
description: Restore the original official context-usage ring near the composer controls as much as possible, and inject a lightweight fallback ring only when the official ring is unavailable.
version: 0.1.2
-author: jby
+author: win9zhx
*/
(() => {
From 983563bb1682a04e6eac315201a70c23032e13f3 Mon Sep 17 00:00:00 2001
From: win9 <19194309279@163.com>
Date: Tue, 26 May 2026 16:56:45 +0800
Subject: [PATCH 3/3] Remove standalone repo dependency
---
index.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/index.json b/index.json
index 813858b..5f6e0a5 100644
--- a/index.json
+++ b/index.json
@@ -15,7 +15,7 @@
"restore",
"native"
],
- "homepage": "https://github.com/win9zhx/codex-context-ring-restore",
+ "homepage": "https://github.com/win9zhx/CodexPlusPlusScriptMarket/tree/add-codex-context-ring-restore/scripts",
"script_url": "https://raw.githubusercontent.com/BigPizzaV3/CodexPlusPlusScriptMarket/main/scripts/codex-context-ring-restore.js",
"sha256": "e3d9409442f17112fe41542fefab8cd62b2776fc8808d8ac2d841630f127d676"
},