diff --git a/.gitattributes b/.gitattributes
index 9a1b3e7..0099425 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -4,5 +4,6 @@
windows/tests/fixtures/payload-root/** -text
windows/tests/fixtures/payload-manifest.*.json -text
windows/tests/fixtures/end-to-end/script-market/** -text
+Resources/script-market-sources/** -text
windows/vendor/plugin-seeds/** -text
patches/CodexPlusPlus/*.patch -text
diff --git a/.github/workflows/online-release.yml b/.github/workflows/online-release.yml
index e531d36..b197770 100644
--- a/.github/workflows/online-release.yml
+++ b/.github/workflows/online-release.yml
@@ -12,7 +12,7 @@ on:
release_tag:
description: Release tag
required: true
- default: v1.0.0-online
+ default: v1.1.0-online
push:
tags:
- 'v*-online'
@@ -51,8 +51,13 @@ jobs:
- name: Download Codex++ Windows installer
id: cpp
shell: pwsh
+ env:
+ GH_TOKEN: ${{ github.token }}
run: |
- $release = Invoke-RestMethod -Headers @{'User-Agent'='Uni-codex'} -Uri 'https://api.github.com/repos/BigPizzaV3/CodexPlusPlus/releases/latest'
+ $release = Invoke-RestMethod -Headers @{
+ 'User-Agent'='Uni-codex'
+ 'Authorization'="Bearer $env:GH_TOKEN"
+ } -Uri 'https://api.github.com/repos/BigPizzaV3/CodexPlusPlus/releases/latest'
$asset = @($release.assets | Where-Object name -match 'windows-x64-setup\.exe$')
if ($asset.Count -ne 1) { throw 'Codex++ Windows asset missing or ambiguous' }
$path = Join-Path $env:RUNNER_TEMP $asset[0].name
@@ -75,8 +80,52 @@ jobs:
runs-on: macos-latest
steps:
- uses: actions/checkout@v6
+ - name: Download Codex++ macOS installers
+ shell: bash
+ env:
+ GH_TOKEN: ${{ github.token }}
+ run: |
+ release_json="$RUNNER_TEMP/codex-plus.json"
+ curl --fail --location --retry 3 --connect-timeout 20 --max-time 1800 \
+ -H 'User-Agent: Uni-codex' \
+ -H "Authorization: Bearer $GH_TOKEN" \
+ 'https://api.github.com/repos/BigPizzaV3/CodexPlusPlus/releases/latest' \
+ -o "$release_json"
+ python3 - "$release_json" "$RUNNER_TEMP" <<'PY'
+ import hashlib
+ import json
+ import pathlib
+ import subprocess
+ import sys
+
+ release = json.loads(pathlib.Path(sys.argv[1]).read_text())
+ output = pathlib.Path(sys.argv[2])
+ assets = {asset['name']: asset for asset in release.get('assets', [])}
+ for architecture in ('arm64', 'x64'):
+ name = f"CodexPlusPlus-{release['tag_name'].lstrip('v')}-macos-{architecture}.dmg"
+ asset = assets.get(name)
+ if asset is None:
+ raise SystemExit(f'missing Codex++ macOS asset: {name}')
+ destination = output / f'CodexPlusPlus-{architecture}.dmg'
+ subprocess.run([
+ 'curl', '--fail', '--location', '--retry', '3',
+ '--connect-timeout', '20', '--max-time', '1800',
+ '-o', str(destination), asset['browser_download_url'],
+ ], check=True)
+ digest = asset.get('digest', '')
+ if not digest.startswith('sha256:'):
+ raise SystemExit(f'Codex++ asset has no SHA-256 digest: {name}')
+ actual = hashlib.sha256(destination.read_bytes()).hexdigest()
+ if actual != digest.removeprefix('sha256:'):
+ raise SystemExit(f'Codex++ SHA-256 mismatch: {name}')
+ PY
- name: Build macOS online DMG
- run: bash macos/online/build-online-dmg.sh dist/macos
+ run: |
+ CodexPlusPlusArmDmg="$RUNNER_TEMP/CodexPlusPlus-arm64.dmg"
+ CodexPlusPlusX64Dmg="$RUNNER_TEMP/CodexPlusPlus-x64.dmg"
+ bash macos/online/build-online-dmg.sh dist/macos \
+ "$CodexPlusPlusArmDmg" \
+ "$CodexPlusPlusX64Dmg"
- uses: actions/upload-artifact@v7
with:
name: online-macos
diff --git a/.github/workflows/windows-ci.yml b/.github/workflows/windows-ci.yml
index a244e8f..01c86e9 100644
--- a/.github/workflows/windows-ci.yml
+++ b/.github/workflows/windows-ci.yml
@@ -70,12 +70,14 @@ jobs:
}).Count -ne 1) { throw "NSIS makensis.exe must report exact version v3.12" }
$root >> $env:GITHUB_PATH
- name: Core full serialized tests
+ timeout-minutes: 15
shell: pwsh
run: |
New-Item -ItemType Directory -Path test-results/core -Force | Out-Null
dotnet test windows/tests/CodexOneClick.Core.Tests/CodexOneClick.Core.Tests.csproj `
-c Release --no-restore `
-m:1 -p:TestTfmsInParallel=false `
+ --blame-hang --blame-hang-timeout 2m `
--logger "trx;LogFileName=core-targeted.trx" `
--results-directory test-results/core
- name: WPF startup, view-model, and layout tests
@@ -139,7 +141,7 @@ jobs:
name: windows-pr-test-evidence
if-no-files-found: error
path: |
- test-results/core/*.trx
+ test-results/core/**
test-results/layout/*.trx
test-results/layout/screenshots/*.png
test-results/runtime/*.json
diff --git a/.github/workflows/windows-release.yml b/.github/workflows/windows-release.yml
index 1326622..16037d1 100644
--- a/.github/workflows/windows-release.yml
+++ b/.github/workflows/windows-release.yml
@@ -127,16 +127,16 @@ jobs:
shell: pwsh
run: |
windows/scripts/build-codex-plus-compatibility-payload.ps1 `
- -Tag v1.2.43 `
- -Patch patches/CodexPlusPlus/v1.2.43-cross-provider-history.patch `
+ -Tag v1.2.44 `
+ -Patch patches/CodexPlusPlus/v1.2.44-cross-provider-history.patch `
-OutputRoot windows/build/codex-plus-compat
- name: Refresh complete official offline payload
shell: pwsh
run: |
windows/scripts/refresh-offline-payloads.ps1 `
-OutputRoot windows/vendor/offline-payloads `
- -CodexPlusPlusSetup windows/build/codex-plus-compat/CodexPlusPlus-1.2.43-codexkit.1-windows-x64-setup.exe `
- -CodexPlusPlusSource windows/build/codex-plus-compat/CodexPlusPlus-v1.2.43-codexkit.1-source.tar.gz
+ -CodexPlusPlusSetup windows/build/codex-plus-compat/CodexPlusPlus-1.2.44-codexkit.1-windows-x64-setup.exe `
+ -CodexPlusPlusSource windows/build/codex-plus-compat/CodexPlusPlus-v1.2.44-codexkit.1-source.tar.gz
- name: Strict payload signatures and licenses
id: payload
shell: pwsh
diff --git a/README.md b/README.md
index 1c649ef..ea2b2b6 100644
--- a/README.md
+++ b/README.md
@@ -1,118 +1,140 @@
-# Codex 一键安装生态
+# Uni-codex
-本仓库提供 macOS 与 Windows 的 Codex/Codex++ 部署工具,并连接 Uni-Scholar 科研生态。
+
-## 生态优势:连续科研工作流
+**给普通用户准备的 Codex + Codex++ 一键安装器**
-- [Uni-Scholar 云端工作站](https://uni-scholar.asia):云端检索、协作与长流程科研任务。
-- [Research Kit 本地中枢](https://uni-scholar.asia/research-kit):连接 Zotero、Obsidian 与本地知识库。
-- Codex/Codex++ 执行层:完成代码、文档、数据处理与自动化。
+不用 Git,不用命令行,不用配置开发环境。下载、双击,跟着提示完成安装。
-三层共同构成从知识沉淀、研究推理到执行交付的连续科研工作流。Research Kit 当前为 macOS 生态产品;Windows 用户可把它作为生态链接和可选配套,但它不随 Windows 安装包预装。
+[](https://github.com/fancr-code/Uni-codex/releases/download/v1.1.0-online/Uni-codex-Windows-x64-Online-Setup.exe)
+[](https://github.com/fancr-code/Uni-codex/releases/download/v1.1.0-online/Uni-codex-macOS-Online.dmg)
-## 在线精简版 Release
+[查看最新版本](https://github.com/fancr-code/Uni-codex/releases/latest) · [问题反馈](https://github.com/fancr-code/Uni-codex/issues)
-Release 提供 Windows x64 与 macOS 在线精简安装包。安装包不镜像、不再分发官方 Codex 桌面应用:Windows 安装时通过 Microsoft Store 产品 `9PLM9XGG6VKS` 获取,macOS 安装时从 OpenAI 官方 `persistent.oaistatic.com` 获取。Codex++ 从其 GitHub Release 获取或随 Windows 引导器集成。
+
-- `Uni-codex-Windows-x64-Online-Setup.exe`
-- `Uni-codex-macOS-Online.dmg`
+## 为什么用 Uni-codex?
-构建入口为 `.github/workflows/online-release.yml`。推送形如 `v1.0.0-online` 的标签或手动运行工作流即可创建 Release。安装过程需要网络。
+官方 Codex 很强,但第一次安装、寻找正确版本、安装 Codex++,对新手并不直观。Uni-codex 把这些步骤放进一个图形化安装包里。
-本仓库不公开镜像分发官方 Codex 桌面应用、Microsoft Store 包或未声明许可证的脚本市场源码。最低 Windows 系统仍为 Windows 10 版本 1809(Build 17763),仅支持 x64。
+| 自己配置 | 使用 Uni-codex |
+| --- | --- |
+| 查找适合系统的 Codex 下载来源 | 自动获取对应平台的官方 Codex |
+| 手动下载并安装 Codex++ | 安装器统一处理 |
+| 分辨 Windows、Apple Silicon、Intel 版本 | 自动识别或提供正确入口 |
+| 阅读多份安装说明 | 跟着安装向导操作即可 |
+| 容易重复安装或覆盖已有版本 | 优先复用已安装且可用的 Codex |
-完整说明见 [Windows 文档](windows/README.md)。
+> Uni-codex 是轻量的**在线安装器**:安装时从官方来源获取最新组件,因此安装包体积小,使用时需要联网。
-## Windows 能力
+## 一键安装
-默认纯 API,支持 DeepSeek、Kimi 开放平台、Kimi Code、智谱 GLM、阿里千问、Xiaomi MiMo。安装器内提供各服务商官方 API Key 申请入口,并优先刷新上游模型;断网或刷新失败时回退到随包校验的离线快照,Kimi 开放平台快照包含 Kimi K3。
+### Windows
-推荐“OpenAI 账号 + 所选国产 API”:可在安装器内点击“获取 OpenAI 授权”,但实际模型调用仍由所选服务商完成。OpenAI 授权不等于 GPT API Key,不赠送 GPT API 额度,也不会获得账号未拥有的权限。
+支持 **Windows 10 1809 及以上版本、Windows 11(x64)**。
-安装器会复用健康 Codex,不重复安装或降级。Codex++ 带有 `cross-provider-content-v1`;同时配置 3 个插件市场和 9 个插件。用户可从作者的上游地址直接获取 `Context Used Meter` 与 `Codex Token Usage`;本公开仓库不镜像这些未声明许可证的脚本源码。API Key、OAuth Token、设备码、对话正文与敏感报告字段都会被排除或脱敏。
+1. 下载 [Windows 一键安装包](https://github.com/fancr-code/Uni-codex/releases/download/v1.1.0-online/Uni-codex-Windows-x64-Online-Setup.exe)。
+2. 双击 `Uni-codex-Windows-x64-Online-Setup.exe`。
+3. 按安装向导提示完成安装。
-## macOS
+安装器会通过微软官方渠道获取 Codex,并完成 Codex++ 的安装或集成。
-macOS 安装器支持 Apple Silicon 与 Intel。构建入口:
+### macOS
-```bash
-bash build-codex-one-click-installer.sh
-```
+支持 **Apple Silicon(M 系列)和 Intel Mac**。
-macOS 同样默认使用所选 API 服务商,可选 OpenAI 账号授权,并复用健康应用。具体构建、签名、硬件烟测和分发边界以仓库内 macOS 脚本及安装包引导为准。
+1. 下载 [macOS 一键安装包](https://github.com/fancr-code/Uni-codex/releases/download/v1.1.0-online/Uni-codex-macOS-Online.dmg)。
+2. 打开 `Uni-codex-macOS-Online.dmg`。
+3. 按窗口中的说明完成安装。
-### 开发构建与测试
+安装器会识别 Mac 的芯片类型,从 OpenAI 官方地址获取适合的 Codex,并安装 Codex++。
-```bash
-# macOS 构建与测试
-bash build-codex-one-click-installer.sh
-bash tests/run-all-tests.sh
-```
+## 你会得到什么?
-```powershell
-# Windows 开发构建与测试(从仓库根目录,在 Windows x64)
-# 前置:.NET 8 SDK、Node.js 22、Rust stable、NSIS、Inno Setup 7.0.2,
-# 并确保 dotnet、node、cargo、makensis、ISCC.exe 均在 PATH。
-# StorePackageResolver 会按 ProductId 从微软 DisplayCatalog/FE3 解析当前官方 MSIX;
-# 构建不会信任会变化为联网引导 EXE 的 get.microsoft.com 下载响应。
-dotnet restore windows/CodexOneClickInstaller.sln --locked-mode
-$compat = 'windows/build/codex-plus-compat'
-pwsh windows/scripts/build-codex-plus-compatibility-payload.ps1 `
- -Tag v1.2.43 `
- -Patch patches/CodexPlusPlus/v1.2.43-cross-provider-history.patch `
- -OutputRoot $compat
-
-pwsh windows/scripts/refresh-offline-payloads.ps1 `
- -OutputRoot windows/vendor/offline-payloads `
- -CodexPlusPlusSetup "$compat/CodexPlusPlus-1.2.43-codexkit.1-windows-x64-setup.exe" `
- -CodexPlusPlusSource "$compat/CodexPlusPlus-v1.2.43-codexkit.1-source.tar.gz"
-
-. windows/scripts/offline-payload-supply.ps1
-$active = Resolve-ActivePayloadRoot (
- [IO.Path]::GetFullPath('windows/vendor/offline-payloads'))
-$iscc = (Get-Command ISCC.exe -ErrorAction Stop).Source
-
-pwsh windows/tests/run-all-tests.ps1 `
- -PayloadRoot $active `
- -BuiltRoot $compat `
- -TestResultsRoot windows/test-results/full
-pwsh windows/scripts/build-installer.ps1 `
- -PayloadRoot $active -OutputDir dist -IsccPath $iscc
-```
+- **Codex 桌面版**:由官方来源下载,不在本仓库重复打包。
+- **Codex++**:随引导流程安装,减少手动配置步骤。
+- **211 个科研 Skills**:在线版与离线版都预装 Nature Skills、Scientific Agent Skills 和 Research Skills。
+- **跨平台支持**:一个项目同时覆盖 Windows 和 macOS。
+- **已有安装保护**:检测到健康的 Codex 时优先复用,避免无意义地重复安装或降级。
+- **更适合中文用户**:下载入口、安装说明和常见问题集中在同一页面。
+
+## 可选模型与科研生态
+
+Windows 配置工具支持 DeepSeek、Kimi 开放平台、Kimi Code、智谱 GLM、阿里千问和 Xiaomi MiMo 等 API 服务商,并提供相应的官方 API Key 申请入口。
+
+Uni-codex 也可以配合以下工具形成连续科研工作流:
+
+- [Uni-Scholar 云端工作站](https://uni-scholar.asia):检索、协作与长流程科研任务。
+- [Research Kit](https://uni-scholar.asia/research-kit):连接 Zotero、Obsidian 与本地知识库,目前主要面向 macOS。
+- Codex / Codex++:执行代码、文档、数据处理与自动化任务。
+
+这些功能均为可选项。只想安装 Codex 的用户,直接使用上方安装包即可。
+
+### 开箱即用的科研 Skills
+
+安装完成后,Codex 会自动获得三套经过固定版本管理的开源技能合集:
+
+- [Nature Skills](https://github.com/Yuan1z0825/nature-skills):Nature 风格论文写作、润色、审稿、作图和投稿工作流,共 19 个技能。
+- [Scientific Agent Skills](https://github.com/K-Dense-AI/scientific-agent-skills):覆盖生物、化学、医学、数据分析和科研数据库等场景,共 158 个技能。
+- [Research Skills](https://github.com/neuromechanist/research-skills):研究规划、实验设计、文献与图表处理、工程化研究流程,共 34 个技能。
+
+三套合集合计 **211 个技能**。在线安装包在安装时获取锁定版本;离线安装包在构建时已将同一版本完整打包。安装器会保留用户自己维护的同名技能,不会静默覆盖。
+三套合集的 MIT 许可证会随技能一并保留在 `.codex/skills/.uni-codex-licenses/`。
+
+## 常见问题
+
+### 为什么安装包这么小?
+
+因为它是在线安装器,不把体积较大的官方 Codex 应用重复塞进仓库。运行安装器后,才会从 OpenAI、Microsoft 和相关项目的官方地址下载所需组件。
-### 项目结构
+### 安装时必须联网吗?
+
+是。下载 Codex、Codex++,以及刷新上游模型信息都需要网络。
+
+### Release 里的 Source code 是安装包吗?
+
+不是。`Source code (zip)` 和 `Source code (tar.gz)` 是 GitHub 为每个版本标签自动生成的源码快照。普通用户只需下载 `.exe` 或 `.dmg`。
+
+### OpenAI 授权等于 API Key 吗?
+
+不等于。OpenAI 账号授权不会赠送 GPT API 额度,也不会获得账号原本没有的权限。选择第三方 API 服务商时,实际模型调用及费用由相应服务商负责。
+
+### 这是 OpenAI 官方项目吗?
+
+不是。Uni-codex 是社区项目,与 OpenAI 没有隶属关系。Codex 的名称、商标和官方应用归其各自权利人所有。
+
+## 安全与隐私
+
+- Codex 桌面应用只从 OpenAI 或 Microsoft 官方渠道获取。
+- Codex++ 从其 GitHub Release 获取,或由安装引导器集成。
+- 本仓库不镜像分发官方 Codex 桌面应用、Microsoft Store 包或许可证不明确的脚本源码。
+- API Key、OAuth Token、设备码、对话正文等敏感内容不会作为公开构建产物上传。
+- 请不要在 Issue、日志、截图或公开配置中提交任何密钥和令牌。
+
+## 给开发者
+
+普通用户不需要阅读本节。项目的在线发布入口是 [`.github/workflows/online-release.yml`](.github/workflows/online-release.yml),版本标签格式为 `v*-online`。
```text
.
-├── README.md
-├── Resources/ # macOS 资源与安装包指南
-├── scripts/ # macOS 构建、验证与烟测
-├── tests/ # macOS 测试
-├── windows/
-│ ├── README.md
-│ ├── installer/ # Inno Setup 外层安装器
-│ ├── src/ # WPF GUI 与 InstallerCore
-│ ├── scripts/ # Windows 构建与载荷工具
-│ ├── tests/ # Windows 契约与运行时测试
-│ └── docs/guides/ # Windows 随包指南
-└── .github/workflows/ # Windows CI 与正式发布工作流
+├── macos/ # macOS 在线安装器
+├── windows/ # Windows 安装器与配置工具
+├── scripts/ # 构建、验证与烟雾测试
+├── tests/ # 自动化测试
+├── Resources/ # macOS 资源
+└── .github/workflows/ # CI 与 Release 工作流
```
-## 安全边界
-
-- 只从明确的官方服务商页面或已取得再分发授权的项目 Release 获取文件与凭据。
-- API Key、OAuth Token、设备码不应进入日志、报告、截图或公开配置。
-- 模型调用、OpenAI 授权和上游模型刷新需要网络,并受所选服务商与账号权限约束。
-- 本项目不是 OpenAI 产品;公开分发前应核对适用的许可、商标与再分发条款。
+Windows 的完整构建说明见 [windows/README.md](windows/README.md)。第三方组件和再分发边界见 [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md) 与 [LICENSES](LICENSES)。
## 开源许可证
-Uni-codex 原创安装器代码采用 [MIT License](LICENSE)。MIT 仅适用于本项目拥有
-版权的原创部分,不会重新授权 Codex、Codex++、插件、脚本、图标或商标。
+Uni-codex 原创安装器代码采用 [MIT License](LICENSE)。该许可证只适用于本项目拥有版权的原创部分,不会重新授权 Codex、Codex++、插件、脚本、图标或商标;第三方组件继续遵循各自的上游许可证。
+
+---
+
+
-- OpenAI Codex CLI:Apache-2.0。
-- Codex++ 及其兼容补丁:AGPL-3.0-only。
-- 其他组件:保持各自上游许可证;未声明许可证的内容不由本仓库镜像分发。
+如果 Uni-codex 帮你省下了配置时间,欢迎点一个 ⭐,也欢迎通过 [Issue](https://github.com/fancr-code/Uni-codex/issues) 提交建议。
-完整边界见 [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md) 和
-[`LICENSES/`](LICENSES/)。
+
diff --git a/Resources/installer-core.sh b/Resources/installer-core.sh
index fe3c44a..109e25c 100755
--- a/Resources/installer-core.sh
+++ b/Resources/installer-core.sh
@@ -1836,6 +1836,15 @@ install_script_market() {
fi
}
+install_skill_collections() {
+ [[ -x "$SCRIPT_DIR/install-skill-collections.sh" \
+ && -f "$SCRIPT_DIR/skill-collections.json" \
+ && -d "$SCRIPT_DIR/skill-collections" ]] || return 66
+ UNICODEX_SKILL_MANIFEST="$SCRIPT_DIR/skill-collections.json" \
+ UNICODEX_SKILL_BUNDLE="$SCRIPT_DIR/skill-collections" \
+ "$SCRIPT_DIR/install-skill-collections.sh"
+}
+
script_market_version() {
local filename="$1"
local config="$HOME/.config/Codex++/user_scripts.json"
@@ -2389,6 +2398,8 @@ install_command() {
emit_event installing_scripts 0.78 'installing the Codex++ script market' null
install_script_market || return $?
+ emit_event installing_skills 0.81 'installing 211 research skills' null
+ install_skill_collections || return $?
if [[ "$TEST_MODE" == "1" && "${FAIL_AFTER_SCRIPTS:-0}" == "1" ]]; then
emit_event install_failed null 'injected failure after script deployment' '"injected_script_failure"'
return 75
diff --git a/Resources/licenses/Third-Party-Notices.md b/Resources/licenses/Third-Party-Notices.md
index c907d44..971d306 100644
--- a/Resources/licenses/Third-Party-Notices.md
+++ b/Resources/licenses/Third-Party-Notices.md
@@ -12,7 +12,7 @@
来源:https://github.com/BigPizzaV3/CodexPlusPlus
-上游版本:v1.2.43。安装包使用 `codexkit.1` / `cross-provider-content-v1` 下游兼容修订,修复 DeepSeek/Kimi 与 OpenAI 模型之间切换时旧历史消息 `content` 类型不兼容的问题。
+上游版本:v1.2.44。安装包使用 `codexkit.1` / `cross-provider-content-v1` 下游兼容修订,修复 DeepSeek/Kimi 与 OpenAI 模型之间切换时旧历史消息 `content` 类型不兼容的问题。
许可证:AGPL-3.0-only。DMG 的“第三方许可与源码”目录同时包含与二进制版本一致的完整修订源码归档、`CODEXKIT-PATCH.md`、可单独审计的补丁文件和许可证文本。
diff --git a/Resources/script-market-overrides.json b/Resources/script-market-overrides.json
index c2fe49b..93fc700 100644
--- a/Resources/script-market-overrides.json
+++ b/Resources/script-market-overrides.json
@@ -9,6 +9,28 @@
"pinnedURL": "https://raw.githubusercontent.com/hL091015/CodexPlusPlusScriptMarket/482076e76af9c78f18e3998bd99a96dc6033eb5d/scripts/zh_CN%E6%B1%89%E5%8C%96.user.js",
"pinnedSHA256": "be19a7930116dfe8fa1c68571d6a3bb3130714f77c7e32a6c1da543a182270f5",
"sourceCommit": "482076e76af9c78f18e3998bd99a96dc6033eb5d"
+ },
+ {
+ "mode": "managed",
+ "id": "codex-context-used-meter",
+ "upstreamURL": "https://raw.githubusercontent.com/BigPizzaV3/CodexPlusPlusScriptMarket/main/scripts/codex-context-used-meter.js",
+ "upstreamSHA256": "d2a5efe42d88d9b706b8ca8799947d2bc31c1b608b30c2673ac36fa0050911ab",
+ "managedSource": "script-market-sources/codex-context-used-meter.js",
+ "managedURL": "https://raw.githubusercontent.com/fancr-code/Uni-codex/8f47a2b20bc7fd16b771ca51d5e409219f0fd7df/Resources/script-market-sources/codex-context-used-meter.js",
+ "managedSHA256": "7d1f79dd2f379bf25787ed1fc65778266fd286cd33966692708f985fe3adba7d",
+ "sourceCommit": "8f47a2b20bc7fd16b771ca51d5e409219f0fd7df",
+ "provenance": "codex-context-meter-v101"
+ },
+ {
+ "mode": "managed",
+ "id": "codex-token-usage",
+ "upstreamURL": "https://raw.githubusercontent.com/BigPizzaV3/CodexPlusPlusScriptMarket/main/scripts/codex-token-usage.js",
+ "upstreamSHA256": "03808d22f53da374837227636ebd9f1ba593fe5aba6219e90e636d7ed7c806eb",
+ "managedSource": "script-market-sources/codex-token-usage.js",
+ "managedURL": "https://raw.githubusercontent.com/fancr-code/Uni-codex/8f47a2b20bc7fd16b771ca51d5e409219f0fd7df/Resources/script-market-sources/codex-token-usage.js",
+ "managedSHA256": "bf233607f8e60f56b3c68d29c15bbd5ed5d7582fc488380f56b8d2f553bb4ddd",
+ "sourceCommit": "8f47a2b20bc7fd16b771ca51d5e409219f0fd7df",
+ "provenance": "codex-token-usage-dedupe-v1"
}
]
}
diff --git a/Resources/script-market-sources/codex-context-used-meter.js b/Resources/script-market-sources/codex-context-used-meter.js
new file mode 100644
index 0000000..64922e1
--- /dev/null
+++ b/Resources/script-market-sources/codex-context-used-meter.js
@@ -0,0 +1,3801 @@
+(() => {
+ // Codex++ 会把本文件注入 Codex 渲染页;所有读取都只能依赖页面里已经暴露的运行态信号。
+ const INSTALL_KEY = "__codexContextMeterInstalled";
+ const API_KEY = "__codexContextMeter";
+ const STYLE_ID = "codex-context-meter-style";
+ const ROOT_ID = "codex-context-meter";
+ const HISTORY_PORTAL_ID = "codex-context-meter-history-portal";
+ const CONFIG_KEY = "__codexContextMeterConfig";
+ const UI_STATE_STORAGE_KEY = "__codexContextMeterUiState";
+ const PROVIDER_SUMMARY_KEY = "__codexContextMeterProviderSummary";
+ const PROVIDER_SUMMARY_EVENT = "codex-context-meter-provider-summary";
+ const SCRIPT_VERSION = 101;
+ const UPDATE_INTERVAL_MS = 5000;
+ const SLOW_SCAN_INTERVAL_MS = UPDATE_INTERVAL_MS;
+ const CONTEXT_USAGE_BACKGROUND_SAMPLE_INTERVAL_MS = UPDATE_INTERVAL_MS;
+ const CONTEXT_USAGE_BACKGROUND_SAMPLE_MAX_CONVERSATIONS = 32;
+ const SWITCH_RETRY_WINDOW_MS = 8000;
+ const SWITCH_RETRY_INTERVAL_MS = 700;
+ const NAVIGATION_PENDING_MS = 1500;
+ const NAVIGATION_UPDATE_DELAY_MS = 30;
+ const MUTATION_UPDATE_DELAY_MS = 500;
+ const ACTIVE_CONVERSATION_LOOKUP_CACHE_MS = 250;
+ const APP_SIGNAL_READING_CACHE_MS = 120;
+ const APP_SIGNAL_IMPORT_GRACE_MS = 600;
+ const INLINE_MOUNT_CACHE_MS = 5000;
+ // 以下限额只约束兜底扫描;主路径读 app signal / 已缓存读数,不受这些值影响。
+ const EXPENSIVE_FALLBACK_INTERVAL_MS = 2500;
+ const REACT_HOST_SCAN_LIMIT = 180;
+ const WINDOW_KEY_CACHE_MS = 10000;
+ const SPEND_HISTORY_WINDOW_MS = 60 * 60 * 1000;
+ const SPEND_HISTORY_MAX_ITEMS = 200;
+ const SPEND_HISTORY_CHART_WIDTH = 244;
+ const SPEND_HISTORY_CHART_HEIGHT = 72;
+ const SPEND_HISTORY_CHART_PADDING = 8;
+ const SPEND_HISTORY_CHART_AXIS_WIDTH = 52;
+ const SPEND_EFFECT_DURATION_MS = 3000;
+ const SPEND_EFFECT_FALLBACK_MS = 3200;
+ const CONTEXT_SPEND_DEDUPE_WINDOW_MS = UPDATE_INTERVAL_MS * 2 + MUTATION_UPDATE_DELAY_MS;
+ const CONTEXT_SPEND_DEDUPE_KEY = "__codexContextMeterContextSpendDedupe";
+ const PROVIDER_SPEND_DEDUPE_WINDOW_MS = 1500;
+ const PROVIDER_SPEND_DEDUPE_KEY = "__codexContextMeterProviderSpendDedupe";
+ const HISTORY_PANEL_VIEWPORT_PADDING = 8;
+ const HISTORY_PANEL_GAP = 8;
+ const HISTORY_PANEL_MIN_WIDTH = 240;
+ const HISTORY_PANEL_MIN_HEIGHT = 80;
+ const HISTORY_PORTAL_THEME_VARIABLES = [
+ "--ccm-card-value",
+ "--ccm-panel-border",
+ "--ccm-panel-bg",
+ "--ccm-panel-text",
+ "--ccm-panel-shadow",
+ "--ccm-muted-strong",
+ "--ccm-muted",
+ "--ccm-muted-soft",
+ "--ccm-axis-line",
+ "--ccm-gridline",
+ ];
+ const FLOAT_DRAG_HOLD_MS = 260;
+ const FLOAT_SCALE_MIN = 0.7;
+ const FLOAT_SCALE_MAX = 1.8;
+ const FLOAT_SCALE_STEP = 0.08;
+ const DEFAULT_FLOATING_UI = {
+ mode: "inline",
+ floatingLayout: "horizontal",
+ theme: "dark",
+ x: 16,
+ y: 10,
+ scale: 1,
+ };
+ const DEFAULT_UI_CONFIG = {
+ context: {
+ showUsedInsteadOfLeft: false,
+ compressionWarningLeftPercent: 20,
+ levelThresholds: {
+ criticalLeftPercent: 30,
+ dangerLeftPercent: 40,
+ warnLeftPercent: 50,
+ noticeLeftPercent: 60,
+ },
+ },
+ provider: {
+ levelThresholds: {
+ criticalLeftPercent: 30,
+ dangerLeftPercent: 40,
+ warnLeftPercent: 50,
+ noticeLeftPercent: 60,
+ },
+ },
+ };
+ const CODEX_COMPOSER_SELECTOR = `[data-codex-composer="true"]`;
+ const THREAD_COMPOSER_SELECTOR = `[data-thread-find-composer="true"]`;
+ const CODEX_INTELLIGENCE_TRIGGER_SELECTOR = `[data-codex-intelligence-trigger="true"]`;
+ const REACT_CONVERSATION_SCAN_DEPTH = 14;
+ const APP_SIGNAL_SELECTOR_SCAN_INTERVAL_MS = 2000;
+ const APP_SIGNAL_SELECTOR_SCAN_LIMIT = 360;
+ const THREAD_CONTENT_SELECTOR = [
+ `[data-thread-find-target="conversation"]`,
+ THREAD_COMPOSER_SELECTOR,
+ '[data-app-shell-main-content-layout*="thread"]',
+ ].join(",");
+ const REACT_STATE_HOST_SELECTOR = [
+ "[data-app-action-sidebar-thread-id]",
+ THREAD_COMPOSER_SELECTOR,
+ CODEX_COMPOSER_SELECTOR,
+ `[data-thread-find-target="conversation"]`,
+ "[data-message-author-role]",
+ "main",
+ "article",
+ ].join(",");
+ const CONVERSATION_CONTENT_SELECTOR = [
+ `[data-thread-find-target]`,
+ `[data-message-author-role]`,
+ `article`,
+ ].join(",");
+ const INVALID_INLINE_MOUNT_SELECTOR = [
+ "button",
+ "[role='button']",
+ "[aria-haspopup]",
+ "[data-codex-intelligence-trigger]",
+ ].join(",");
+ const MESSAGE_MUTATION_SELECTOR = [
+ `[data-thread-find-target]`,
+ `[data-message-author-role]`,
+ `article`,
+ ].join(",");
+ const PREFERRED_STATUS_KEYS = [
+ "contextUsage",
+ "context_usage",
+ "tokenUsage",
+ "token_usage",
+ "usage",
+ "data",
+ "props",
+ "memoizedState",
+ "memoizedProps",
+ "pendingProps",
+ "updateQueue",
+ "dependencies",
+ "alternate",
+ "return",
+ "child",
+ "sibling",
+ "stateNode",
+ "current",
+ "value",
+ "store",
+ "atom",
+ "atoms",
+ "map",
+ "cache",
+ ];
+ const PREFERRED_STATUS_KEY_SET = new Set(PREFERRED_STATUS_KEYS);
+ const STATUS_TREE_KEY_RE = /context|usage|status|thread|conversation|token|query|data|props|memoized|pending|return|child|sibling|state|value|current|store|atom|map|cache/i;
+ const APP_SIGNAL_SCOPE_KEY_RE = /memoized|pending|dependencies|firstContext|context|value|current|return|child|sibling|state|store|node|chain|scope|provider|props|query|cache/i;
+ const CONVERSATION_REACT_KEY_RE = /^(?:props|children|memoizedProps|pendingProps|memoizedState|stateNode|child|sibling|return|alternate|value|current|context|node|chain|conversationId|localConversationId|threadId|id|key|params|thread|conversation)$/;
+ const REACT_PRIVATE_KEY_RE = /^__react(?:Props|Fiber|Container)\$/;
+ const CONVERSATION_ID_KEYS = [
+ "conversationId",
+ "localConversationId",
+ "threadId",
+ "id",
+ "key",
+ ];
+ const CONVERSATION_ID_KEY_SET = new Set(CONVERSATION_ID_KEYS);
+
+ for (const key of Object.keys(window)) {
+ if (!/CodexContextUsageMeter(?:Installed)?$/.test(key)) continue;
+
+ const legacyApi = window[key];
+ if (legacyApi && typeof legacyApi.destroy === "function") {
+ legacyApi.destroy();
+ }
+ delete window[key];
+ }
+
+ const legacyRootId = ["codex", "context", "usage", "meter"].join("-");
+ document.getElementById(legacyRootId)?.remove();
+ document.getElementById(`${legacyRootId}-style`)?.remove();
+
+ if (window[INSTALL_KEY]) {
+ const api = window[API_KEY];
+ if (api && api.version !== SCRIPT_VERSION && typeof api.destroy === "function") {
+ api.destroy();
+ } else {
+ if (api && typeof api.refresh === "function") {
+ api.refresh();
+ }
+ return;
+ }
+ }
+
+ window[INSTALL_KEY] = true;
+
+ const state = {
+ activeConversationId: null,
+ lastReading: null,
+ readingsByConversationId: new Map(),
+ lastAnimatedUsedByConversationId: new Map(),
+ lastAnimatedProviderUsedById: new Map(),
+ root: null,
+ contextCard: null,
+ providerCard: null,
+ historyPanel: null,
+ historyPortal: null,
+ value: null,
+ fill: null,
+ compressionZone: null,
+ contextRing: null,
+ providerValue: null,
+ providerFill: null,
+ providerRing: null,
+ providerSummary: null,
+ inlineHost: null,
+ inlineBefore: null,
+ inlineMountCache: null,
+ inlineMountPending: false,
+ inlineMountLookupAt: 0,
+ uiState: DEFAULT_FLOATING_UI,
+ contextMenu: null,
+ contextMenuCloseListener: null,
+ floatingPointerCleanup: null,
+ floatingDrag: null,
+ spendHistory: {
+ context: [],
+ provider: [],
+ },
+ spendEffectQueue: [],
+ spendEffectActive: null,
+ spendEffectTimer: 0,
+ contextSessionTotalsByConversationId: new Map(),
+ providerSessionTotalsByConversationId: new Map(),
+ historyCloseTimer: 0,
+ historyHoverCleanup: null,
+ uiConfig: DEFAULT_UI_CONFIG,
+ providerSummaryListener: null,
+ lastScanAt: 0,
+ lastScannedConversationId: null,
+ contextUsageBackgroundSampleAt: 0,
+ contextUsageBackgroundSampleConversationIds: [],
+ navigationPendingUntil: 0,
+ switchRetryUntil: 0,
+ retryTimer: 0,
+ timer: 0,
+ observer: null,
+ navigationListener: null,
+ pendingUpdate: 0,
+ pendingUpdateDueAt: 0,
+ cachedActiveConversationId: null,
+ activeConversationIdLookupAt: 0,
+ appSignalScope: null,
+ appSignalModules: null,
+ appSignalModulesPromise: null,
+ appSignalLastLookupAt: 0,
+ appSignalCachedReading: null,
+ appSignalCachedConversationId: null,
+ appSignalCachedAt: 0,
+ appSignalLastSuccessAt: 0,
+ appSignalModulesRequestedAt: 0,
+ appSignalTokenUsageSelector: null,
+ appSignalTokenUsageSelectorExport: null,
+ appSignalTokenUsageSelectorLookupAt: 0,
+ waitingForAppSignalModules: false,
+ expensiveFallbackScannedAt: 0,
+ expensiveFallbackConversationId: null,
+ windowUsageKeys: null,
+ windowUsageKeysAt: 0,
+ reactPrivateKeyCache: new WeakMap(),
+ scanGeneration: 0,
+ filteredReflectKeyCache: new WeakMap(),
+ appSignalSkipGeneration: new WeakMap(),
+ threadContentLookupAt: 0,
+ threadContentLookupResult: false,
+ };
+
+ function installStyle() {
+ const existingStyle = document.getElementById(STYLE_ID);
+ if (existingStyle && existingStyle.dataset.version === String(SCRIPT_VERSION)) return;
+ existingStyle?.remove();
+
+ const style = document.createElement("style");
+ style.id = STYLE_ID;
+ style.dataset.version = String(SCRIPT_VERSION);
+ style.textContent = `
+ #${ROOT_ID} {
+ --ccm-card-border: rgba(255, 255, 255, 0.16);
+ --ccm-card-bg: rgba(20, 22, 28, 0.78);
+ --ccm-card-bg-strong: rgba(20, 22, 28, 0.88);
+ --ccm-card-text: rgba(255, 255, 255, 0.92);
+ --ccm-card-value: rgba(255, 255, 255, 0.98);
+ --ccm-card-shadow: 0 5px 18px rgba(0, 0, 0, 0.18);
+ --ccm-card-shadow-strong: 0 8px 28px rgba(0, 0, 0, 0.24);
+ --ccm-ring-rest: rgba(255, 255, 255, 0.18);
+ --ccm-ring-core: rgba(20, 22, 28, 0.96);
+ --ccm-track-bg: rgba(255, 255, 255, 0.16);
+ --ccm-panel-border: rgba(255, 255, 255, 0.14);
+ --ccm-panel-bg: rgba(16, 18, 24, 0.94);
+ --ccm-panel-text: rgba(255, 255, 255, 0.9);
+ --ccm-panel-shadow: 0 10px 30px rgba(0, 0, 0, 0.28);
+ --ccm-muted-strong: rgba(255, 255, 255, 0.72);
+ --ccm-muted: rgba(255, 255, 255, 0.48);
+ --ccm-muted-soft: rgba(255, 255, 255, 0.46);
+ --ccm-axis-line: rgba(255, 255, 255, 0.16);
+ --ccm-gridline: rgba(255, 255, 255, 0.1);
+ --ccm-fill-normal: #22c55e;
+ --ccm-fill-normal-start: #2563eb;
+ --ccm-fill-normal-end: #22c55e;
+ --ccm-fill-notice: #0ea5e9;
+ --ccm-fill-notice-start: #06b6d4;
+ --ccm-fill-notice-end: #0ea5e9;
+ --ccm-fill-warn: #ea580c;
+ --ccm-fill-warn-start: #d97706;
+ --ccm-fill-warn-end: #ea580c;
+ --ccm-fill-danger: #dc2626;
+ --ccm-fill-danger-start: #e11d48;
+ --ccm-fill-danger-end: #dc2626;
+ --ccm-fill-critical: #b91c1c;
+ --ccm-fill-critical-start: #be123c;
+ --ccm-fill-critical-end: #b91c1c;
+ --ccm-ring-size: 22px;
+ --ccm-ring-width: 3px;
+ --ccm-inline-max-width: 210px;
+ position: fixed;
+ top: var(--ccm-float-y, 10px);
+ left: var(--ccm-float-x, 16px);
+ transform: scale(var(--ccm-float-scale, 1));
+ transform-origin: top left;
+ z-index: 2147483647;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ min-height: var(--ccm-ring-size);
+ max-width: calc(100vw - 32px);
+ overflow: visible;
+ pointer-events: auto;
+ user-select: none;
+ /* Codex/Electron 顶部可能是窗口拖拽区;不退出拖拽区时,真实鼠标 hover 会被系统层吞掉。 */
+ -webkit-app-region: no-drag;
+ }
+
+ #${ROOT_ID}[data-theme="light"] {
+ --ccm-card-border: rgba(15, 23, 42, 0.14);
+ --ccm-card-bg: rgba(248, 250, 252, 0.9);
+ --ccm-card-bg-strong: rgba(248, 250, 252, 0.96);
+ --ccm-card-text: rgba(15, 23, 42, 0.86);
+ --ccm-card-value: rgba(15, 23, 42, 0.96);
+ --ccm-card-shadow: 0 5px 18px rgba(15, 23, 42, 0.14);
+ --ccm-card-shadow-strong: 0 8px 28px rgba(15, 23, 42, 0.18);
+ --ccm-ring-rest: rgba(15, 23, 42, 0.24);
+ --ccm-ring-core: rgba(248, 250, 252, 0.96);
+ --ccm-track-bg: rgba(15, 23, 42, 0.2);
+ --ccm-panel-border: rgba(15, 23, 42, 0.12);
+ --ccm-panel-bg: rgba(255, 255, 255, 0.96);
+ --ccm-panel-text: rgba(15, 23, 42, 0.88);
+ --ccm-panel-shadow: 0 10px 30px rgba(15, 23, 42, 0.16);
+ --ccm-muted-strong: rgba(51, 65, 85, 0.72);
+ --ccm-muted: rgba(71, 85, 105, 0.58);
+ --ccm-muted-soft: rgba(71, 85, 105, 0.54);
+ --ccm-axis-line: rgba(15, 23, 42, 0.16);
+ --ccm-gridline: rgba(15, 23, 42, 0.1);
+ }
+
+ #${ROOT_ID}[data-placement="inline"] {
+ position: relative;
+ inset: auto;
+ z-index: auto;
+ transform: none;
+ flex: 0 0 auto;
+ align-self: center;
+ max-width: min(42vw, 360px);
+ margin-right: 8px;
+ justify-content: flex-start;
+ }
+
+ #${ROOT_ID}[data-placement="inline"] .ccm-card {
+ width: var(--ccm-ring-size);
+ max-width: var(--ccm-ring-size);
+ padding: 0;
+ border: 0;
+ background: transparent;
+ box-shadow: none;
+ backdrop-filter: none;
+ }
+
+ #${ROOT_ID}[data-placement="inline"] .ccm-row {
+ gap: 0;
+ }
+
+ #${ROOT_ID}[data-placement="inline"] .ccm-value,
+ #${ROOT_ID}[data-placement="inline"] .ccm-provider-value {
+ display: none !important;
+ }
+
+ #${ROOT_ID}[data-placement="floating"] {
+ cursor: default;
+ }
+
+ #${ROOT_ID}[data-placement="floating"][data-floating-layout="vertical"] {
+ flex-direction: column;
+ align-items: stretch;
+ }
+
+ #${ROOT_ID}[data-dragging="true"] {
+ cursor: grabbing;
+ }
+
+ #${ROOT_ID}[hidden] {
+ display: none !important;
+ }
+
+ #${HISTORY_PORTAL_ID} {
+ position: fixed;
+ inset: 0;
+ z-index: 2147483647;
+ pointer-events: none;
+ }
+
+ #${HISTORY_PORTAL_ID}[hidden] {
+ display: none !important;
+ }
+
+ #${ROOT_ID} .ccm-card {
+ position: relative;
+ box-sizing: border-box;
+ flex: 0 1 auto;
+ width: auto;
+ min-width: 0;
+ max-width: var(--ccm-inline-max-width);
+ padding: 5px 8px;
+ border: 1px solid var(--ccm-card-border);
+ border-radius: 999px;
+ background: var(--ccm-card-bg);
+ color: var(--ccm-card-text);
+ box-shadow: var(--ccm-card-shadow);
+ font: 12px/1.35 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
+ overflow: visible;
+ backdrop-filter: blur(10px);
+ pointer-events: auto;
+ -webkit-app-region: no-drag;
+ }
+
+ #${ROOT_ID}[data-placement="floating"] .ccm-card {
+ max-width: 240px;
+ padding: 8px 10px 9px;
+ border-radius: 8px;
+ background: var(--ccm-card-bg-strong);
+ box-shadow: var(--ccm-card-shadow-strong);
+ }
+
+ #${ROOT_ID}[data-placement="floating"] .ccm-row {
+ justify-content: center;
+ margin-bottom: 6px;
+ }
+
+ #${ROOT_ID}[data-placement="floating"] .ccm-ring {
+ display: none;
+ }
+
+ #${ROOT_ID}[data-placement="floating"] .ccm-track {
+ display: block;
+ }
+
+ #${ROOT_ID} .ccm-card[hidden] {
+ display: none !important;
+ }
+
+ #${ROOT_ID} .ccm-row {
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+ gap: 7px;
+ min-width: 0;
+ margin-bottom: 0;
+ white-space: nowrap;
+ }
+
+ #${ROOT_ID} .ccm-ring {
+ position: relative;
+ flex: 0 0 var(--ccm-ring-size);
+ width: var(--ccm-ring-size);
+ height: var(--ccm-ring-size);
+ border-radius: 50%;
+ background:
+ conic-gradient(var(--ccm-fill-color, var(--ccm-fill-normal)) 0deg, var(--ccm-fill-color, var(--ccm-fill-normal)) var(--ccm-ring-angle, 0deg), var(--ccm-ring-rest) var(--ccm-ring-angle, 0deg) 360deg);
+ filter: drop-shadow(0 1px 1px rgba(15, 23, 42, 0.16));
+ }
+
+ #${ROOT_ID} .ccm-ring::after {
+ content: "";
+ position: absolute;
+ inset: var(--ccm-ring-width);
+ border-radius: 50%;
+ background: var(--ccm-ring-core);
+ }
+
+ #${ROOT_ID} .ccm-value {
+ color: var(--ccm-card-value);
+ font-weight: 650;
+ font-variant-numeric: tabular-nums;
+ overflow: hidden;
+ text-align: left;
+ text-overflow: ellipsis;
+ }
+
+ #${ROOT_ID} .ccm-track {
+ display: none;
+ position: relative;
+ width: 100%;
+ height: 7px;
+ overflow: hidden;
+ border-radius: 999px;
+ background: var(--ccm-track-bg);
+ }
+
+ #${ROOT_ID} .ccm-fill {
+ --ccm-fill-color: var(--ccm-fill-normal);
+ --ccm-fill-gradient: linear-gradient(90deg, var(--ccm-fill-normal-start), var(--ccm-fill-normal-end));
+ width: 0%;
+ height: 100%;
+ border-radius: inherit;
+ background: var(--ccm-fill-gradient);
+ transition: width 180ms ease, background 180ms ease;
+ }
+
+ #${ROOT_ID} .ccm-compression-zone {
+ position: absolute;
+ inset: 0 auto 0 0;
+ z-index: 1;
+ width: 0%;
+ border-radius: inherit;
+ background:
+ linear-gradient(90deg, rgba(255, 196, 0, 0.2), rgba(255, 126, 34, 0.12)),
+ repeating-linear-gradient(
+ -45deg,
+ rgba(253, 224, 71, 0.72) 0,
+ rgba(253, 224, 71, 0.72) 4px,
+ rgba(251, 146, 60, 0.64) 4px,
+ rgba(251, 146, 60, 0.64) 8px
+ );
+ box-shadow:
+ inset 0 0 0 1px rgba(251, 191, 36, 0.38),
+ inset 0 0 7px rgba(251, 146, 60, 0.18);
+ opacity: 0.82;
+ pointer-events: none;
+ transition: width 180ms ease, opacity 180ms ease;
+ }
+
+ #${ROOT_ID} .ccm-provider-value {
+ color: var(--ccm-card-value);
+ font-weight: 650;
+ font-variant-numeric: tabular-nums;
+ overflow: hidden;
+ text-align: left;
+ text-overflow: ellipsis;
+ }
+
+ #${ROOT_ID} .ccm-context-card[data-level="warn"] .ccm-fill,
+ #${ROOT_ID} .ccm-context-card[data-level="warn"] .ccm-ring,
+ #${ROOT_ID} .ccm-provider-card[data-level="warn"] .ccm-ring,
+ #${ROOT_ID} .ccm-provider-card[data-level="warn"] .ccm-fill {
+ --ccm-fill-color: var(--ccm-fill-warn);
+ --ccm-fill-gradient: linear-gradient(90deg, var(--ccm-fill-warn-start), var(--ccm-fill-warn-end));
+ }
+
+ #${ROOT_ID} .ccm-context-card[data-level="danger"] .ccm-fill,
+ #${ROOT_ID} .ccm-context-card[data-level="danger"] .ccm-ring,
+ #${ROOT_ID} .ccm-provider-card[data-level="danger"] .ccm-ring,
+ #${ROOT_ID} .ccm-provider-card[data-level="danger"] .ccm-fill {
+ --ccm-fill-color: var(--ccm-fill-danger);
+ --ccm-fill-gradient: linear-gradient(90deg, var(--ccm-fill-danger-start), var(--ccm-fill-danger-end));
+ }
+
+ #${ROOT_ID} .ccm-context-card[data-level="notice"] .ccm-fill,
+ #${ROOT_ID} .ccm-context-card[data-level="notice"] .ccm-ring,
+ #${ROOT_ID} .ccm-provider-card[data-level="notice"] .ccm-ring,
+ #${ROOT_ID} .ccm-provider-card[data-level="notice"] .ccm-fill {
+ --ccm-fill-color: var(--ccm-fill-notice);
+ --ccm-fill-gradient: linear-gradient(90deg, var(--ccm-fill-notice-start), var(--ccm-fill-notice-end));
+ }
+
+ #${ROOT_ID} .ccm-context-card[data-level="critical"] .ccm-fill,
+ #${ROOT_ID} .ccm-context-card[data-level="critical"] .ccm-ring,
+ #${ROOT_ID} .ccm-provider-card[data-level="critical"] .ccm-ring,
+ #${ROOT_ID} .ccm-provider-card[data-level="critical"] .ccm-fill {
+ --ccm-fill-color: var(--ccm-fill-critical);
+ --ccm-fill-gradient: linear-gradient(90deg, var(--ccm-fill-critical-start), var(--ccm-fill-critical-end));
+ }
+
+ #${ROOT_ID} .ccm-context-card[data-compression-warning="true"] .ccm-compression-zone {
+ opacity: 1;
+ }
+
+ #${ROOT_ID} .ccm-context-card[data-show-used-instead-of-left="true"] .ccm-compression-zone {
+ left: auto;
+ right: 0;
+ }
+
+ #${HISTORY_PORTAL_ID} .ccm-history-panel {
+ position: absolute;
+ top: calc(100% + 8px);
+ left: 0;
+ right: auto;
+ z-index: 1;
+ box-sizing: border-box;
+ width: max-content;
+ min-width: 240px;
+ max-width: min(560px, var(--ccm-history-max-width, calc(100vw - 32px)));
+ max-height: var(--ccm-history-max-height, calc(100vh - 16px));
+ overflow: hidden;
+ padding: 9px 10px 10px;
+ border: 1px solid var(--ccm-panel-border);
+ border-radius: 8px;
+ background: var(--ccm-panel-bg);
+ color: var(--ccm-panel-text);
+ box-shadow: var(--ccm-panel-shadow);
+ font: 12px/1.35 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
+ opacity: 0;
+ transform: translateY(-4px);
+ pointer-events: none;
+ visibility: hidden;
+ backdrop-filter: blur(12px);
+ -webkit-app-region: no-drag;
+ transition: opacity 140ms ease, transform 140ms ease, visibility 140ms ease;
+ }
+
+ #${HISTORY_PORTAL_ID} .ccm-history-panel {
+ position: fixed;
+ top: var(--ccm-history-top, 0px);
+ left: var(--ccm-history-left, 0px);
+ right: auto;
+ z-index: 2147483647;
+ }
+
+ #${HISTORY_PORTAL_ID} .ccm-history-panel::before {
+ content: "";
+ position: absolute;
+ left: 0;
+ right: 0;
+ top: -8px;
+ height: 8px;
+ pointer-events: auto;
+ }
+
+ #${HISTORY_PORTAL_ID}[data-history-open="true"] .ccm-history-panel {
+ opacity: 1;
+ transform: translateY(0);
+ pointer-events: auto;
+ visibility: visible;
+ }
+
+ #${HISTORY_PORTAL_ID} .ccm-history-grid {
+ display: grid;
+ grid-template-columns: minmax(0, 260px) minmax(0, 260px);
+ gap: 12px;
+ }
+
+ #${HISTORY_PORTAL_ID} .ccm-history-grid[data-provider-visible="false"] {
+ grid-template-columns: minmax(0, 292px);
+ }
+
+ #${HISTORY_PORTAL_ID} .ccm-history-section {
+ min-width: 0;
+ --ccm-history-accent: #38bdf8;
+ --ccm-history-fill: rgba(56, 189, 248, 0.12);
+ }
+
+ #${HISTORY_PORTAL_ID} .ccm-history-section[data-history-kind="provider"] {
+ --ccm-history-accent: #f97316;
+ --ccm-history-fill: rgba(249, 115, 22, 0.12);
+ }
+
+ #${HISTORY_PORTAL_ID} .ccm-history-section[hidden] {
+ display: none !important;
+ }
+
+ #${HISTORY_PORTAL_ID} .ccm-history-head {
+ display: flex;
+ align-items: baseline;
+ justify-content: space-between;
+ gap: 8px;
+ margin-bottom: 6px;
+ white-space: nowrap;
+ }
+
+ #${HISTORY_PORTAL_ID} .ccm-history-title {
+ color: var(--ccm-card-value);
+ font-weight: 700;
+ }
+
+ #${HISTORY_PORTAL_ID} .ccm-history-total {
+ color: var(--ccm-muted-strong);
+ font-variant-numeric: tabular-nums;
+ }
+
+ #${HISTORY_PORTAL_ID} .ccm-history-chart {
+ position: relative;
+ width: 100%;
+ min-height: 78px;
+ }
+
+ #${HISTORY_PORTAL_ID} .ccm-history-svg {
+ display: block;
+ width: 100%;
+ height: 78px;
+ overflow: visible;
+ }
+
+ #${HISTORY_PORTAL_ID} .ccm-history-axis-line {
+ fill: none;
+ stroke: var(--ccm-axis-line);
+ stroke-width: 1;
+ vector-effect: non-scaling-stroke;
+ }
+
+ #${HISTORY_PORTAL_ID} .ccm-history-axis-label {
+ fill: var(--ccm-muted-soft);
+ font-size: 10px;
+ font-variant-numeric: tabular-nums;
+ }
+
+ #${HISTORY_PORTAL_ID} .ccm-history-gridline {
+ fill: none;
+ stroke: var(--ccm-gridline);
+ stroke-dasharray: 2 4;
+ stroke-width: 1;
+ }
+
+ #${HISTORY_PORTAL_ID} .ccm-history-area {
+ fill: var(--ccm-history-fill);
+ }
+
+ #${HISTORY_PORTAL_ID} .ccm-history-line {
+ fill: none;
+ stroke: var(--ccm-history-accent);
+ stroke-linecap: round;
+ stroke-linejoin: round;
+ stroke-width: 2.2;
+ vector-effect: non-scaling-stroke;
+ }
+
+ #${HISTORY_PORTAL_ID} .ccm-history-point {
+ fill: #fff7ed;
+ stroke: var(--ccm-history-accent);
+ stroke-width: 1.5;
+ }
+
+ #${HISTORY_PORTAL_ID} .ccm-history-hit {
+ fill: transparent;
+ pointer-events: all;
+ }
+
+ #${HISTORY_PORTAL_ID} .ccm-history-caption {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ margin-top: 2px;
+ color: var(--ccm-muted);
+ font-size: 11px;
+ font-variant-numeric: tabular-nums;
+ white-space: nowrap;
+ }
+
+ #${HISTORY_PORTAL_ID} .ccm-history-empty {
+ position: absolute;
+ inset: 19px 0 auto 0;
+ color: var(--ccm-muted-soft);
+ }
+
+ .ccm-context-menu {
+ --ccm-menu-border: rgba(255, 255, 255, 0.14);
+ --ccm-menu-bg: rgba(18, 20, 26, 0.96);
+ --ccm-menu-text: rgba(255, 255, 255, 0.92);
+ --ccm-menu-shadow: 0 12px 32px rgba(0, 0, 0, 0.36);
+ --ccm-menu-hover: rgba(255, 255, 255, 0.1);
+ --ccm-menu-checked: rgba(255, 255, 255, 0.08);
+ --ccm-menu-separator: rgba(255, 255, 255, 0.12);
+ --ccm-menu-check: #86efac;
+ --ccm-menu-hint: rgba(255, 255, 255, 0.48);
+ position: fixed;
+ z-index: 2147483647;
+ min-width: 150px;
+ padding: 5px;
+ border: 1px solid var(--ccm-menu-border);
+ border-radius: 8px;
+ background: var(--ccm-menu-bg);
+ color: var(--ccm-menu-text);
+ box-shadow: var(--ccm-menu-shadow);
+ font: 12px/1.35 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
+ backdrop-filter: blur(12px);
+ -webkit-app-region: no-drag;
+ }
+
+ .ccm-context-menu[data-theme="light"] {
+ --ccm-menu-border: rgba(15, 23, 42, 0.12);
+ --ccm-menu-bg: rgba(255, 255, 255, 0.98);
+ --ccm-menu-text: rgba(15, 23, 42, 0.9);
+ --ccm-menu-shadow: 0 12px 32px rgba(15, 23, 42, 0.18);
+ --ccm-menu-hover: rgba(15, 23, 42, 0.08);
+ --ccm-menu-checked: rgba(15, 23, 42, 0.07);
+ --ccm-menu-separator: rgba(15, 23, 42, 0.12);
+ --ccm-menu-check: #16a34a;
+ --ccm-menu-hint: rgba(71, 85, 105, 0.58);
+ }
+
+ .ccm-context-menu button {
+ display: flex;
+ align-items: center;
+ gap: 7px;
+ box-sizing: border-box;
+ width: 100%;
+ padding: 6px 8px;
+ border: 0;
+ border-radius: 6px;
+ background: transparent;
+ color: inherit;
+ font: inherit;
+ text-align: left;
+ cursor: pointer;
+ }
+
+ .ccm-context-menu button:hover {
+ background: var(--ccm-menu-hover);
+ }
+
+ .ccm-context-menu button[aria-checked="true"] {
+ background: var(--ccm-menu-checked);
+ }
+
+ .ccm-context-menu .ccm-menu-separator {
+ height: 1px;
+ margin: 5px 4px;
+ background: var(--ccm-menu-separator);
+ }
+
+ .ccm-context-menu .ccm-menu-check {
+ flex: 0 0 14px;
+ width: 14px;
+ color: var(--ccm-menu-check);
+ font-weight: 800;
+ text-align: center;
+ }
+
+ .ccm-context-menu .ccm-menu-hint {
+ padding: 5px 8px 6px 21px;
+ color: var(--ccm-menu-hint);
+ font-size: 11px;
+ line-height: 1.3;
+ white-space: nowrap;
+ }
+
+ #${ROOT_ID} .ccm-hit-pop {
+ position: absolute;
+ left: 0;
+ right: auto;
+ top: 50%;
+ z-index: 1;
+ color: #fff7ed;
+ background: linear-gradient(92deg, #fff7ed 0%, #fecdd3 38%, #fb7185 68%, #f97316 100%);
+ -webkit-background-clip: text;
+ background-clip: text;
+ -webkit-text-fill-color: transparent;
+ font-size: 14px;
+ font-weight: 850;
+ line-height: 1;
+ opacity: 0;
+ filter: drop-shadow(0 1px 0 rgba(0, 0, 0, 0.78))
+ drop-shadow(0 3px 8px rgba(0, 0, 0, 0.58))
+ drop-shadow(0 0 14px rgba(251, 113, 133, 0.56))
+ drop-shadow(0 0 26px rgba(249, 115, 22, 0.24));
+ text-shadow: 0 0 1px rgba(255, 255, 255, 0.45);
+ transform: translate(-108%, -50%) scale(0.72);
+ transform-origin: center center;
+ animation: ccm-hit-pop ${SPEND_EFFECT_DURATION_MS}ms cubic-bezier(0.16, 0.84, 0.24, 1) forwards;
+ pointer-events: none;
+ white-space: nowrap;
+ will-change: opacity, transform, filter;
+ }
+
+ @keyframes ccm-hit-pop {
+ 0% {
+ opacity: 0;
+ transform: translate(-108%, -50%) scale(0.72);
+ }
+ 12% {
+ opacity: 1;
+ transform: translate(-114%, -51%) scale(1);
+ }
+ 72% {
+ opacity: 1;
+ transform: translate(-146%, -54%) scale(1.22);
+ }
+ 100% {
+ opacity: 0;
+ transform: translate(-160%, -55%) scale(1.34);
+ }
+ }
+
+ @media (max-width: 720px) {
+ #${ROOT_ID}[data-placement="inline"] {
+ max-width: 72px;
+ }
+
+ #${ROOT_ID}[data-placement="inline"] .ccm-value,
+ #${ROOT_ID}[data-placement="inline"] .ccm-provider-value {
+ display: none;
+ }
+ }
+ `;
+ document.head.appendChild(style);
+ }
+
+ function bindRootElements(root) {
+ if (!root) return;
+
+ state.root = root;
+ state.contextCard = root.querySelector(".ccm-context-card");
+ state.providerCard = root.querySelector(".ccm-provider-card");
+ state.historyPortal = document.getElementById(HISTORY_PORTAL_ID);
+ state.historyPanel = state.historyPortal?.querySelector(".ccm-history-panel") || null;
+ state.value = root.querySelector(".ccm-value");
+ state.fill = root.querySelector(".ccm-fill");
+ state.compressionZone = root.querySelector(".ccm-compression-zone");
+ state.contextRing = root.querySelector(".ccm-context-card .ccm-ring");
+ state.providerValue = root.querySelector(".ccm-provider-value");
+ state.providerFill = root.querySelector(".ccm-provider-fill");
+ state.providerRing = root.querySelector(".ccm-provider-card .ccm-ring");
+ }
+
+ function isRootBound(root) {
+ return !!(
+ root &&
+ state.root === root &&
+ state.contextCard &&
+ state.value &&
+ state.fill &&
+ state.compressionZone &&
+ state.contextRing &&
+ state.providerCard &&
+ state.providerValue &&
+ state.providerFill &&
+ state.providerRing &&
+ state.historyPortal &&
+ state.historyPanel
+ );
+ }
+
+ function historyPanelMarkup() {
+ return `
+
+
+
+
+ Context / Session
+ --
+
+
+
+
+
+ Provider / Session
+ --
+
+
+
+
+
+ `;
+ }
+
+ function syncHistoryPortalTheme(root) {
+ const portal = state.historyPortal || document.getElementById(HISTORY_PORTAL_ID);
+ if (!root || !portal) return;
+
+ portal.dataset.theme = root.dataset.theme === "light" ? "light" : "dark";
+ const computed = getComputedStyle(root);
+ for (const name of HISTORY_PORTAL_THEME_VARIABLES) {
+ const value = computed.getPropertyValue(name);
+ if (value) portal.style.setProperty(name, value.trim());
+ }
+ }
+
+ function ensureHistoryPortal(root) {
+ let portal = state.historyPortal && state.historyPortal.isConnected
+ ? state.historyPortal
+ : document.getElementById(HISTORY_PORTAL_ID);
+ if (!portal) {
+ portal = document.createElement("div");
+ portal.id = HISTORY_PORTAL_ID;
+ portal.dataset.historyOpen = "false";
+ document.body.appendChild(portal);
+ } else if (portal.parentNode !== document.body) {
+ document.body.appendChild(portal);
+ }
+
+ if (portal.dataset.infrastructureVersion !== String(SCRIPT_VERSION)) {
+ portal.innerHTML = historyPanelMarkup();
+ portal.dataset.infrastructureVersion = String(SCRIPT_VERSION);
+ }
+
+ state.historyPortal = portal;
+ state.historyPanel = portal.querySelector(".ccm-history-panel");
+ if (root && portal.dataset.historyOpen !== root.dataset.historyOpen) {
+ portal.dataset.historyOpen = root.dataset.historyOpen === "true" ? "true" : "false";
+ }
+ if (state.historyPanel) {
+ state.historyPanel.setAttribute(
+ "aria-hidden",
+ portal.dataset.historyOpen === "true" ? "false" : "true",
+ );
+ }
+ syncHistoryPortalTheme(root);
+ return portal;
+ }
+
+ function ensureRootInfrastructure(root) {
+ ensureHistoryPortal(root);
+ if (root.dataset.infrastructureVersion !== String(SCRIPT_VERSION)) {
+ root.querySelector(".ccm-hover-zone")?.remove();
+ root.querySelector(".ccm-history-panel")?.remove();
+ const contextTrack = root.querySelector(".ccm-context-card .ccm-track");
+ if (contextTrack && !contextTrack.querySelector(".ccm-compression-zone")) {
+ contextTrack.insertBefore(document.createElement("div"), contextTrack.firstChild);
+ contextTrack.firstElementChild.className = "ccm-compression-zone";
+ }
+ root.dataset.infrastructureVersion = String(SCRIPT_VERSION);
+ }
+ if (!isRootBound(root)) bindRootElements(root);
+ installHistoryHover(root);
+ installContextMenu(root);
+ installFloatingControls(root);
+ }
+
+ function isInlineMountCurrent(root) {
+ const uiState = state.uiState || readUiState();
+ if (uiState.mode === "floating") return root.parentNode === document.body && root.dataset.placement === "floating";
+ if (root.dataset.placement !== "inline") return false;
+ if (!state.inlineHost || !state.inlineHost.isConnected || root.parentNode !== state.inlineHost) return false;
+ if (state.inlineHost.closest(INVALID_INLINE_MOUNT_SELECTOR)) return false;
+ return state.inlineBefore ? state.inlineBefore.isConnected && root.nextSibling === state.inlineBefore : true;
+ }
+
+ function ensureRoot() {
+ let root = state.root && state.root.isConnected ? state.root : document.getElementById(ROOT_ID);
+ if (root) {
+ ensureRootInfrastructure(root);
+ state.uiState = readUiState();
+ if (!isInlineMountCurrent(root)) mountRoot(root);
+ return root;
+ }
+
+ root = document.createElement("div");
+ root.id = ROOT_ID;
+ root.innerHTML = `
+
+
+
+ Context Left --
+
+
+
+
+
+
+ Provider Left --
+
+
+
+ `;
+ ensureRootInfrastructure(root);
+ mountRoot(root);
+ return root;
+ }
+
+ function findInlineMount() {
+ const now = Date.now();
+ if (
+ state.inlineMountCache &&
+ now - state.inlineMountLookupAt < INLINE_MOUNT_CACHE_MS &&
+ state.inlineMountCache.parent &&
+ state.inlineMountCache.parent.isConnected &&
+ (!state.inlineMountCache.before || state.inlineMountCache.before.isConnected)
+ ) {
+ return state.inlineMountCache;
+ }
+ state.inlineMountLookupAt = now;
+
+ const visibleDirectChildren = (node) =>
+ Array.from(node.children || []).filter((child) => child.id !== ROOT_ID && isVisibleElement(child));
+ const firstVisibleChild = (node) => visibleDirectChildren(node)[0] || null;
+ const classText = (node) => (typeof node?.className === "string" ? node.className : "");
+ const hasClassToken = (node, token) => classText(node).split(/\s+/).includes(token);
+ const sortByLeft = (nodes) =>
+ nodes.slice().sort(
+ (left, right) => left.getBoundingClientRect().left - right.getBoundingClientRect().left
+ );
+ const hasVisibleInteractiveControl = (node) =>
+ Array.from(
+ node.querySelectorAll(`button, [role='button'], [aria-haspopup], ${CODEX_INTELLIGENCE_TRIGGER_SELECTOR}`)
+ ).some((child) => child.id !== ROOT_ID && isVisibleElement(child));
+ const directChildOf = (parent, node) => {
+ let current = node;
+ while (current && current.parentElement && current.parentElement !== parent) {
+ current = current.parentElement;
+ }
+ return current && current.parentElement === parent ? current : null;
+ };
+ const isComposerArea = (node) => {
+ const rect = node && node.getBoundingClientRect();
+ if (!rect || rect.top < window.innerHeight * 0.45) return false;
+ if (node.closest(CONVERSATION_CONTENT_SELECTOR)) return false;
+ if (node.closest("aside, nav, [data-app-action-sidebar-thread-id], [data-app-action-sidebar-thread-active]")) return false;
+ if (node.closest("article, [data-message-author-role]")) return false;
+ if (!node.querySelector(`textarea, input, [contenteditable='true'], [role='textbox'], ${CODEX_COMPOSER_SELECTOR}`)) return false;
+ return true;
+ };
+ const findComposerArea = (node) => {
+ let current = node;
+ while (current && current !== document.body) {
+ if (isComposerArea(current)) return current;
+ current = current.parentElement;
+ }
+ return null;
+ };
+ const findComposerFooterMount = () => {
+ const footers = Array.from(document.querySelectorAll(".composer-footer"))
+ .filter((footer) => isVisibleElement(footer) && footer.getBoundingClientRect().top > window.innerHeight * 0.45)
+ .sort((left, right) => right.getBoundingClientRect().top - left.getBoundingClientRect().top);
+
+ for (const footer of footers) {
+ const footerChildren = sortByLeft(visibleDirectChildren(footer));
+ const toolbarRoot = footerChildren
+ .filter((child) => hasClassToken(child, "justify-end") && hasVisibleInteractiveControl(child))
+ .sort((left, right) => right.getBoundingClientRect().right - left.getBoundingClientRect().right)[0];
+ if (!toolbarRoot) continue;
+
+ const toolbarChildren = sortByLeft(visibleDirectChildren(toolbarRoot));
+ const providerGroup = toolbarChildren.find(
+ (child) =>
+ hasVisibleInteractiveControl(child) &&
+ !hasClassToken(child, "shrink-0") &&
+ (hasClassToken(child, "flex-1") || hasClassToken(child, "min-w-0"))
+ );
+ if (!providerGroup) continue;
+ const before = firstVisibleChild(providerGroup);
+ if (!before) continue;
+ const footerRect = footer.getBoundingClientRect();
+ const beforeRect = before.getBoundingClientRect();
+ if (beforeRect.left < footerRect.left + footerRect.width * 0.5) continue;
+
+ return {
+ parent: providerGroup,
+ before,
+ };
+ }
+
+ return null;
+ };
+ const findStructuralMountForControl = (control) => {
+ const footer = control.closest(".composer-footer");
+ if (footer) {
+ const footerRect = footer.getBoundingClientRect();
+ const controlRect = control.getBoundingClientRect();
+ if (controlRect.left < footerRect.left + footerRect.width * 0.5) return null;
+ }
+
+ let current = control.parentElement;
+ while (current && current !== document.body) {
+ const rect = current.getBoundingClientRect();
+ if (rect.top < window.innerHeight * 0.45) break;
+ if (current.closest(CONVERSATION_CONTENT_SELECTOR)) break;
+ if (!current.closest(INVALID_INLINE_MOUNT_SELECTOR)) {
+ const before = directChildOf(current, control) || control;
+ if (before && before !== current && current.contains(before)) {
+ return {
+ parent: current,
+ before,
+ };
+ }
+ }
+ current = current.parentElement;
+ }
+ return null;
+ };
+ const rememberMount = (mount) => {
+ if (
+ mount &&
+ mount.parent &&
+ mount.parent.isConnected &&
+ !mount.parent.closest(INVALID_INLINE_MOUNT_SELECTOR)
+ ) {
+ state.inlineMountCache = mount;
+ return mount;
+ }
+
+ state.inlineMountCache = null;
+ return null;
+ };
+
+ const footerMount = findComposerFooterMount();
+ if (footerMount) return rememberMount(footerMount);
+
+ const codexModelTrigger = document.querySelector(CODEX_INTELLIGENCE_TRIGGER_SELECTOR);
+ if (codexModelTrigger && isVisibleElement(codexModelTrigger)) {
+ const bar = findComposerArea(codexModelTrigger);
+ if (bar && isVisibleElement(bar) && isComposerArea(bar)) {
+ const triggerMount = findStructuralMountForControl(codexModelTrigger);
+ if (triggerMount) return rememberMount(triggerMount);
+ }
+ }
+
+ state.inlineMountCache = null;
+ return null;
+ }
+
+ function mountRoot(root) {
+ state.uiState = readUiState();
+ if (state.uiState.mode === "floating") {
+ state.inlineMountCache = null;
+ state.inlineMountPending = false;
+ if (root.parentNode !== document.body) document.body.appendChild(root);
+ state.inlineHost = null;
+ root.dataset.placement = "floating";
+ applyFloatingUiState(root);
+ refreshOpenSpendHistory(root);
+ return;
+ }
+
+ const mount = findInlineMount();
+ if (!mount || !mount.parent || root.contains(mount.parent)) {
+ state.inlineHost = null;
+ state.inlineBefore = null;
+ state.inlineMountCache = null;
+ state.inlineMountPending = true;
+ if (root.parentNode !== document.body) document.body.appendChild(root);
+ root.dataset.placement = "inline";
+ root.hidden = true;
+ applyFloatingUiState(root);
+ closeSpendHistory();
+ scheduleUpdate(SWITCH_RETRY_INTERVAL_MS);
+ return;
+ }
+
+ const before = mount.before || null;
+ if (before !== root && (root.parentNode !== mount.parent || root.nextSibling !== before)) {
+ mount.parent.insertBefore(root, before);
+ }
+ state.inlineHost = mount.parent;
+ state.inlineBefore = before;
+ state.inlineMountCache = mount;
+ state.inlineMountPending = false;
+ root.dataset.placement = "inline";
+ applyFloatingUiState(root);
+ refreshOpenSpendHistory(root);
+ }
+
+ function applyFloatingUiState(root) {
+ const uiState = state.uiState || DEFAULT_FLOATING_UI;
+ root.style.setProperty("--ccm-float-x", `${Math.round(uiState.x)}px`);
+ root.style.setProperty("--ccm-float-y", `${Math.round(uiState.y)}px`);
+ root.style.setProperty("--ccm-float-scale", String(uiState.scale));
+ root.dataset.floatingLayout = uiState.floatingLayout === "vertical" ? "vertical" : "horizontal";
+ root.dataset.theme = uiState.theme === "light" ? "light" : "dark";
+ syncHistoryPortalTheme(root);
+ }
+
+ function setUiMode(mode) {
+ state.uiState = {
+ ...readUiState(),
+ mode: mode === "floating" ? "floating" : "inline",
+ };
+ writeUiState();
+ closeContextMenu();
+ state.inlineMountCache = null;
+ const root = state.root || document.getElementById(ROOT_ID);
+ if (root) mountRoot(root);
+ }
+
+ function setUiTheme(theme) {
+ state.uiState = {
+ ...readUiState(),
+ theme: theme === "light" ? "light" : "dark",
+ };
+ writeUiState();
+ closeContextMenu();
+ const root = state.root || document.getElementById(ROOT_ID);
+ if (root) applyFloatingUiState(root);
+ }
+
+ function setFloatingLayout(layout) {
+ state.uiState = {
+ ...readUiState(),
+ floatingLayout: layout === "vertical" ? "vertical" : "horizontal",
+ };
+ writeUiState();
+ closeContextMenu();
+ const root = state.root || document.getElementById(ROOT_ID);
+ if (root) applyFloatingUiState(root);
+ }
+
+ function closeContextMenu() {
+ if (state.contextMenu) {
+ state.contextMenu.remove();
+ state.contextMenu = null;
+ }
+ if (state.contextMenuCloseListener) {
+ document.removeEventListener("pointerdown", state.contextMenuCloseListener, true);
+ document.removeEventListener("keydown", state.contextMenuCloseListener, true);
+ state.contextMenuCloseListener = null;
+ }
+ }
+
+ function openContextMenu(event) {
+ const root = state.root || document.getElementById(ROOT_ID);
+ if (!root || !root.contains(event.target)) return;
+ event.preventDefault();
+ closeContextMenu();
+ state.uiState = readUiState();
+
+ const currentMode = (state.uiState && state.uiState.mode) === "floating" ? "floating" : "inline";
+ const currentFloatingLayout =
+ (state.uiState && state.uiState.floatingLayout) === "vertical" ? "vertical" : "horizontal";
+ const currentTheme = (state.uiState && state.uiState.theme) === "light" ? "light" : "dark";
+ const menu = document.createElement("div");
+ menu.className = "ccm-context-menu";
+ menu.dataset.theme = currentTheme;
+ menu.setAttribute("role", "menu");
+ const createRadioItem = (group, value, label, checked) => {
+ const item = document.createElement("button");
+ item.type = "button";
+ item.dataset[group] = value;
+ item.setAttribute("role", "menuitemradio");
+ item.setAttribute("aria-checked", checked ? "true" : "false");
+
+ const check = document.createElement("span");
+ check.className = "ccm-menu-check";
+ check.setAttribute("aria-hidden", "true");
+ check.textContent = checked ? "✓" : "";
+
+ const text = document.createElement("span");
+ text.textContent = label;
+
+ item.append(check, text);
+ return item;
+ };
+ const items = [
+ createRadioItem("theme", "dark", "Dark theme", currentTheme === "dark"),
+ createRadioItem("theme", "light", "Light theme", currentTheme === "light"),
+ document.createElement("div"),
+ createRadioItem("mode", "inline", "Inline mode", currentMode === "inline"),
+ createRadioItem("mode", "floating", "Floating mode", currentMode === "floating"),
+ ];
+ items[2].className = "ccm-menu-separator";
+ items[2].setAttribute("role", "separator");
+ if (currentMode === "floating") {
+ const separator = document.createElement("div");
+ separator.className = "ccm-menu-separator";
+ separator.setAttribute("role", "separator");
+ const hint = document.createElement("div");
+ hint.className = "ccm-menu-hint";
+ hint.textContent = "Use mouse wheel to resize";
+ items.push(
+ separator,
+ createRadioItem("floatingLayout", "horizontal", "Horizontal layout", currentFloatingLayout === "horizontal"),
+ createRadioItem("floatingLayout", "vertical", "Vertical layout", currentFloatingLayout === "vertical"),
+ hint,
+ );
+ }
+ menu.replaceChildren(...items);
+ menu.style.left = `${Math.max(6, event.clientX)}px`;
+ menu.style.top = `${Math.max(6, event.clientY)}px`;
+ menu.addEventListener("pointerdown", (menuEvent) => {
+ menuEvent.stopPropagation();
+ });
+ menu.addEventListener("click", (menuEvent) => {
+ const button = menuEvent.target && menuEvent.target.closest("button[data-mode]");
+ if (button) {
+ setUiMode(button.dataset.mode);
+ return;
+ }
+
+ const layoutButton = menuEvent.target && menuEvent.target.closest("button[data-floating-layout]");
+ if (layoutButton) {
+ setFloatingLayout(layoutButton.dataset.floatingLayout);
+ return;
+ }
+
+ const themeButton = menuEvent.target && menuEvent.target.closest("button[data-theme]");
+ if (themeButton) setUiTheme(themeButton.dataset.theme);
+ });
+ document.body.appendChild(menu);
+ const menuRect = menu.getBoundingClientRect();
+ const x = clampNumber(event.clientX, 6, Math.max(6, window.innerWidth - menuRect.width - 6));
+ const y = clampNumber(event.clientY, 6, Math.max(6, window.innerHeight - menuRect.height - 6));
+ menu.style.left = `${Math.round(x)}px`;
+ menu.style.top = `${Math.round(y)}px`;
+ state.contextMenu = menu;
+ state.contextMenuCloseListener = (closeEvent) => {
+ if (closeEvent.type === "keydown" && closeEvent.key !== "Escape") return;
+ if (state.contextMenu && closeEvent.target && state.contextMenu.contains(closeEvent.target)) return;
+ closeContextMenu();
+ };
+ window.setTimeout(() => {
+ document.addEventListener("pointerdown", state.contextMenuCloseListener, true);
+ document.addEventListener("keydown", state.contextMenuCloseListener, true);
+ }, 0);
+ }
+
+ function installContextMenu(root) {
+ if (!root || root.dataset.contextMenuInstalled === "true") return;
+ root.dataset.contextMenuInstalled = "true";
+ root.addEventListener("contextmenu", openContextMenu);
+ }
+
+ function clampFloatingPosition(root, x, y, scale) {
+ const rect = root.getBoundingClientRect();
+ const safeScale = clampNumber(scale, FLOAT_SCALE_MIN, FLOAT_SCALE_MAX);
+ const width = Math.max(40, rect.width / (Number(state.uiState.scale) || 1) * safeScale);
+ const height = Math.max(28, rect.height / (Number(state.uiState.scale) || 1) * safeScale);
+ return {
+ x: clampNumber(x, 0, Math.max(0, window.innerWidth - width)),
+ y: clampNumber(y, 0, Math.max(0, window.innerHeight - height)),
+ scale: safeScale,
+ };
+ }
+
+ function installFloatingControls(root) {
+ if (!root || root.dataset.floatingControlsInstalled === "true") return;
+ root.dataset.floatingControlsInstalled = "true";
+
+ const onPointerDown = (event) => {
+ if (root.dataset.placement !== "floating" || event.button !== 0) return;
+ if (event.target && event.target.closest(".ccm-context-menu")) return;
+
+ const uiState = readUiState();
+ const startX = event.clientX;
+ const startY = event.clientY;
+ const pointerId = event.pointerId;
+ let dragging = false;
+ const holdTimer = window.setTimeout(() => {
+ dragging = true;
+ root.dataset.dragging = "true";
+ try {
+ root.setPointerCapture(pointerId);
+ } catch {
+ }
+ }, FLOAT_DRAG_HOLD_MS);
+
+ const onPointerMove = (moveEvent) => {
+ if (!dragging) return;
+ const next = clampFloatingPosition(
+ root,
+ uiState.x + moveEvent.clientX - startX,
+ uiState.y + moveEvent.clientY - startY,
+ uiState.scale,
+ );
+ state.uiState = { ...uiState, ...next, mode: "floating" };
+ applyFloatingUiState(root);
+ };
+
+ const finish = () => {
+ window.clearTimeout(holdTimer);
+ document.removeEventListener("pointermove", onPointerMove, true);
+ document.removeEventListener("pointerup", finish, true);
+ document.removeEventListener("pointercancel", finish, true);
+ if (dragging) {
+ root.dataset.dragging = "false";
+ writeUiState();
+ }
+ };
+
+ document.addEventListener("pointermove", onPointerMove, true);
+ document.addEventListener("pointerup", finish, true);
+ document.addEventListener("pointercancel", finish, true);
+ };
+
+ const onWheel = (event) => {
+ if (root.dataset.placement !== "floating") return;
+ event.preventDefault();
+ const current = readUiState();
+ const direction = event.deltaY < 0 ? 1 : -1;
+ const nextScale = clampNumber(current.scale + direction * FLOAT_SCALE_STEP, FLOAT_SCALE_MIN, FLOAT_SCALE_MAX);
+ const next = clampFloatingPosition(root, current.x, current.y, nextScale);
+ state.uiState = { ...current, ...next, mode: "floating" };
+ applyFloatingUiState(root);
+ writeUiState();
+ };
+
+ root.addEventListener("pointerdown", onPointerDown);
+ root.addEventListener("wheel", onWheel, { passive: false });
+ state.floatingPointerCleanup = () => {
+ root.removeEventListener("pointerdown", onPointerDown);
+ root.removeEventListener("wheel", onWheel);
+ };
+ }
+
+ function toNumber(value, unit) {
+ if (value == null) return null;
+
+ const parsed = Number(String(value).replace(/,/g, ""));
+ if (!Number.isFinite(parsed)) return null;
+
+ const normalizedUnit = String(unit || "").toLowerCase();
+ if (normalizedUnit === "k") return parsed * 1000;
+ if (normalizedUnit === "m") return parsed * 1000000;
+ return parsed;
+ }
+
+ function clampPercent(value) {
+ if (!Number.isFinite(value)) return null;
+ return Math.max(0, Math.min(100, value));
+ }
+
+ function clampNumber(value, min, max) {
+ if (!Number.isFinite(value)) return min;
+ return Math.max(min, Math.min(max, value));
+ }
+
+ function numberOrDefault(value, fallback) {
+ const number = Number(value);
+ return Number.isFinite(number) ? number : fallback;
+ }
+
+ function readUiState() {
+ try {
+ const parsed = JSON.parse(localStorage.getItem(UI_STATE_STORAGE_KEY) || "null");
+ const input = parsed && typeof parsed === "object" ? parsed : {};
+ return {
+ mode: input.mode === "floating" ? "floating" : "inline",
+ floatingLayout: input.floatingLayout === "vertical" ? "vertical" : "horizontal",
+ theme: input.theme === "light" ? "light" : "dark",
+ x: clampNumber(Number(input.x), 0, Math.max(0, window.innerWidth - 80)),
+ y: clampNumber(Number(input.y), 0, Math.max(0, window.innerHeight - 40)),
+ scale: clampNumber(Number(input.scale) || 1, FLOAT_SCALE_MIN, FLOAT_SCALE_MAX),
+ };
+ } catch {
+ return { ...DEFAULT_FLOATING_UI };
+ }
+ }
+
+ function writeUiState() {
+ try {
+ localStorage.setItem(UI_STATE_STORAGE_KEY, JSON.stringify(state.uiState));
+ } catch {
+ }
+ }
+
+ function shouldShowUsedInsteadOfLeft(config = state.uiConfig) {
+ const context = config && config.context;
+ return !!(context && context.showUsedInsteadOfLeft === true);
+ }
+
+ function normalizeLevelThresholds(value, defaults) {
+ const input = value && typeof value === "object" ? value : {};
+
+ return {
+ criticalLeftPercent: clampPercent(numberOrDefault(input.criticalLeftPercent, defaults.criticalLeftPercent)),
+ dangerLeftPercent: clampPercent(numberOrDefault(input.dangerLeftPercent, defaults.dangerLeftPercent)),
+ warnLeftPercent: clampPercent(numberOrDefault(input.warnLeftPercent, defaults.warnLeftPercent)),
+ noticeLeftPercent: clampPercent(numberOrDefault(input.noticeLeftPercent, defaults.noticeLeftPercent)),
+ };
+ }
+
+ function normalizeUiConfig(value) {
+ const input = value && typeof value === "object" ? value : {};
+ const context = input.context && typeof input.context === "object" ? input.context : {};
+ const provider = input.provider && typeof input.provider === "object" ? input.provider : {};
+
+ return {
+ context: {
+ showUsedInsteadOfLeft: context.showUsedInsteadOfLeft === true,
+ compressionWarningLeftPercent: clampPercent(numberOrDefault(
+ context.compressionWarningLeftPercent,
+ DEFAULT_UI_CONFIG.context.compressionWarningLeftPercent,
+ )),
+ levelThresholds: normalizeLevelThresholds(
+ context.levelThresholds,
+ DEFAULT_UI_CONFIG.context.levelThresholds,
+ ),
+ },
+ provider: {
+ levelThresholds: normalizeLevelThresholds(
+ provider.levelThresholds,
+ DEFAULT_UI_CONFIG.provider.levelThresholds,
+ ),
+ },
+ };
+ }
+
+ function readUiConfig() {
+ const summaryConfig = state.providerSummary && state.providerSummary.ui;
+ return normalizeUiConfig(summaryConfig || window[CONFIG_KEY] || DEFAULT_UI_CONFIG);
+ }
+
+ function levelForLeftPercent(leftPercent, scope) {
+ const config = scope === "provider" ? state.uiConfig.provider : state.uiConfig.context;
+ const thresholds = config.levelThresholds;
+ if (leftPercent <= thresholds.criticalLeftPercent) return "critical";
+ if (leftPercent <= thresholds.dangerLeftPercent) return "danger";
+ if (leftPercent <= thresholds.warnLeftPercent) return "warn";
+ if (leftPercent <= thresholds.noticeLeftPercent) return "notice";
+ return "normal";
+ }
+
+ function shouldShowCompressionWarning(leftPercent) {
+ const threshold = state.uiConfig.context.compressionWarningLeftPercent;
+ return Number.isFinite(threshold) && leftPercent <= threshold;
+ }
+
+ function compactNumber(value) {
+ if (!Number.isFinite(value)) return "";
+ if (value >= 1000000) return `${(value / 1000000).toFixed(1)}M`;
+ if (value >= 1000) return `${(value / 1000).toFixed(1)}K`;
+ return String(Math.round(value));
+ }
+
+ function formatTokenCount(value) {
+ if (!Number.isFinite(value)) return "--";
+ return Math.round(value).toLocaleString("en-US");
+ }
+
+ function formatAmount(value) {
+ if (!Number.isFinite(value)) return "--";
+ if (Math.abs(value) >= 1000) return value.toLocaleString("en-US", { maximumFractionDigits: 1 });
+ return value.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
+ }
+
+ function formatMoney(value) {
+ const amount = formatAmount(value);
+ return amount === "--" ? amount : `$${amount}`;
+ }
+
+ function formatProviderTitle(name, provider, usedAmount, remainingAmount, totalAmount, usedPercent, leftPercent) {
+ return [
+ `${name} Balance`,
+ `Left: ${formatMoney(remainingAmount)} (${leftPercent.toFixed(1)}%)`,
+ `Used: ${formatMoney(usedAmount)} (${usedPercent.toFixed(1)}%)`,
+ `Total: ${formatMoney(totalAmount)}`,
+ `Status: ${provider.status || "unknown"}`,
+ ].join(" | ");
+ }
+
+ function formatContextTitle(reading, usedTokens, remainingTokens, leftPercent, usedPercent) {
+ const limitTokens = Number.isFinite(reading.limit) ? reading.limit : null;
+ const parts = [
+ "Context",
+ `Left: ${formatTokenCount(remainingTokens)} Tokens (${leftPercent.toFixed(1)}%)`,
+ `Used: ${formatTokenCount(usedTokens)} Tokens (${usedPercent.toFixed(1)}%)`,
+ ];
+ if (Number.isFinite(limitTokens)) parts.push(`Total: ${formatTokenCount(limitTokens)} Tokens`);
+ if (reading.source) parts.push(`Source: ${reading.source}`);
+ return parts.join(" | ");
+ }
+
+ function pruneSpendHistory(now = Date.now()) {
+ const cutoff = now - SPEND_HISTORY_WINDOW_MS;
+ for (const kind of ["context", "provider"]) {
+ const items = state.spendHistory[kind];
+ while (items.length && items[0].time < cutoff) items.shift();
+ if (items.length > SPEND_HISTORY_MAX_ITEMS) {
+ items.splice(0, items.length - SPEND_HISTORY_MAX_ITEMS);
+ }
+ }
+ }
+
+ function recordSpend(kind, amount, meta) {
+ if (!Number.isFinite(amount) || amount <= 0 || !state.spendHistory[kind]) return;
+
+ const now = Date.now();
+ const conversationId =
+ kind === "context"
+ ? normalizeConversationId(meta || state.activeConversationId || "__unknown__") || "__unknown__"
+ : metaConversationId();
+ const itemMeta = kind === "provider" ? String(meta || "") : "";
+ if (kind === "context") {
+ const previousTotal = state.contextSessionTotalsByConversationId.get(conversationId) || 0;
+ state.contextSessionTotalsByConversationId.set(conversationId, previousTotal + amount);
+ } else {
+ const previousTotal = state.providerSessionTotalsByConversationId.get(conversationId) || 0;
+ state.providerSessionTotalsByConversationId.set(conversationId, previousTotal + amount);
+ }
+
+ state.spendHistory[kind].push({
+ time: now,
+ amount,
+ conversationId,
+ meta: itemMeta,
+ });
+ pruneSpendHistory(now);
+ if (state.root && state.root.dataset.historyOpen === "true") {
+ renderSpendHistory();
+ }
+ }
+
+ function formatHistoryTime(time) {
+ const date = new Date(time);
+ return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
+ }
+
+ function formatHistoryDelta(kind, amount) {
+ if (kind === "provider") return `-${formatMoney(amount)}`;
+ return `-${Math.round(amount).toLocaleString("en-US")} Tokens`;
+ }
+
+ function formatHistoryAxisValue(kind, amount) {
+ if (kind === "provider") return formatMoney(amount);
+ if (!Number.isFinite(amount)) return "--";
+ if (amount >= 1000000) return `${(amount / 1000000).toFixed(1)}M`;
+ if (amount >= 1000) return `${(amount / 1000).toFixed(1)}K`;
+ return String(Math.round(amount));
+ }
+
+ function formatHistoryPointTitle(kind, point) {
+ const lines = [
+ `${formatHistoryTime(point.item.time)} ${formatHistoryDelta(kind, point.item.amount)}`,
+ ];
+ if (point.item.meta) lines.push(String(point.item.meta));
+ return lines.join("\n");
+ }
+
+ function currentContextHistorySnapshot(conversationId) {
+ const reading = state.lastReading;
+ if (!reading || !conversationIdsMatch(reading.conversationId, conversationId)) return null;
+ const used = Number(reading.used);
+ if (!Number.isFinite(used) || used <= 0) return null;
+
+ return {
+ time: Date.now(),
+ amount: used,
+ conversationId: normalizeConversationId(conversationId || reading.conversationId || "__unknown__") || "__unknown__",
+ meta: "Current used",
+ };
+ }
+
+ function currentProviderHistorySnapshot(conversationId) {
+ const provider = pickProviderSummary(readProviderSummary());
+ if (!provider) return null;
+
+ const remainingAmount = Number(provider.remainingAmount);
+ const totalAmount = Number(provider.totalAmount);
+ const usedAmount = Number.isFinite(Number(provider.usedAmount))
+ ? Number(provider.usedAmount)
+ : totalAmount - remainingAmount;
+ if (!Number.isFinite(usedAmount) || usedAmount <= 0) return null;
+
+ return {
+ time: Date.now(),
+ amount: usedAmount,
+ conversationId: normalizeConversationId(conversationId || "__unknown__") || "__unknown__",
+ meta: String(provider.displayName || provider.id || "Current used"),
+ };
+ }
+
+ function currentHistorySnapshot(kind, conversationId) {
+ return kind === "context"
+ ? currentContextHistorySnapshot(conversationId)
+ : currentProviderHistorySnapshot(conversationId);
+ }
+
+ function svgPoint(value) {
+ return Number.isFinite(value) ? value.toFixed(1) : "0.0";
+ }
+
+ function makeSpendHistoryChart(items, kind) {
+ const now = Date.now();
+ const cutoff = now - SPEND_HISTORY_WINDOW_MS;
+ const width = SPEND_HISTORY_CHART_WIDTH;
+ const height = SPEND_HISTORY_CHART_HEIGHT;
+ const padding = SPEND_HISTORY_CHART_PADDING;
+ const axisWidth = SPEND_HISTORY_CHART_AXIS_WIDTH;
+ const plotLeft = axisWidth;
+ const plotRight = width - padding;
+ const plotTop = padding;
+ const plotBottom = height - padding;
+ const innerWidth = plotRight - plotLeft;
+ const innerHeight = height - padding * 2;
+ const chart = document.createElement("div");
+ chart.className = "ccm-history-chart";
+
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+ svg.setAttribute("class", "ccm-history-svg");
+ svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
+ svg.setAttribute("preserveAspectRatio", "none");
+ svg.setAttribute("aria-hidden", "true");
+
+ const validItems = [];
+ for (const item of items) {
+ if (!Number.isFinite(item.amount) || item.amount <= 0) continue;
+ const time = Number(item.time);
+ if (!Number.isFinite(time) || time < cutoff) continue;
+ validItems.push({
+ time: clampNumber(time, cutoff, now),
+ amount: item.amount,
+ meta: item.meta || "",
+ });
+ }
+
+ if (!validItems.length) {
+ const empty = document.createElement("div");
+ empty.className = "ccm-history-empty";
+ empty.textContent = "No spend in the last hour";
+ chart.append(svg, empty);
+ return chart;
+ }
+
+ const firstTime = validItems[0].time;
+ const lastTime = validItems[validItems.length - 1].time;
+ const timeSpan = lastTime - firstTime;
+ const useIndexAxis = validItems.length === 1 || timeSpan <= 0;
+ const axisLabel = formatHistoryTime(firstTime) === formatHistoryTime(lastTime)
+ ? formatHistoryTime(firstTime)
+ : `${formatHistoryTime(firstTime)}-${formatHistoryTime(lastTime)}`;
+ const rawPoints = [];
+ validItems.forEach((item, index) => {
+ const xRatio = useIndexAxis
+ ? index / Math.max(validItems.length - 1, 1)
+ : (item.time - firstTime) / timeSpan;
+ const x = plotLeft + xRatio * innerWidth;
+ rawPoints.push({ x, value: item.amount, item });
+ });
+
+ const minValue = Math.min(...rawPoints.map((point) => point.value));
+ const maxValue = Math.max(...rawPoints.map((point) => point.value));
+ const fallbackRange = Math.max(maxValue, 1) * 0.12;
+ const axisMin = minValue === maxValue ? Math.max(0, minValue - fallbackRange) : minValue;
+ const axisMax = minValue === maxValue ? maxValue + fallbackRange : maxValue;
+ const axisRange = Math.max(axisMax - axisMin, 1);
+ const yForValue = (value) => plotBottom - ((value - axisMin) / axisRange) * innerHeight;
+ const points = rawPoints.map((point) => ({
+ ...point,
+ y: yForValue(point.value),
+ }));
+
+ if (points.length === 1) {
+ const onlyPoint = points[0];
+ points.push({
+ x: plotRight,
+ y: onlyPoint.y,
+ value: onlyPoint.value,
+ item: onlyPoint.item,
+ isSynthetic: true,
+ });
+ }
+
+ const yAxis = document.createElementNS("http://www.w3.org/2000/svg", "path");
+ yAxis.setAttribute("class", "ccm-history-axis-line");
+ yAxis.setAttribute("d", `M ${svgPoint(plotLeft)} ${svgPoint(plotTop)} V ${svgPoint(plotBottom)} H ${svgPoint(plotRight)}`);
+ svg.appendChild(yAxis);
+
+ const topGridline = document.createElementNS("http://www.w3.org/2000/svg", "path");
+ topGridline.setAttribute("class", "ccm-history-gridline");
+ topGridline.setAttribute("d", `M ${svgPoint(plotLeft)} ${svgPoint(plotTop)} H ${svgPoint(plotRight)}`);
+ svg.appendChild(topGridline);
+
+ const bottomGridline = document.createElementNS("http://www.w3.org/2000/svg", "path");
+ bottomGridline.setAttribute("class", "ccm-history-gridline");
+ bottomGridline.setAttribute("d", `M ${svgPoint(plotLeft)} ${svgPoint(plotBottom)} H ${svgPoint(plotRight)}`);
+ svg.appendChild(bottomGridline);
+
+ const topLabel = document.createElementNS("http://www.w3.org/2000/svg", "text");
+ topLabel.setAttribute("class", "ccm-history-axis-label");
+ topLabel.setAttribute("x", "2");
+ topLabel.setAttribute("y", svgPoint(plotTop + 4));
+ topLabel.textContent = formatHistoryAxisValue(kind, axisMax);
+ svg.appendChild(topLabel);
+
+ const bottomLabel = document.createElementNS("http://www.w3.org/2000/svg", "text");
+ bottomLabel.setAttribute("class", "ccm-history-axis-label");
+ bottomLabel.setAttribute("x", "2");
+ bottomLabel.setAttribute("y", svgPoint(plotBottom));
+ bottomLabel.textContent = formatHistoryAxisValue(kind, axisMin);
+ svg.appendChild(bottomLabel);
+
+ const linePath = points
+ .map((point, index) => `${index === 0 ? "M" : "L"} ${svgPoint(point.x)} ${svgPoint(point.y)}`)
+ .join(" ");
+ const areaPath = `${linePath} L ${svgPoint(points[points.length - 1].x)} ${svgPoint(plotBottom)} L ${svgPoint(points[0].x)} ${svgPoint(plotBottom)} Z`;
+
+ const area = document.createElementNS("http://www.w3.org/2000/svg", "path");
+ area.setAttribute("class", "ccm-history-area");
+ area.setAttribute("d", areaPath);
+ svg.appendChild(area);
+
+ const line = document.createElementNS("http://www.w3.org/2000/svg", "path");
+ line.setAttribute("class", "ccm-history-line");
+ line.setAttribute("d", linePath);
+ svg.appendChild(line);
+
+ const realPoints = points.filter((historyPoint) => !historyPoint.isSynthetic);
+ const lastPoint = realPoints[realPoints.length - 1] || points[points.length - 1];
+ for (const historyPoint of realPoints) {
+ const isLatestPoint = historyPoint === lastPoint;
+ const point = document.createElementNS("http://www.w3.org/2000/svg", "circle");
+ point.setAttribute("class", "ccm-history-point");
+ point.setAttribute("cx", svgPoint(historyPoint.x));
+ point.setAttribute("cy", svgPoint(historyPoint.y));
+ point.setAttribute("r", isLatestPoint ? "3" : "2.4");
+ const pointTitle = document.createElementNS("http://www.w3.org/2000/svg", "title");
+ pointTitle.textContent = formatHistoryPointTitle(kind, historyPoint);
+ point.appendChild(pointTitle);
+ svg.appendChild(point);
+
+ const hit = document.createElementNS("http://www.w3.org/2000/svg", "circle");
+ hit.setAttribute("class", "ccm-history-hit");
+ hit.setAttribute("cx", svgPoint(historyPoint.x));
+ hit.setAttribute("cy", svgPoint(historyPoint.y));
+ hit.setAttribute("r", "8");
+ const title = document.createElementNS("http://www.w3.org/2000/svg", "title");
+ title.textContent = formatHistoryPointTitle(kind, historyPoint);
+ hit.appendChild(title);
+ svg.appendChild(hit);
+ }
+
+ const caption = document.createElement("div");
+ caption.className = "ccm-history-caption";
+ const windowLabel = document.createElement("span");
+ windowLabel.textContent = axisLabel;
+ const lastLabel = document.createElement("span");
+ lastLabel.textContent = formatHistoryDelta(kind, lastPoint.item.amount);
+ if (lastPoint.item.meta) lastLabel.title = String(lastPoint.item.meta);
+ caption.append(windowLabel, lastLabel);
+
+ chart.append(svg, caption);
+ return chart;
+ }
+
+ function renderHistorySection(kind) {
+ const panel = state.historyPanel;
+ const section = panel && panel.querySelector(`[data-history-kind="${kind}"]`);
+ if (!section) return;
+
+ const visibleCard = kind === "provider" ? state.providerCard : state.contextCard;
+ if (!visibleCard || visibleCard.hidden) {
+ section.hidden = true;
+ return;
+ }
+ if (section.hidden) section.hidden = false;
+
+ pruneSpendHistory();
+ const conversationId = metaConversationId();
+ let items = state.spendHistory[kind].filter((item) => {
+ const itemConversationId = normalizeConversationId(item.conversationId || "__unknown__") || "__unknown__";
+ return itemConversationId === conversationId;
+ });
+ let total =
+ kind === "context"
+ ? contextSessionTotal(conversationId)
+ : providerSessionTotal(conversationId);
+ if (!items.length) {
+ const snapshot = currentHistorySnapshot(kind, conversationId);
+ if (snapshot) {
+ items = [snapshot];
+ total = snapshot.amount;
+ }
+ }
+ const totalNode = section.querySelector(".ccm-history-total");
+ const chart = section.querySelector(".ccm-history-chart");
+ if (totalNode) totalNode.textContent = total > 0 ? formatHistoryDelta(kind, total) : "--";
+ if (!chart) return;
+
+ chart.replaceWith(makeSpendHistoryChart(items, kind));
+ }
+
+ function metaConversationId() {
+ return normalizeConversationId(state.activeConversationId || (state.lastReading && state.lastReading.conversationId) || "__unknown__") || "__unknown__";
+ }
+
+ function contextSessionTotal(conversationId) {
+ const normalizedConversationId = normalizeConversationId(conversationId || "__unknown__") || "__unknown__";
+ return state.contextSessionTotalsByConversationId.get(normalizedConversationId) || 0;
+ }
+
+ function providerSessionTotal(conversationId) {
+ const normalizedConversationId = normalizeConversationId(conversationId || "__unknown__") || "__unknown__";
+ return state.providerSessionTotalsByConversationId.get(normalizedConversationId) || 0;
+ }
+
+ function renderSpendHistory() {
+ const grid = state.historyPanel && state.historyPanel.querySelector(".ccm-history-grid");
+ if (grid) {
+ grid.dataset.providerVisible = state.providerCard && !state.providerCard.hidden ? "true" : "false";
+ }
+ renderHistorySection("context");
+ renderHistorySection("provider");
+ }
+
+ function clampHistoryPanelToViewport(root) {
+ const panel = state.historyPanel;
+ if (!root || !panel) return;
+
+ const viewportWidth = window.innerWidth || document.documentElement.clientWidth || 0;
+ const viewportHeight = window.innerHeight || document.documentElement.clientHeight || 0;
+ if (viewportWidth <= 0 || viewportHeight <= 0) return;
+
+ const maxWidth = Math.max(
+ HISTORY_PANEL_MIN_WIDTH,
+ viewportWidth - HISTORY_PANEL_VIEWPORT_PADDING * 2,
+ );
+ const maxHeight = Math.max(
+ HISTORY_PANEL_MIN_HEIGHT,
+ viewportHeight - HISTORY_PANEL_VIEWPORT_PADDING * 2,
+ );
+ panel.style.setProperty("--ccm-history-max-width", `${maxWidth}px`);
+ panel.style.setProperty("--ccm-history-max-height", `${maxHeight}px`);
+
+ const anchor = state.contextCard && !state.contextCard.hidden
+ ? state.contextCard
+ : state.providerCard && !state.providerCard.hidden
+ ? state.providerCard
+ : root;
+ const anchorRect = anchor.getBoundingClientRect();
+ const panelRect = panel.getBoundingClientRect();
+ const panelWidth = Math.min(
+ Math.max(panelRect.width || HISTORY_PANEL_MIN_WIDTH, HISTORY_PANEL_MIN_WIDTH),
+ maxWidth,
+ );
+ const panelHeight = Math.min(
+ Math.max(panelRect.height || HISTORY_PANEL_MIN_HEIGHT, HISTORY_PANEL_MIN_HEIGHT),
+ maxHeight,
+ );
+ const left = clampNumber(
+ anchorRect.left,
+ HISTORY_PANEL_VIEWPORT_PADDING,
+ Math.max(HISTORY_PANEL_VIEWPORT_PADDING, viewportWidth - panelWidth - HISTORY_PANEL_VIEWPORT_PADDING),
+ );
+ const belowTop = anchorRect.bottom + HISTORY_PANEL_GAP;
+ const aboveTop = anchorRect.top - panelHeight - HISTORY_PANEL_GAP;
+ const top = belowTop + panelHeight + HISTORY_PANEL_VIEWPORT_PADDING <= viewportHeight
+ ? belowTop
+ : Math.max(HISTORY_PANEL_VIEWPORT_PADDING, aboveTop);
+
+ panel.style.setProperty("--ccm-history-left", `${Math.round(left)}px`);
+ panel.style.setProperty("--ccm-history-top", `${Math.round(top)}px`);
+ }
+
+ function openSpendHistory() {
+ const root = state.root;
+ if (!root) return;
+ if (root.hidden) {
+ closeSpendHistory();
+ return;
+ }
+ ensureHistoryPortal(root);
+ if (state.historyCloseTimer) {
+ window.clearTimeout(state.historyCloseTimer);
+ state.historyCloseTimer = 0;
+ }
+ renderSpendHistory();
+ clampHistoryPanelToViewport(root);
+ if (root.dataset.historyOpen !== "true") root.dataset.historyOpen = "true";
+ if (state.historyPortal) state.historyPortal.dataset.historyOpen = "true";
+ if (state.historyPanel) state.historyPanel.setAttribute("aria-hidden", "false");
+ }
+
+ function refreshOpenSpendHistory(root = state.root) {
+ if (!root || root.dataset.historyOpen !== "true") return;
+ ensureHistoryPortal(root);
+ renderSpendHistory();
+ clampHistoryPanelToViewport(root);
+ }
+
+ function closeSpendHistory() {
+ const root = state.root;
+ if (!root) return;
+ if (state.historyCloseTimer) {
+ window.clearTimeout(state.historyCloseTimer);
+ state.historyCloseTimer = 0;
+ }
+ if (root.dataset.historyOpen !== "false") root.dataset.historyOpen = "false";
+ if (state.historyPortal) state.historyPortal.dataset.historyOpen = "false";
+ if (state.historyPanel) {
+ state.historyPanel.setAttribute("aria-hidden", "true");
+ }
+ }
+
+ function scheduleCloseSpendHistory(event) {
+ const root = state.root;
+ if (!root) return;
+ const relatedTarget = event && event.relatedTarget;
+ if (relatedTarget && typeof relatedTarget.nodeType === "number" && root.contains(relatedTarget)) return;
+ if (state.historyCloseTimer) window.clearTimeout(state.historyCloseTimer);
+ state.historyCloseTimer = 0;
+ closeSpendHistory();
+ }
+
+ function expandedRectContainsPoint(rect, x, y, expand) {
+ return !!rect &&
+ x >= rect.left - expand &&
+ x <= rect.right + expand &&
+ y >= rect.top - expand &&
+ y <= rect.bottom + expand;
+ }
+
+ function isPointerInsideHistorySurface(x, y) {
+ const root = state.root;
+ if (!root || root.hidden) return false;
+
+ const cards = [state.contextCard, state.providerCard].filter((card) => card && !card.hidden);
+ if (cards.some((card) => expandedRectContainsPoint(card.getBoundingClientRect(), x, y, 6))) {
+ return true;
+ }
+
+ return false;
+ }
+
+ function installHistoryPointerTracker(root) {
+ const onPointerMove = (event) => {
+ if (isPointerInsideHistorySurface(event.clientX, event.clientY)) {
+ openSpendHistory();
+ } else if (root.dataset.historyOpen === "true") {
+ scheduleCloseSpendHistory();
+ }
+ };
+ const onPointerLeave = () => {
+ if (root.dataset.historyOpen === "true") scheduleCloseSpendHistory();
+ };
+
+ document.addEventListener("pointermove", onPointerMove, { passive: true });
+ document.addEventListener("pointerdown", onPointerMove, { passive: true });
+ window.addEventListener("blur", closeSpendHistory);
+ document.addEventListener("mouseleave", onPointerLeave);
+
+ return () => {
+ document.removeEventListener("pointermove", onPointerMove);
+ document.removeEventListener("pointerdown", onPointerMove);
+ window.removeEventListener("blur", closeSpendHistory);
+ document.removeEventListener("mouseleave", onPointerLeave);
+ };
+ }
+
+ function installHistoryHover(root) {
+ if (!root) return;
+ if (state.historyHoverCleanup && root.dataset.historyHoverInstalled === "true") return;
+ if (state.historyHoverCleanup) state.historyHoverCleanup();
+ root.dataset.historyHoverInstalled = "true";
+ if (root.dataset.historyOpen !== "true") root.dataset.historyOpen = "false";
+ ensureHistoryPortal(root);
+ if (state.historyPortal && state.historyPortal.dataset.historyOpen !== "true") {
+ state.historyPortal.dataset.historyOpen = "false";
+ }
+ state.historyHoverCleanup = installHistoryPointerTracker(root);
+ }
+
+ function clearSpendEffects() {
+ window.clearTimeout(state.spendEffectTimer);
+ state.spendEffectTimer = 0;
+ state.spendEffectQueue.length = 0;
+ if (state.spendEffectActive) {
+ state.spendEffectActive.remove();
+ state.spendEffectActive = null;
+ }
+ const root = state.root || document.getElementById(ROOT_ID);
+ root?.querySelectorAll(".ccm-hit-pop").forEach((node) => node.remove());
+ }
+
+ function hasPendingSpendEffects() {
+ return !!(state.spendEffectActive || state.spendEffectQueue.length);
+ }
+
+ function finishSpendEffect(pop) {
+ if (state.spendEffectActive !== pop) return;
+ window.clearTimeout(state.spendEffectTimer);
+ state.spendEffectTimer = 0;
+ state.spendEffectActive = null;
+ pop.remove();
+ playNextSpendEffect();
+ if (!hasPendingSpendEffects()) {
+ const root = state.root || document.getElementById(ROOT_ID);
+ if (root) updateDockVisibility(root);
+ }
+ }
+
+ function playNextSpendEffect() {
+ if (state.spendEffectActive) return;
+ const root = state.root || document.getElementById(ROOT_ID);
+ if (!root || root.hidden) return;
+
+ const text = state.spendEffectQueue.shift();
+ if (!text) return;
+
+ const pop = document.createElement("div");
+ pop.className = "ccm-hit-pop";
+ pop.textContent = text;
+ state.spendEffectActive = pop;
+ root.appendChild(pop);
+
+ const onAnimationEnd = (event) => {
+ if (event.target !== pop || event.animationName !== "ccm-hit-pop") return;
+ pop.removeEventListener("animationend", onAnimationEnd);
+ finishSpendEffect(pop);
+ };
+ pop.addEventListener("animationend", onAnimationEnd);
+ state.spendEffectTimer = window.setTimeout(() => finishSpendEffect(pop), SPEND_EFFECT_FALLBACK_MS);
+ }
+
+ function enqueueSpendEffect(text) {
+ if (!text) return;
+ state.spendEffectQueue.push(text);
+ playNextSpendEffect();
+ }
+
+ function showTokenSpendEffect(deltaTokens) {
+ if (!Number.isFinite(deltaTokens) || deltaTokens <= 0) return;
+ enqueueSpendEffect(`-${Math.round(deltaTokens).toLocaleString("en-US")} Tokens`);
+ }
+
+ function showProviderSpendEffect(deltaAmount) {
+ if (!Number.isFinite(deltaAmount) || deltaAmount <= 0) return;
+ enqueueSpendEffect(`-${formatMoney(deltaAmount)}`);
+ }
+
+ function shouldShowContextSpendEffect(conversationId, currentUsed) {
+ if (!Number.isFinite(currentUsed)) return false;
+ const now = Date.now();
+ const normalizedConversationId = normalizeConversationId(conversationId || "__unknown__") || "__unknown__";
+ const roundedCurrentUsed = Math.round(currentUsed);
+ const previous = window[CONTEXT_SPEND_DEDUPE_KEY];
+ const isSameReading =
+ previous &&
+ previous.currentUsed === roundedCurrentUsed &&
+ (
+ previous.conversationId === normalizedConversationId ||
+ previous.conversationId === "__unknown__" ||
+ normalizedConversationId === "__unknown__"
+ );
+ if (isSameReading && now - previous.at < CONTEXT_SPEND_DEDUPE_WINDOW_MS) {
+ return false;
+ }
+ window[CONTEXT_SPEND_DEDUPE_KEY] = {
+ conversationId: normalizedConversationId,
+ currentUsed: roundedCurrentUsed,
+ at: now,
+ };
+ return true;
+ }
+
+ function shouldShowProviderSpendEffect(providerId, currentUsed) {
+ if (!providerId || !Number.isFinite(currentUsed)) return false;
+ const now = Date.now();
+ const key = `${providerId}:${currentUsed}`;
+ const previous = window[PROVIDER_SPEND_DEDUPE_KEY];
+ if (previous && previous.key === key && now - previous.at < PROVIDER_SPEND_DEDUPE_WINDOW_MS) {
+ return false;
+ }
+ window[PROVIDER_SPEND_DEDUPE_KEY] = { key, at: now };
+ return true;
+ }
+
+ function hasDescendant(element, selector) {
+ return !!(element && element.querySelector(selector));
+ }
+
+ // 只把主会话内容区当作可显示区域;Codex App DOM 更新后,优先从这里重找 thread/transcript 锚点。
+ function hasThreadContentSurface() {
+ if (!isConversationWindow()) return false;
+
+ const now = Date.now();
+ if (now - state.threadContentLookupAt < ACTIVE_CONVERSATION_LOOKUP_CACHE_MS) {
+ return state.threadContentLookupResult;
+ }
+
+ const main = document.querySelector("main");
+ const result = hasDescendant(main, THREAD_CONTENT_SELECTOR);
+ state.threadContentLookupAt = now;
+ state.threadContentLookupResult = result;
+ return result;
+ }
+
+ function invalidateThreadContentCache() {
+ state.threadContentLookupAt = 0;
+ state.inlineMountLookupAt = 0;
+ }
+
+ // Pet/头像层也运行在 app://-/index.html,需要额外按 route 排除非对话窗口。
+ function isConversationWindow() {
+ const url = new URL(location.href);
+ const route = `${url.pathname} ${url.search} ${url.hash}`.toLowerCase();
+ if (route.includes("avatar-overlay") || route.includes("pet")) return false;
+
+ return url.protocol === "app:" && url.pathname.endsWith("/index.html");
+ }
+
+ function updateDockVisibility(root) {
+ const contextVisible = state.contextCard && !state.contextCard.hidden;
+ const providerVisible = state.providerCard && !state.providerCard.hidden;
+ if (state.inlineMountPending && (state.uiState || readUiState()).mode !== "floating") {
+ root.hidden = true;
+ closeSpendHistory();
+ return;
+ }
+
+ const hidden = !contextVisible && !providerVisible;
+ const keepVisibleForSpend = hidden && hasPendingSpendEffects();
+ root.hidden = hidden && !keepVisibleForSpend;
+ if (hidden) {
+ closeSpendHistory();
+ if (!keepVisibleForSpend) clearSpendEffects();
+ } else if (root.dataset.historyOpen === "true") {
+ renderSpendHistory();
+ }
+ if (!root.hidden) playNextSpendEffect();
+ }
+
+ function hideMeter(root, card, value, fill, title) {
+ if (!card) return;
+ if (card.dataset.known !== "false") card.dataset.known = "false";
+ if (card.dataset.level !== "normal") card.dataset.level = "normal";
+ if (card.dataset.compressionWarning !== "false") card.dataset.compressionWarning = "false";
+ if (card.title !== title) card.title = title;
+ if (value.textContent !== "Context Left --") value.textContent = "Context Left --";
+ if (fill.style.width !== "0%") fill.style.width = "0%";
+ const ring = state.contextRing || card.querySelector(".ccm-ring");
+ if (ring && !state.contextRing) state.contextRing = ring;
+ if (ring) ring.style.setProperty("--ccm-ring-angle", "0deg");
+ card.hidden = true;
+ updateDockVisibility(root);
+ }
+
+ function hideProviderMeter(root, reason) {
+ const card = state.providerCard;
+ if (!card) return;
+ if (card.dataset.known !== "false") card.dataset.known = "false";
+ if (card.dataset.level !== "normal") card.dataset.level = "normal";
+ if (card.title !== reason) card.title = reason;
+ if (state.providerFill && state.providerFill.style.width !== "0%") state.providerFill.style.width = "0%";
+ const ring = state.providerRing || card.querySelector(".ccm-ring");
+ if (ring && !state.providerRing) state.providerRing = ring;
+ if (ring) ring.style.setProperty("--ccm-ring-angle", "0deg");
+ card.hidden = true;
+ updateDockVisibility(root);
+ }
+
+ function pickProviderSummary(summary) {
+ const providers = summary && Array.isArray(summary.providers) ? summary.providers : [];
+ return providers.find((provider) => provider && provider.status === "active") || null;
+ }
+
+ function renderProviderMeter(root) {
+ state.providerSummary = readProviderSummary();
+ state.uiConfig = readUiConfig();
+ const provider = pickProviderSummary(state.providerSummary);
+ if (!provider) {
+ hideProviderMeter(root, "No active provider summary is available.");
+ return;
+ }
+
+ const card = state.providerCard;
+ if (!card || !state.providerValue || !state.providerFill) return;
+
+ const usedPercent = clampPercent(Number(provider.usedPercent));
+ const remainingAmount = Number(provider.remainingAmount);
+ const totalAmount = Number(provider.totalAmount);
+ const usedAmount = Number.isFinite(Number(provider.usedAmount))
+ ? Number(provider.usedAmount)
+ : totalAmount - remainingAmount;
+ if (usedPercent == null || !Number.isFinite(remainingAmount) || !Number.isFinite(totalAmount) || !Number.isFinite(usedAmount)) {
+ hideProviderMeter(root, "Provider summary is missing quota values.");
+ return;
+ }
+
+ const leftPercent = clampPercent(100 - usedPercent);
+ const level = levelForLeftPercent(leftPercent, "provider");
+ const name = String(provider.displayName || provider.id || "Provider").slice(0, 48);
+ const text = `${name} Left ${leftPercent.toFixed(1)}% (${formatMoney(remainingAmount)} left)`;
+ const width = `${leftPercent.toFixed(1)}%`;
+ const title = formatProviderTitle(name, provider, usedAmount, remainingAmount, totalAmount, usedPercent, leftPercent);
+ const providerId = String(provider.id || name || "__provider__");
+ const currentUsed = Number(provider.used);
+
+ if (Number.isFinite(currentUsed)) {
+ const previousUsed = state.lastAnimatedProviderUsedById.get(providerId);
+ if (Number.isFinite(previousUsed) && currentUsed > previousUsed && Number.isFinite(provider.total) && provider.total > 0) {
+ const deltaAmount = (currentUsed - previousUsed) * (totalAmount / Number(provider.total));
+ if (shouldShowProviderSpendEffect(providerId, currentUsed)) {
+ recordSpend("provider", deltaAmount, providerId);
+ showProviderSpendEffect(deltaAmount);
+ }
+ }
+ state.lastAnimatedProviderUsedById.set(providerId, currentUsed);
+ }
+
+ if (card.dataset.known !== "true") card.dataset.known = "true";
+ if (card.dataset.level !== level) card.dataset.level = level;
+ if (card.title !== title) card.title = title;
+ if (state.providerValue.textContent !== text) state.providerValue.textContent = text;
+ if (state.providerFill.style.width !== width) state.providerFill.style.width = width;
+ const providerRing = state.providerRing || card.querySelector(".ccm-ring");
+ if (providerRing && !state.providerRing) state.providerRing = providerRing;
+ if (providerRing) providerRing.style.setProperty("--ccm-ring-angle", `${leftPercent * 3.6}deg`);
+ if (card.hidden) card.hidden = false;
+ updateDockVisibility(root);
+ }
+
+ function readProviderSummary() {
+ const summary = window[PROVIDER_SUMMARY_KEY];
+ return summary && typeof summary === "object" ? summary : null;
+ }
+
+ // helper 通过 CDP 写入脱敏 summary;真实 token、用户 ID 和服务商地址不进入渲染页。
+ function setProviderSummary(summary) {
+ state.providerSummary = summary && typeof summary === "object" ? summary : null;
+ if (state.providerSummary) {
+ window[PROVIDER_SUMMARY_KEY] = state.providerSummary;
+ if (state.providerSummary.ui && typeof state.providerSummary.ui === "object") {
+ window[CONFIG_KEY] = normalizeUiConfig(state.providerSummary.ui);
+ }
+ }
+ const root = state.root || document.getElementById(ROOT_ID);
+ if (root) renderProviderMeter(root);
+ }
+
+ function installProviderSummaryListener() {
+ if (state.providerSummaryListener) return;
+ state.providerSummary = readProviderSummary();
+ state.providerSummaryListener = (event) => {
+ setProviderSummary(event && event.detail);
+ };
+ try {
+ window.addEventListener(PROVIDER_SUMMARY_EVENT, state.providerSummaryListener);
+ } catch {
+ }
+ }
+
+ function makeReading(percent, source, raw, used, limit) {
+ const safePercent = clampPercent(percent);
+ if (safePercent == null) return null;
+
+ return {
+ percent: safePercent,
+ source,
+ raw: String(raw || "").slice(0, 240),
+ used: Number.isFinite(used) ? used : null,
+ limit: Number.isFinite(limit) ? limit : null,
+ conversationId: 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 normalizeConversationUuid(value) {
+ if (value == null) return null;
+ if (typeof value !== "string" && typeof value !== "number") return null;
+
+ const match = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i.exec(String(value));
+ return match ? match[0].toLowerCase() : null;
+ }
+
+ function conversationIdsMatch(left, right) {
+ const normalizedLeft = normalizeConversationId(left);
+ const normalizedRight = normalizeConversationId(right);
+ return !!normalizedLeft && !!normalizedRight && normalizedLeft === normalizedRight;
+ }
+
+ function withConversationId(reading, conversationId) {
+ if (!reading) return null;
+
+ const normalizedConversationId = normalizeConversationId(conversationId);
+ if (normalizedConversationId) {
+ reading.conversationId = normalizedConversationId;
+ }
+
+ return reading;
+ }
+
+ // React 私有 key 每次构建都会换后缀;按节点缓存 key 列表能避开冷启动时的大量 Reflect.ownKeys。
+ function getReactPrivateKeys(node) {
+ if (!node || (typeof node !== "object" && typeof node !== "function")) return [];
+
+ const cached = state.reactPrivateKeyCache.get(node);
+ if (cached) return cached;
+
+ let keys = [];
+ try {
+ keys = Reflect.ownKeys(node).map(String).filter((key) => REACT_PRIVATE_KEY_RE.test(key));
+ } catch {
+ keys = [];
+ }
+
+ state.reactPrivateKeyCache.set(node, keys);
+ return keys;
+ }
+
+ function getFilteredReflectKeys(value, cacheName, pattern, limit) {
+ if (!value || (typeof value !== "object" && typeof value !== "function")) return [];
+
+ let cache = state.filteredReflectKeyCache.get(value);
+ if (!cache) {
+ cache = {};
+ state.filteredReflectKeyCache.set(value, cache);
+ }
+
+ if (cache[cacheName]) return cache[cacheName];
+
+ let keys = [];
+ try {
+ keys = Reflect.ownKeys(value)
+ .map(String)
+ .filter((key) => pattern.test(key))
+ .slice(0, limit);
+ } catch {
+ keys = [];
+ }
+
+ cache[cacheName] = keys;
+ return keys;
+ }
+
+ // Codex 部分会话 ID 只存在于 React 写到 DOM 节点上的私有 props,普通 attribute 读不到。
+ function getReactPropValue(node, propName) {
+ if (!node) return null;
+
+ for (const key of getReactPrivateKeys(node)) {
+ if (!key.startsWith("__reactProps$")) continue;
+
+ let props;
+ try {
+ props = node[key];
+ } catch {
+ continue;
+ }
+
+ if (props && props[propName] != null) {
+ return props[propName];
+ }
+ }
+
+ return null;
+ }
+
+ 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") ||
+ getReactPropValue(node, "data-app-action-sidebar-thread-id") ||
+ getReactPropValue(node, "data-thread-id") ||
+ getReactPropValue(node, "data-conversation-id");
+ const normalized = normalizeConversationId(attrValue);
+ if (normalized) return normalized;
+ }
+
+ return null;
+ }
+
+ function getLikelyObjectConversationId(value, depth = 3, seen = new WeakSet()) {
+ if (!value || typeof value !== "object") return null;
+ if (depth < 0 || seen.has(value)) return null;
+ seen.add(value);
+
+ for (const key of CONVERSATION_ID_KEYS) {
+ let candidate;
+ try {
+ candidate = value[key];
+ } catch {
+ continue;
+ }
+
+ const normalized = normalizeConversationUuid(candidate);
+ if (normalized) return normalized;
+ }
+
+ const nested = [
+ value.params,
+ value.thread,
+ value.conversation,
+ value.props,
+ ];
+ for (const child of nested) {
+ if (!child || typeof child !== "object") continue;
+ const normalized = getLikelyObjectConversationId(child, depth - 1, seen);
+ if (normalized) return normalized;
+ }
+
+ return null;
+ }
+
+ function readReactConversationIdFromValue(value, depth, seen) {
+ if (!value || typeof value !== "object" || depth < 0) return null;
+ if (seen.has(value)) return null;
+ seen.add(value);
+
+ const direct = getLikelyObjectConversationId(value);
+ if (direct) return direct;
+
+ if (value.nodeType === Node.ELEMENT_NODE) {
+ const elementConversationId = getElementConversationId(value);
+ if (elementConversationId) return elementConversationId;
+
+ for (const key of getReactPrivateKeys(value)) {
+ let child;
+ try {
+ child = value[key];
+ } catch {
+ continue;
+ }
+
+ const childConversationId = readReactConversationIdFromValue(child, depth - 1, seen);
+ if (childConversationId) return childConversationId;
+ }
+ }
+
+ if (Array.isArray(value)) {
+ const limit = Math.min(value.length, 40);
+ for (let index = 0; index < limit; index += 1) {
+ const childConversationId = readReactConversationIdFromValue(value[index], depth - 1, seen);
+ if (childConversationId) return childConversationId;
+ }
+ return null;
+ }
+
+ if (value instanceof Map) {
+ let index = 0;
+ for (const [mapKey, mapValue] of value) {
+ if (index >= 40) break;
+
+ const keyConversationId = normalizeConversationUuid(mapKey);
+ if (keyConversationId) return keyConversationId;
+
+ const mapKeyConversationId = readReactConversationIdFromValue(mapKey, depth - 1, seen);
+ if (mapKeyConversationId) return mapKeyConversationId;
+
+ const mapValueConversationId = readReactConversationIdFromValue(mapValue, depth - 1, seen);
+ if (mapValueConversationId) return mapValueConversationId;
+
+ index += 1;
+ }
+ return null;
+ }
+
+ const keys = getFilteredReflectKeys(value, "reactConversationId", CONVERSATION_REACT_KEY_RE, 120);
+ for (const key of keys) {
+ let child;
+ try {
+ child = value[key];
+ } catch {
+ continue;
+ }
+
+ if (CONVERSATION_ID_KEY_SET.has(key)) {
+ const keyConversationId = normalizeConversationUuid(child);
+ if (keyConversationId) return keyConversationId;
+ }
+
+ const childConversationId = readReactConversationIdFromValue(child, depth - 1, seen);
+ if (childConversationId) return childConversationId;
+ }
+
+ return null;
+ }
+
+ function readActiveConversationIdFromReact() {
+ const anchors = [
+ document.querySelector("main"),
+ document.querySelector(`[data-thread-find-target="conversation"]`),
+ document.querySelector(THREAD_COMPOSER_SELECTOR),
+ document.querySelector(CODEX_COMPOSER_SELECTOR),
+ document.getElementById("root"),
+ ].filter(Boolean);
+ const seen = new WeakSet();
+
+ for (const anchor of anchors) {
+ const direct = getElementConversationId(anchor);
+ if (direct) return direct;
+
+ for (const key of getReactPrivateKeys(anchor)) {
+ let value;
+ try {
+ value = anchor[key];
+ } catch {
+ continue;
+ }
+
+ const conversationId = readReactConversationIdFromValue(value, REACT_CONVERSATION_SCAN_DEPTH, seen);
+ if (conversationId) return conversationId;
+ }
+ }
+
+ return null;
+ }
+
+ function readActiveConversationId() {
+ const now = Date.now();
+ if (now - state.activeConversationIdLookupAt < ACTIVE_CONVERSATION_LOOKUP_CACHE_MS) {
+ return state.cachedActiveConversationId;
+ }
+
+ // 当前会话 ID 主要挂在左侧会话列表的 current/selected/active 状态节点上。
+ 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) {
+ state.cachedActiveConversationId = conversationId;
+ state.activeConversationIdLookupAt = now;
+ return conversationId;
+ }
+ }
+
+ const reactConversationId = readActiveConversationIdFromReact();
+ if (reactConversationId) {
+ state.cachedActiveConversationId = reactConversationId;
+ state.activeConversationIdLookupAt = now;
+ return reactConversationId;
+ }
+
+ state.cachedActiveConversationId = null;
+ state.activeConversationIdLookupAt = now;
+ return null;
+ }
+
+ function getObjectConversationId(value) {
+ if (!value || typeof value !== "object") return null;
+
+ const candidates = [
+ value.conversationId,
+ value.localConversationId,
+ value.threadId,
+ value.id,
+ value.key,
+ value.params && value.params.conversationId,
+ value.params && value.params.localConversationId,
+ value.params && value.params.threadId,
+ value.thread && value.thread.id,
+ value.thread && value.thread.threadId,
+ value.conversation && value.conversation.id,
+ ];
+
+ for (const candidate of candidates) {
+ const normalized = normalizeConversationId(candidate);
+ if (normalized) return normalized;
+ }
+
+ return null;
+ }
+
+ function activateConversationId(activeConversationId, options = {}) {
+ const normalizedConversationId = normalizeConversationId(activeConversationId);
+ if (!normalizedConversationId) return false;
+
+ if (normalizedConversationId !== state.activeConversationId) {
+ clearRetryUpdate();
+ window.clearTimeout(state.pendingUpdate);
+ state.pendingUpdateDueAt = 0;
+ state.activeConversationId = normalizedConversationId;
+ state.cachedActiveConversationId = normalizedConversationId;
+ state.activeConversationIdLookupAt = Date.now();
+ state.lastReading = state.readingsByConversationId.get(normalizedConversationId) || null;
+ state.lastScanAt = 0;
+ state.lastScannedConversationId = null;
+ state.expensiveFallbackScannedAt = 0;
+ state.expensiveFallbackConversationId = null;
+ state.inlineMountLookupAt = 0;
+ state.navigationPendingUntil = options.pendingNavigation ? Date.now() + NAVIGATION_PENDING_MS : 0;
+ state.switchRetryUntil = Date.now() + SWITCH_RETRY_WINDOW_MS;
+ scheduleRetryUpdate();
+ refreshOpenSpendHistory();
+ return true;
+ }
+
+ return false;
+ }
+
+ // 只刷新会话指针,不触发“切换会话”副作用;用于侧栏隐藏或短暂缺失 active 节点的场景。
+ function retainConversationId(conversationId) {
+ const normalizedConversationId = normalizeConversationId(conversationId);
+ if (!normalizedConversationId) return false;
+
+ state.activeConversationId = normalizedConversationId;
+ state.cachedActiveConversationId = normalizedConversationId;
+ state.activeConversationIdLookupAt = Date.now();
+ return true;
+ }
+
+ function updateActiveConversationId() {
+ const activeConversationId = readActiveConversationId();
+ if (activeConversationId) {
+ if (
+ state.activeConversationId &&
+ !conversationIdsMatch(activeConversationId, state.activeConversationId) &&
+ Date.now() < state.navigationPendingUntil
+ ) {
+ return state.activeConversationId;
+ }
+
+ activateConversationId(activeConversationId);
+ } else if (hasThreadContentSurface() && state.activeConversationId) {
+ // sidebar 隐藏时 active/current 节点会消失;主会话区仍在时沿用最后确认的会话 ID。
+ retainConversationId(state.activeConversationId);
+ } else if (hasThreadContentSurface() && state.lastReading && state.lastReading.conversationId) {
+ retainConversationId(state.lastReading.conversationId);
+ } else {
+ state.activeConversationId = null;
+ state.lastReading = null;
+ state.cachedActiveConversationId = null;
+ state.activeConversationIdLookupAt = Date.now();
+ state.navigationPendingUntil = 0;
+ state.switchRetryUntil = 0;
+ clearRetryUpdate();
+ }
+
+ return state.activeConversationId;
+ }
+
+ function parseStatusContextUsageObject(value, source, conversationId) {
+ if (!value || typeof value !== "object") return null;
+
+ const modelContextWindow = firstFiniteNumber(value.modelContextWindow, value.model_context_window);
+ const lastUsage = value.last || value.lastTokenUsage || value.last_token_usage;
+ const totalTokens = firstFiniteNumber(
+ lastUsage && lastUsage.totalTokens,
+ lastUsage && lastUsage.total_tokens,
+ );
+ if (Number.isFinite(modelContextWindow) && modelContextWindow > 0 && Number.isFinite(totalTokens) && totalTokens >= 0) {
+ const usedTokens = Math.min(totalTokens, modelContextWindow);
+ return withConversationId(makeReading(
+ (usedTokens / modelContextWindow) * 100,
+ source,
+ "status tokenUsage",
+ usedTokens,
+ modelContextWindow,
+ ), conversationId);
+ }
+
+ const percent = Number(value.percent);
+ const usedTokens = firstFiniteNumber(value.usedTokens, value.used_tokens);
+ const contextWindow = firstFiniteNumber(value.contextWindow, value.context_window);
+
+ if (Number.isFinite(usedTokens) && Number.isFinite(contextWindow) && contextWindow > 0) {
+ return withConversationId(makeReading(
+ (usedTokens / contextWindow) * 100,
+ source,
+ "status contextUsage",
+ usedTokens,
+ contextWindow,
+ ), conversationId);
+ }
+
+ if (Number.isFinite(percent) && percent >= 0 && percent <= 100) {
+ return withConversationId(makeReading(percent, source, "status contextUsage.percent"), conversationId);
+ }
+
+ return null;
+ }
+
+ function looksLikeStatusContextUsageObject(value) {
+ if (!value || typeof value !== "object") return false;
+
+ const modelContextWindow = firstFiniteNumber(value.modelContextWindow, value.model_context_window);
+ const lastUsage = value.last || value.lastTokenUsage || value.last_token_usage;
+ const totalTokens = firstFiniteNumber(
+ lastUsage && lastUsage.totalTokens,
+ lastUsage && lastUsage.total_tokens,
+ );
+ if (Number.isFinite(modelContextWindow) && modelContextWindow > 0 && Number.isFinite(totalTokens) && totalTokens >= 0) {
+ return true;
+ }
+
+ const usedTokens = firstFiniteNumber(value.usedTokens, value.used_tokens);
+ const contextWindow = firstFiniteNumber(value.contextWindow, value.context_window);
+ return Number.isFinite(usedTokens) && usedTokens >= 0 && Number.isFinite(contextWindow) && contextWindow > 0;
+ }
+
+ function firstFiniteNumber(...values) {
+ for (const value of values) {
+ const number = Number(value);
+ if (Number.isFinite(number)) return number;
+ }
+
+ return null;
+ }
+
+ // 反向遍历 React / window 状态树时,Map key 或父对象常常比叶子值更像会话归属来源。
+ function findStatusContextUsageObject(value, depth, seen, activeConversationId, ownerConversationId) {
+ if (!value || typeof value !== "object" || depth < 0) return null;
+ if (seen.has(value)) return null;
+ seen.add(value);
+
+ const valueConversationId = getObjectConversationId(value);
+ const nextOwnerConversationId = valueConversationId || ownerConversationId;
+ const ownerMatchesActive =
+ !activeConversationId ||
+ conversationIdsMatch(activeConversationId, nextOwnerConversationId) ||
+ conversationIdsMatch(activeConversationId, ownerConversationId);
+
+ if (ownerMatchesActive) {
+ const direct = parseStatusContextUsageObject(value, "status-react", nextOwnerConversationId);
+ if (direct) return direct;
+ }
+
+ if (Array.isArray(value)) {
+ const limit = Math.min(value.length, 120);
+ for (let index = 0; index < limit; index += 1) {
+ const reading = findStatusContextUsageObject(
+ value[index],
+ depth - 1,
+ seen,
+ activeConversationId,
+ nextOwnerConversationId,
+ );
+ if (reading) return reading;
+ }
+ return null;
+ }
+
+ if (value instanceof Map) {
+ let index = 0;
+ for (const [mapKey, mapValue] of value) {
+ if (index >= 120) break;
+
+ const mapKeyConversationId = normalizeConversationId(mapKey);
+ const childOwnerConversationId = mapKeyConversationId || nextOwnerConversationId;
+
+ const keyReading = findStatusContextUsageObject(
+ mapKey,
+ depth - 1,
+ seen,
+ activeConversationId,
+ childOwnerConversationId,
+ );
+ if (keyReading) return keyReading;
+
+ const valueReading = findStatusContextUsageObject(
+ mapValue,
+ depth - 1,
+ seen,
+ activeConversationId,
+ childOwnerConversationId,
+ );
+ if (valueReading) return valueReading;
+
+ index += 1;
+ }
+ return null;
+ }
+
+ if (value instanceof Set) {
+ let index = 0;
+ for (const setValue of value) {
+ if (index >= 120) break;
+
+ const reading = findStatusContextUsageObject(
+ setValue,
+ depth - 1,
+ seen,
+ activeConversationId,
+ nextOwnerConversationId,
+ );
+ if (reading) return reading;
+
+ index += 1;
+ }
+ return null;
+ }
+
+ const keys = getFilteredReflectKeys(value, "statusTree", STATUS_TREE_KEY_RE, 120);
+ const keySet = new Set(keys);
+
+ for (const key of PREFERRED_STATUS_KEYS) {
+ if (!keySet.has(key)) continue;
+
+ let child;
+ try {
+ child = value[key];
+ } catch {
+ continue;
+ }
+
+ const reading = findStatusContextUsageObject(
+ child,
+ depth - 1,
+ seen,
+ activeConversationId,
+ nextOwnerConversationId,
+ );
+ if (reading) return reading;
+ }
+
+ for (const key of keys) {
+ if (PREFERRED_STATUS_KEY_SET.has(key)) continue;
+
+ let child;
+ try {
+ child = value[key];
+ } catch {
+ continue;
+ }
+
+ const keyConversationId = normalizeConversationId(key);
+ const childOwnerConversationId = keyConversationId || nextOwnerConversationId;
+ const reading = findStatusContextUsageObject(
+ child,
+ depth - 1,
+ seen,
+ activeConversationId,
+ childOwnerConversationId,
+ );
+ if (reading) return reading;
+ }
+
+ return null;
+ }
+
+ // React 私有字段是从界面节点回溯运行态状态的桥;升级后若读不到值,优先检查 __react* 键。
+ function scanStatusReactContextUsage(activeConversationId) {
+ const nodes = [
+ document.getElementById("root"),
+ document.querySelector(`[aria-current="page"]`),
+ document.querySelector(`[data-app-action-sidebar-thread-active="true"]`),
+ document.body,
+ document.documentElement,
+ ].filter(Boolean);
+ const limit = nodes.length;
+
+ for (let index = 0; index < limit; index += 1) {
+ const node = nodes[index];
+ const keys = getReactPrivateKeys(node);
+
+ for (const key of keys) {
+ let value;
+ try {
+ value = node[key];
+ } catch {
+ continue;
+ }
+
+ const reading = findStatusContextUsageObject(value, 10, new WeakSet(), activeConversationId, null);
+ if (reading) return reading;
+ }
+ }
+
+ return null;
+ }
+
+ function isAppSignalScope(value) {
+ return !!(
+ value &&
+ typeof value === "object" &&
+ typeof value.get === "function" &&
+ typeof value.watch === "function" &&
+ value.node &&
+ value.chain
+ );
+ }
+
+ // app signal scope 没有稳定全局入口,只能从 React fiber 链里按结构特征反查。
+ function findAppSignalScopeInValue(value, depth, seen) {
+ if (!value || typeof value !== "object" || depth < 0) return null;
+ if (seen.has(value)) return null;
+ if (state.appSignalSkipGeneration.get(value) === state.scanGeneration) return null;
+ seen.add(value);
+
+ if (isAppSignalScope(value)) return value;
+
+ if (Array.isArray(value)) {
+ const limit = Math.min(value.length, 40);
+ for (let index = 0; index < limit; index += 1) {
+ const scope = findAppSignalScopeInValue(value[index], depth - 1, seen);
+ if (scope) return scope;
+ }
+ return null;
+ }
+
+ if (value instanceof Map) {
+ let index = 0;
+ for (const [mapKey, mapValue] of value) {
+ if (index >= 30) break;
+
+ const keyScope = findAppSignalScopeInValue(mapKey, depth - 1, seen);
+ if (keyScope) return keyScope;
+
+ const valueScope = findAppSignalScopeInValue(mapValue, depth - 1, seen);
+ if (valueScope) return valueScope;
+
+ index += 1;
+ }
+ }
+
+ const keys = getFilteredReflectKeys(value, "appSignalScope", APP_SIGNAL_SCOPE_KEY_RE, 120);
+
+ for (const key of keys) {
+ let child;
+ try {
+ child = value[key];
+ } catch {
+ continue;
+ }
+
+ const scope = findAppSignalScopeInValue(child, depth - 1, seen);
+ if (scope) return scope;
+ }
+
+ state.appSignalSkipGeneration.set(value, state.scanGeneration);
+ return null;
+ }
+
+ function findAppSignalScope() {
+ if (isAppSignalScope(state.appSignalScope)) return state.appSignalScope;
+
+ const now = Date.now();
+ if (now - state.appSignalLastLookupAt < 2000) return null;
+ state.appSignalLastLookupAt = now;
+
+ // app signal scope 挂在 React 树里;只扫稳定锚点,避免初始化时遍历整页 DOM。
+ const root = document.getElementById("root");
+ const current = document.querySelector(`[aria-current="page"]`);
+ const activeThread = document.querySelector(`[data-app-action-sidebar-thread-active="true"]`);
+ const nodes = [
+ root,
+ current,
+ activeThread,
+ current && current.closest("[data-app-action-sidebar-thread-id]"),
+ activeThread && activeThread.closest("[data-app-action-sidebar-thread-id]"),
+ document.body,
+ document.documentElement,
+ ...(root
+ ? Array.from(root.querySelectorAll(REACT_STATE_HOST_SELECTOR)).slice(0, REACT_HOST_SCAN_LIMIT)
+ : []),
+ ].filter(Boolean);
+
+ const seen = new WeakSet();
+ for (const node of nodes) {
+ const keys = getReactPrivateKeys(node);
+ for (const key of keys) {
+ let value;
+ try {
+ value = node[key];
+ } catch {
+ continue;
+ }
+
+ const scope = findAppSignalScopeInValue(value, 18, seen);
+ if (scope) {
+ state.appSignalScope = scope;
+ return scope;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ // hashed asset 文件名会随版本变化;先从已加载资源定位,fallback 只保留当前版本的相对路径。
+ function findLoadedAssetUrl(fragment, fallbackPath) {
+ const selectors = [
+ `script[src*="${fragment}"]`,
+ `link[href*="${fragment}"]`,
+ ];
+
+ for (const selector of selectors) {
+ const element = document.querySelector(selector);
+ const value = element && (element.src || element.href);
+ if (value) return value;
+ }
+
+ const resources =
+ typeof performance !== "undefined" && typeof performance.getEntriesByType === "function"
+ ? performance.getEntriesByType("resource")
+ : [];
+ for (const resource of resources) {
+ if (resource && typeof resource.name === "string" && resource.name.includes(fragment)) {
+ return resource.name;
+ }
+ }
+
+ const links = document.querySelectorAll(`link[rel="modulepreload"][href*="${fragment}"]`);
+ for (const link of links) {
+ const value = link && link.href;
+ if (value) return value;
+ }
+
+ return new URL(fallbackPath, location.href).href;
+ }
+
+ // 优先读取 Status 使用的 app signal;bundle hash 或导出名变化时,先更新这两个资产入口。
+ function ensureAppSignalModules() {
+ if (state.appSignalModules) return state.appSignalModules;
+ if (state.appSignalModulesPromise) return null;
+ state.appSignalModulesRequestedAt = Date.now();
+
+ const appServerUrl = findLoadedAssetUrl(
+ "app-server-manager-signals",
+ "./assets/app-server-manager-signals-7MlBpIlX.js",
+ );
+ const signalUrl = findLoadedAssetUrl(
+ "setting-storage",
+ "./assets/setting-storage-kJblH-wH.js",
+ );
+
+ state.appSignalModulesPromise = Promise.all([
+ import(appServerUrl),
+ import(signalUrl),
+ ])
+ .then(([appServerSignals, signalStorage]) => {
+ state.appSignalModules = { appServerSignals, signalStorage };
+ scheduleUpdate();
+ return state.appSignalModules;
+ })
+ .catch(() => {
+ state.appSignalModulesPromise = null;
+ return null;
+ });
+
+ return null;
+ }
+
+ // setting-storage 的 rt helper 会解开嵌套 signal;缺失时退回 scope.get 的两段读取。
+ function readSignalValue(scope, selector, argument) {
+ if (!scope || !selector) return null;
+
+ const modules = state.appSignalModules;
+ const readHelper = modules && modules.signalStorage && modules.signalStorage.rt;
+ if (typeof readHelper === "function") {
+ try {
+ return readHelper(scope, selector, argument);
+ } catch {
+ return null;
+ }
+ }
+
+ try {
+ const nestedSignal = scope.get(selector, argument);
+ if (nestedSignal && typeof nestedSignal === "object") {
+ return scope.get(nestedSignal);
+ }
+ } catch {
+ return null;
+ }
+
+ return null;
+ }
+
+ function findTokenUsageSelector(scope, conversationId) {
+ if (!scope || !conversationId) return null;
+
+ if (state.appSignalTokenUsageSelector) return state.appSignalTokenUsageSelector;
+
+ const now = Date.now();
+ if (now - state.appSignalTokenUsageSelectorLookupAt < APP_SIGNAL_SELECTOR_SCAN_INTERVAL_MS) {
+ return null;
+ }
+ state.appSignalTokenUsageSelectorLookupAt = now;
+
+ const modules = state.appSignalModules;
+ const appServerSignals = modules && modules.appServerSignals;
+ if (!appServerSignals || typeof appServerSignals !== "object") return null;
+
+ let scanned = 0;
+ for (const [exportName, selector] of Object.entries(appServerSignals)) {
+ if (!selector || (typeof selector !== "object" && typeof selector !== "function")) continue;
+ scanned += 1;
+ if (scanned > APP_SIGNAL_SELECTOR_SCAN_LIMIT) break;
+
+ const value = readSignalValue(scope, selector, conversationId);
+ if (!looksLikeStatusContextUsageObject(value)) continue;
+
+ state.appSignalTokenUsageSelector = selector;
+ state.appSignalTokenUsageSelectorExport = exportName;
+ return selector;
+ }
+
+ return null;
+ }
+
+ function scanAppSignalContextUsage(activeConversationId) {
+ const now = Date.now();
+ const requestedConversationId = normalizeConversationId(activeConversationId);
+ if (
+ state.appSignalCachedReading &&
+ requestedConversationId &&
+ conversationIdsMatch(requestedConversationId, state.appSignalCachedConversationId) &&
+ now - state.appSignalCachedAt < APP_SIGNAL_READING_CACHE_MS
+ ) {
+ return state.appSignalCachedReading;
+ }
+
+ const modules = ensureAppSignalModules();
+ if (!modules || !modules.appServerSignals) {
+ state.waitingForAppSignalModules = !!state.appSignalModulesPromise;
+ return null;
+ }
+ state.waitingForAppSignalModules = false;
+
+ const scope = findAppSignalScope();
+ if (!scope) return null;
+
+ const conversationId =
+ normalizeConversationId(activeConversationId) ||
+ normalizeConversationId(scope.value && scope.value.conversationId);
+ if (!conversationId) return null;
+
+ const latestTokenUsageSelector = findTokenUsageSelector(scope, conversationId);
+ const tokenUsage = readSignalValue(scope, latestTokenUsageSelector, conversationId);
+ const reading = parseStatusContextUsageObject(tokenUsage, "app-signal", conversationId);
+ if (reading) {
+ state.appSignalCachedReading = reading;
+ state.appSignalCachedConversationId = conversationId;
+ state.appSignalCachedAt = now;
+ state.appSignalLastSuccessAt = now;
+ return reading;
+ }
+
+ return null;
+ }
+
+ function collectContextUsageSampleConversationIds(activeConversationId) {
+ const ids = [];
+ const add = (conversationId) => {
+ const normalizedConversationId = normalizeConversationId(conversationId);
+ if (!normalizedConversationId || ids.includes(normalizedConversationId)) return;
+ ids.push(normalizedConversationId);
+ };
+
+ add(activeConversationId);
+ for (const element of document.querySelectorAll("[data-app-action-sidebar-thread-id]")) {
+ add(getElementConversationId(element));
+ if (ids.length >= CONTEXT_USAGE_BACKGROUND_SAMPLE_MAX_CONVERSATIONS) return ids;
+ }
+ for (const conversationId of state.readingsByConversationId.keys()) add(conversationId);
+ for (const conversationId of state.lastAnimatedUsedByConversationId.keys()) add(conversationId);
+
+ return ids.slice(0, CONTEXT_USAGE_BACKGROUND_SAMPLE_MAX_CONVERSATIONS);
+ }
+
+ function rememberContextUsageReading(reading, fallbackConversationId) {
+ const conversationId = normalizeConversationId(reading && (reading.conversationId || fallbackConversationId));
+ if (!conversationId || !reading) return null;
+
+ reading.conversationId = conversationId;
+ state.readingsByConversationId.set(conversationId, reading);
+ return conversationId;
+ }
+
+ function recordContextUsageDelta(reading, fallbackConversationId, options = {}) {
+ const conversationId = rememberContextUsageReading(reading, fallbackConversationId);
+ if (!conversationId || !Number.isFinite(reading.used)) return null;
+
+ const previousUsed = state.lastAnimatedUsedByConversationId.get(conversationId);
+ if (Number.isFinite(previousUsed) && reading.used > previousUsed) {
+ const deltaTokens = reading.used - previousUsed;
+ if (shouldShowContextSpendEffect(conversationId, reading.used)) {
+ recordSpend("context", deltaTokens, conversationId);
+ if (options.showEffect !== false) showTokenSpendEffect(deltaTokens);
+ }
+ }
+ state.lastAnimatedUsedByConversationId.set(conversationId, reading.used);
+ return conversationId;
+ }
+
+ // 已知/侧栏可见会话的 app-signal 读数可按会话 ID 查询;每 5 秒顺手刷新一次,避免只记录当前可见会话。
+ function sampleKnownConversationContextUsage(activeConversationId) {
+ const now = Date.now();
+ if (now - state.contextUsageBackgroundSampleAt < CONTEXT_USAGE_BACKGROUND_SAMPLE_INTERVAL_MS) return;
+
+ const conversationIds = collectContextUsageSampleConversationIds(activeConversationId);
+ state.contextUsageBackgroundSampleAt = now;
+ state.contextUsageBackgroundSampleConversationIds = conversationIds;
+ if (!conversationIds.length) return;
+
+ for (const conversationId of conversationIds) {
+ if (conversationIdsMatch(conversationId, activeConversationId)) continue;
+
+ const reading = scanAppSignalContextUsage(conversationId);
+ if (!reading) continue;
+ recordContextUsageDelta(reading, conversationId, { showEffect: false });
+ }
+ }
+
+ function shouldWaitForAppSignalModules(now) {
+ return !!(
+ state.appSignalModulesPromise &&
+ !state.appSignalModules &&
+ now - state.appSignalModulesRequestedAt < APP_SIGNAL_IMPORT_GRACE_MS
+ );
+ }
+
+ function isVisibleElement(element) {
+ if (!element || element.nodeType !== Node.ELEMENT_NODE) return false;
+
+ const rect = element.getBoundingClientRect();
+ if (rect.width <= 0 || rect.height <= 0) return false;
+
+ const style = getComputedStyle(element);
+ return style.display !== "none" && style.visibility !== "hidden" && Number(style.opacity || "1") > 0;
+ }
+
+ function isConversationContent(element) {
+ return !!(element && element.closest(CONVERSATION_CONTENT_SELECTOR));
+ }
+
+ function shouldIgnoreMutationTarget(element) {
+ if (!element || element.nodeType !== Node.ELEMENT_NODE) return false;
+ if (element.closest(`#${ROOT_ID}`)) return true;
+ if (element.closest(`#${HISTORY_PORTAL_ID}`)) return true;
+ return !!element.closest(MESSAGE_MUTATION_SELECTOR);
+ }
+
+ // 这里直接找结构化 usage 对象,比把 window 状态拼成文本再正则解析更便宜。
+ function scanWindowForContextUsage(activeConversationId) {
+ const seen = new WeakSet();
+ const now = Date.now();
+ if (!state.windowUsageKeys || now - state.windowUsageKeysAt > WINDOW_KEY_CACHE_MS) {
+ state.windowUsageKeys = Object.keys(window).filter((key) =>
+ /codex|thread|token|usage|context|store|query|cache|notification|message/i.test(key),
+ );
+ state.windowUsageKeysAt = now;
+ }
+
+ for (const key of state.windowUsageKeys) {
+ let value;
+ try {
+ value = window[key];
+ } catch {
+ continue;
+ }
+
+ const reading = findStatusContextUsageObject(value, 6, seen, activeConversationId, null);
+ if (reading) return reading;
+ }
+
+ return null;
+ }
+
+ // 读取顺序按稳定性排列:app signal > 结构化 React 状态 > window 缓存。
+ function detectReading() {
+ state.scanGeneration += 1;
+ const activeConversationId = updateActiveConversationId();
+
+ if (!activeConversationId) {
+ const appSignalReading = scanAppSignalContextUsage(null);
+ if (appSignalReading && appSignalReading.conversationId) {
+ retainConversationId(appSignalReading.conversationId);
+ state.switchRetryUntil = 0;
+ clearRetryUpdate();
+ return appSignalReading;
+ }
+
+ return null;
+ }
+
+ const cachedReading = state.readingsByConversationId.get(activeConversationId) || null;
+ const fallbackReading =
+ state.lastReading && state.lastReading.conversationId && conversationIdsMatch(activeConversationId, state.lastReading.conversationId)
+ ? state.lastReading
+ : cachedReading;
+
+ const now = Date.now();
+ const activeChangedSinceScan = activeConversationId !== state.lastScannedConversationId;
+ const inSwitchRetryWindow = !!activeConversationId && now < state.switchRetryUntil;
+
+ const shouldRunStatusScan =
+ activeChangedSinceScan ||
+ inSwitchRetryWindow ||
+ now - state.lastScanAt >= SLOW_SCAN_INTERVAL_MS;
+ const appSignalReading = scanAppSignalContextUsage(activeConversationId);
+ if (appSignalReading && !shouldRunStatusScan) {
+ state.switchRetryUntil = 0;
+ clearRetryUpdate();
+ return appSignalReading;
+ }
+
+ if (!shouldRunStatusScan) {
+ if (shouldWaitForAppSignalModules(now)) {
+ scheduleUpdate(APP_SIGNAL_IMPORT_GRACE_MS);
+ return state.lastReading;
+ }
+ return fallbackReading;
+ }
+
+ // 会话切换初期允许快速兜底;同一会话内的昂贵扫描按窗口限频。
+ const canRunExpensiveFallback =
+ activeChangedSinceScan ||
+ !conversationIdsMatch(activeConversationId, state.expensiveFallbackConversationId) ||
+ now - state.expensiveFallbackScannedAt >= EXPENSIVE_FALLBACK_INTERVAL_MS;
+
+ state.lastScannedConversationId = activeConversationId;
+ state.lastScanAt = now;
+
+ const statusReactReading = scanStatusReactContextUsage(activeConversationId);
+ if (statusReactReading) {
+ state.switchRetryUntil = 0;
+ clearRetryUpdate();
+ return statusReactReading;
+ }
+
+ if (canRunExpensiveFallback) {
+ state.expensiveFallbackScannedAt = now;
+ state.expensiveFallbackConversationId = activeConversationId;
+
+ // fallback 只保留结构化对象,避免按页面文案猜测。
+ const windowReading = scanWindowForContextUsage(activeConversationId);
+ if (windowReading) {
+ state.switchRetryUntil = 0;
+ clearRetryUpdate();
+ return windowReading;
+ }
+ }
+
+ if (appSignalReading) {
+ state.switchRetryUntil = 0;
+ clearRetryUpdate();
+ return appSignalReading;
+ }
+
+ if (shouldWaitForAppSignalModules(now)) {
+ scheduleUpdate(APP_SIGNAL_IMPORT_GRACE_MS);
+ return state.lastReading;
+ }
+
+ if (inSwitchRetryWindow) {
+ scheduleRetryUpdate();
+ } else {
+ clearRetryUpdate();
+ }
+
+ if (fallbackReading) return fallbackReading;
+
+ return null;
+ }
+
+ function rememberReading(reading, fallbackConversationId) {
+ if (!reading) return;
+
+ rememberContextUsageReading(reading, fallbackConversationId);
+ state.lastReading = reading;
+ }
+
+ function updateMeter() {
+ installStyle();
+
+ if (!document.body) return;
+
+ const root = ensureRoot();
+ state.uiConfig = readUiConfig();
+ const contextCard = state.contextCard;
+ const value = state.value;
+ const fill = state.fill;
+ const compressionZone = state.compressionZone;
+ const reading = detectReading();
+ const activeConversationId = state.activeConversationId || readActiveConversationId();
+
+ if (!contextCard || !value || !fill) return;
+
+ if (!hasThreadContentSurface()) {
+ state.lastReading = null;
+ hideProviderMeter(root, "No thread content is open in the main view.");
+ hideMeter(root, contextCard, value, fill, "No thread content is open in the main view.");
+ return;
+ }
+ renderProviderMeter(root);
+
+ if (!reading) {
+ if (state.waitingForAppSignalModules) {
+ scheduleUpdate(APP_SIGNAL_IMPORT_GRACE_MS);
+ if (contextCard.dataset.known === "true" && value.textContent !== "Context Left --") return;
+ hideMeter(root, contextCard, value, fill, "Waiting for Codex context usage signal.");
+ return;
+ }
+
+ const title = activeConversationId
+ ? `No context usage value is exposed for conversation ${activeConversationId} in the current page state yet.`
+ : "No context usage value is exposed in the current page state yet.";
+ hideMeter(root, contextCard, value, fill, title);
+ return;
+ }
+
+ rememberReading(reading, activeConversationId);
+
+ const leftPercent = clampPercent(100 - reading.percent);
+ const showUsedInsteadOfLeft = shouldShowUsedInsteadOfLeft(state.uiConfig);
+ const displayPercent = showUsedInsteadOfLeft ? clampPercent(reading.percent) : leftPercent;
+ const percentText = displayPercent.toFixed(1);
+ const details =
+ reading.used != null && reading.limit != null
+ ? ` ${compactNumber(reading.used)} / ${compactNumber(reading.limit)}`
+ : "";
+ const readingConversationId = normalizeConversationId(
+ reading.conversationId || activeConversationId || "__unknown__"
+ );
+
+ recordContextUsageDelta(reading, readingConversationId, { showEffect: true });
+ sampleKnownConversationContextUsage(readingConversationId);
+
+ const level = levelForLeftPercent(leftPercent, "context");
+ const compressionWarning = shouldShowCompressionWarning(leftPercent) ? "true" : "false";
+ const remainingTokens = reading.used != null && reading.limit != null ? Math.max(0, reading.limit - reading.used) : null;
+ const title = formatContextTitle(reading, reading.used, remainingTokens, leftPercent, reading.percent);
+ const text = showUsedInsteadOfLeft
+ ? `Context Used ${percentText}%${details}`
+ : Number.isFinite(remainingTokens)
+ ? `Context Left ${percentText}% (${compactNumber(remainingTokens)} left)`
+ : `Context Left ${percentText}%${details}`;
+ const width = `${displayPercent.toFixed(1)}%`;
+ const compressionZoneWidth = `${state.uiConfig.context.compressionWarningLeftPercent.toFixed(1)}%`;
+
+ if (contextCard.dataset.known !== "true") contextCard.dataset.known = "true";
+ if (contextCard.hidden) contextCard.hidden = false;
+ if (contextCard.dataset.level !== level) contextCard.dataset.level = level;
+ const showUsedInsteadOfLeftValue = showUsedInsteadOfLeft ? "true" : "false";
+ if (contextCard.dataset.showUsedInsteadOfLeft !== showUsedInsteadOfLeftValue) {
+ contextCard.dataset.showUsedInsteadOfLeft = showUsedInsteadOfLeftValue;
+ }
+ if (contextCard.dataset.compressionWarning !== compressionWarning) {
+ contextCard.dataset.compressionWarning = compressionWarning;
+ }
+ if (contextCard.title !== title) contextCard.title = title;
+ if (value.textContent !== text) value.textContent = text;
+ if (fill.style.width !== width) fill.style.width = width;
+ const contextRing = state.contextRing;
+ if (contextRing) contextRing.style.setProperty("--ccm-ring-angle", `${displayPercent * 3.6}deg`);
+ if (compressionZone && compressionZone.style.width !== compressionZoneWidth) {
+ compressionZone.style.width = compressionZoneWidth;
+ }
+ updateDockVisibility(root);
+ }
+
+ function scheduleUpdate(delayMs = MUTATION_UPDATE_DELAY_MS) {
+ const delay = Math.max(0, Number(delayMs) || 0);
+ const dueAt = Date.now() + delay;
+ if (state.pendingUpdate && state.pendingUpdateDueAt <= dueAt) return;
+
+ window.clearTimeout(state.pendingUpdate);
+ state.pendingUpdateDueAt = dueAt;
+ state.pendingUpdate = window.setTimeout(() => {
+ state.pendingUpdate = 0;
+ state.pendingUpdateDueAt = 0;
+ updateMeter();
+ }, delay);
+ }
+
+ // 提前捕获侧栏会话切换,避免 DOM/状态更新滞后时短暂沿用旧会话读数。
+ function handlePotentialNavigation(event) {
+ const target = event.target && event.target.closest
+ ? event.target.closest("[data-app-action-sidebar-thread-id]")
+ : null;
+ const conversationId = getElementConversationId(target);
+ if (!conversationId) return;
+
+ activateConversationId(conversationId, { pendingNavigation: true });
+ scheduleUpdate(NAVIGATION_UPDATE_DELAY_MS);
+ }
+
+ function clearRetryUpdate() {
+ window.clearTimeout(state.retryTimer);
+ state.retryTimer = 0;
+ }
+
+ function scheduleRetryUpdate() {
+ if (state.retryTimer || Date.now() >= state.switchRetryUntil) return;
+
+ state.retryTimer = window.setTimeout(() => {
+ state.retryTimer = 0;
+ updateMeter();
+ }, SWITCH_RETRY_INTERVAL_MS);
+ }
+
+ function restoreLegacyCaptureHooks() {
+ const captureState = window.__codexContextMeterCaptureState;
+ if (!captureState || typeof captureState !== "object") return;
+
+ if (captureState.nativeFetch && window.fetch !== captureState.nativeFetch) {
+ window.fetch = captureState.nativeFetch;
+ }
+ if (captureState.NativeWebSocket && window.WebSocket !== captureState.NativeWebSocket) {
+ window.WebSocket = captureState.NativeWebSocket;
+ }
+ if (captureState.messageListener) {
+ window.removeEventListener("message", captureState.messageListener, true);
+ }
+
+ delete window.__codexContextMeterCaptureState;
+ delete window.__codexContextMeterFetchPatched;
+ delete window.__codexContextMeterWebSocketPatched;
+ delete window.__codexContextMeterPostMessagePatched;
+ }
+
+ function installObserver() {
+ if (state.observer) return;
+
+ state.navigationListener = handlePotentialNavigation;
+ document.addEventListener("pointerdown", state.navigationListener, true);
+ document.addEventListener("click", state.navigationListener, true);
+ document.addEventListener("keydown", state.navigationListener, true);
+
+ state.observer = new MutationObserver((mutations) => {
+ for (const mutation of mutations) {
+ const target = mutation.target && mutation.target.nodeType === Node.ELEMENT_NODE
+ ? mutation.target
+ : mutation.target && mutation.target.parentElement;
+ if (shouldIgnoreMutationTarget(target)) continue;
+
+ invalidateThreadContentCache();
+ scheduleUpdate(MUTATION_UPDATE_DELAY_MS);
+ return;
+ }
+ });
+ state.observer.observe(document.documentElement, {
+ attributes: true,
+ attributeFilter: [
+ // 会话切换只依赖侧栏 active/current/id 相关属性;Codex 改名时同步这组属性。
+ "aria-current",
+ "aria-selected",
+ "data-app-action-sidebar-thread-active",
+ "data-app-action-sidebar-thread-id",
+ ],
+ childList: true,
+ subtree: true,
+ });
+ }
+
+ window[API_KEY] = {
+ version: SCRIPT_VERSION,
+ refresh: updateMeter,
+ setProviderSummary,
+ destroy() {
+ window.clearInterval(state.timer);
+ window.clearTimeout(state.pendingUpdate);
+ window.clearTimeout(state.historyCloseTimer);
+ state.pendingUpdateDueAt = 0;
+ state.historyCloseTimer = 0;
+ if (state.historyHoverCleanup) {
+ state.historyHoverCleanup();
+ state.historyHoverCleanup = null;
+ }
+ if (state.floatingPointerCleanup) {
+ state.floatingPointerCleanup();
+ state.floatingPointerCleanup = null;
+ }
+ closeContextMenu();
+ clearSpendEffects();
+ clearRetryUpdate();
+ if (state.observer) state.observer.disconnect();
+ if (state.navigationListener) {
+ document.removeEventListener("pointerdown", state.navigationListener, true);
+ document.removeEventListener("click", state.navigationListener, true);
+ document.removeEventListener("keydown", state.navigationListener, true);
+ }
+ if (state.providerSummaryListener) {
+ window.removeEventListener(PROVIDER_SUMMARY_EVENT, state.providerSummaryListener);
+ state.providerSummaryListener = null;
+ }
+
+ const root = document.getElementById(ROOT_ID);
+ if (root) root.remove();
+
+ const portal = document.getElementById(HISTORY_PORTAL_ID);
+ if (portal) portal.remove();
+
+ const style = document.getElementById(STYLE_ID);
+ if (style) style.remove();
+
+ delete window[INSTALL_KEY];
+ delete window[API_KEY];
+ },
+ getState() {
+ return {
+ activeConversationId: state.activeConversationId,
+ lastReading: state.lastReading,
+ cachedConversationIds: Array.from(state.readingsByConversationId.keys()),
+ animatedConversationIds: Array.from(state.lastAnimatedUsedByConversationId.keys()),
+ backgroundSampledConversationIds: state.contextUsageBackgroundSampleConversationIds.slice(),
+ backgroundSampledAt: state.contextUsageBackgroundSampleAt,
+ hasAppSignalScope: isAppSignalScope(state.appSignalScope),
+ hasAppSignalModules: !!state.appSignalModules,
+ appSignalTokenUsageSelectorExport: state.appSignalTokenUsageSelectorExport,
+ providerSummary: state.providerSummary,
+ };
+ },
+ };
+
+ restoreLegacyCaptureHooks();
+ installStyle();
+ installProviderSummaryListener();
+ updateMeter();
+ installObserver();
+ state.timer = window.setInterval(updateMeter, UPDATE_INTERVAL_MS);
+})();
diff --git a/Resources/script-market-sources/codex-token-usage.js b/Resources/script-market-sources/codex-token-usage.js
new file mode 100644
index 0000000..3ca17dd
--- /dev/null
+++ b/Resources/script-market-sources/codex-token-usage.js
@@ -0,0 +1,1815 @@
+(() => {
+ "use strict";
+
+ const SCRIPT_ID = "codex-token-usage";
+ const SCRIPT_VERSION = "0.1.7";
+ const BADGE_CLASS = "codex-token-usage-badge";
+ const STYLE_ID = "codex-token-usage-style";
+ const RECENT_LIMIT = 20;
+ const DEBUG_LIMIT = 50;
+ const LEDGER_LIMIT = 500;
+ const CONTEXT_POLL_INTERVAL_MS = 1000;
+ const TURN_IDLE_TIMEOUT_MS = 120000;
+ const CONTEXT_MERGE_WINDOW_MS = 30000;
+ const CROSS_SOURCE_DEDUPE_WINDOW_MS = 3000;
+ const STORAGE_KEY = "__codexTokenUsageRecentDetails";
+
+ if (window.__codexTokenUsageScriptInstalled && window.__codexTokenUsageVersion === SCRIPT_VERSION) return;
+ window.__codexTokenUsageScriptInstalled = true;
+ window.__codexTokenUsageVersion = SCRIPT_VERSION;
+
+ const state = {
+ lastMetric: null,
+ lastMetricKey: "",
+ recent: [],
+ ledger: [],
+ byConversation: Object.create(null),
+ byScope: Object.create(null),
+ turnsByScope: Object.create(null),
+ activeProjectId: "",
+ activeConversationId: "",
+ currentTurn: null,
+ eventSeq: 0,
+ turnSeq: 0,
+ turnStartedAt: 0,
+ contextPollTimer: 0,
+ pendingTurnStartAt: 0,
+ historyRestoreState: Object.create(null),
+ debug: [],
+ };
+
+ window.__codexTokenUsageDebug = state.debug;
+ window.__codexTokenUsage = {
+ version: SCRIPT_VERSION,
+ last: null,
+ currentTurn: null,
+ recent: [],
+ debug: state.debug,
+ export: () => ({
+ version: SCRIPT_VERSION,
+ activeProjectId: currentProjectId(),
+ activeConversationId: currentConversationId(),
+ activeScopeKey: currentScopeKey(),
+ last: null,
+ currentTurn: null,
+ calls: [],
+ ledgerEvents: [],
+ recent: [],
+ debug: state.debug.slice(),
+ storedDetails: readStoredDetails(),
+ turns: [],
+ }),
+ };
+
+ function normalizeNumber(value) {
+ const number = Number(value);
+ return Number.isFinite(number) && number >= 0 ? Math.round(number) : 0;
+ }
+
+ function stableEventId(value) {
+ const candidate = value?.id ?? value?.response_id ?? value?.responseId ?? value?.request_id ?? value?.requestId;
+ const eventId = String(candidate ?? "").trim();
+ return eventId && eventId.length <= 240 ? eventId : "";
+ }
+
+ function withEventId(usage, eventId) {
+ return usage && eventId ? { ...usage, eventId } : usage;
+ }
+
+ function normalizeUsage(raw) {
+ if (!raw || typeof raw !== "object") return null;
+ const inputTokens = normalizeNumber(raw.input_tokens ?? raw.inputTokens ?? raw.prompt_tokens ?? raw.promptTokens);
+ const outputTokens = normalizeNumber(
+ raw.output_tokens ?? raw.outputTokens ?? raw.completion_tokens ?? raw.completionTokens,
+ );
+ const explicitTotal = raw.total_tokens ?? raw.totalTokens ?? raw.usedTokens ?? raw.used_tokens ?? raw.used;
+ const totalEstimated = explicitTotal == null && !!(inputTokens || outputTokens);
+ const totalTokens = normalizeNumber(explicitTotal ?? inputTokens + outputTokens);
+ const cachedTokens = normalizeNumber(
+ raw.cached_tokens ??
+ raw.cachedTokens ??
+ raw.cached_input_tokens ??
+ raw.cachedInputTokens ??
+ raw.prompt_tokens_details?.cached_tokens ??
+ raw.promptTokensDetails?.cachedTokens ??
+ raw.input_tokens_details?.cached_tokens ??
+ raw.inputTokensDetails?.cachedTokens,
+ );
+ const cacheReadTokens = normalizeNumber(raw.cache_read_input_tokens ?? raw.cacheReadInputTokens);
+ const cacheCreationTokens = normalizeNumber(raw.cache_creation_input_tokens ?? raw.cacheCreationInputTokens);
+ const cachedReadTokens = cacheReadTokens || cachedTokens;
+ const explicitInputTotal = normalizeNumber(
+ raw.input_total_tokens ?? raw.inputTotalTokens ?? raw.prompt_total_tokens ?? raw.promptTotalTokens,
+ );
+ const contextUsed = normalizeNumber(raw.contextUsed ?? raw.context_used ?? raw.usedTokens ?? raw.used_tokens ?? raw.used);
+ const contextLimit = normalizeNumber(
+ raw.contextLimit ?? raw.context_limit ?? raw.modelContextWindow ?? raw.model_context_window ?? raw.contextWindow ?? raw.context_window ?? raw.limit,
+ );
+ if (
+ !inputTokens &&
+ !outputTokens &&
+ !totalTokens &&
+ !cachedTokens &&
+ !cacheReadTokens &&
+ !cacheCreationTokens &&
+ !contextLimit
+ ) {
+ return null;
+ }
+ const inputFromTotal = totalTokens && outputTokens && totalTokens > outputTokens ? totalTokens - outputTokens : 0;
+ let inputTotalTokens = Math.max(explicitInputTotal, inputTokens, inputFromTotal);
+ if (cachedReadTokens > inputTotalTokens) {
+ inputTotalTokens += cachedReadTokens + cacheCreationTokens;
+ }
+ return {
+ inputTokens,
+ inputTotalTokens,
+ outputTokens,
+ outputTotalTokens: outputTokens,
+ totalTokens,
+ requestTotalTokens: totalTokens,
+ cachedTokens,
+ cachedReadTokens,
+ cacheReadTokens,
+ cacheCreationTokens,
+ totalEstimated,
+ hasBreakdown: !!(inputTokens || outputTokens || cachedTokens || cacheReadTokens || cacheCreationTokens),
+ contextUsed: contextUsed || totalTokens,
+ contextLimit,
+ };
+ }
+
+ function findUsageInObject(value, depth = 0) {
+ if (!value || depth > 8) return null;
+ if (Array.isArray(value)) {
+ for (const item of value) {
+ const usage = findUsageInObject(item, depth + 1);
+ if (usage) return usage;
+ }
+ return null;
+ }
+ if (typeof value !== "object") return null;
+
+ const tokenStatus = value.last || value.lastUsage || value.lastTokenUsage || value.last_token_usage;
+ if (tokenStatus && (value.modelContextWindow || value.model_context_window || value.contextWindow || value.context_window)) {
+ const statusUsage = normalizeUsage({
+ ...tokenStatus,
+ modelContextWindow: value.modelContextWindow ?? value.model_context_window,
+ contextWindow: value.contextWindow ?? value.context_window,
+ });
+ if (statusUsage) return statusUsage;
+ }
+
+ for (const key of ["usage", "last", "lastUsage", "lastTokenUsage", "last_token_usage"]) {
+ const direct = normalizeUsage(value[key]);
+ if (direct) return direct;
+ }
+
+ const self = normalizeUsage(value);
+ if (self) return self;
+
+ for (const key of [
+ "response",
+ "data",
+ "body",
+ "message",
+ "result",
+ "event",
+ "params",
+ "tokenUsage",
+ "token_usage",
+ "contextUsage",
+ "context_usage",
+ "info",
+ ]) {
+ const usage = findUsageInObject(value[key], depth + 1);
+ if (usage) return usage;
+ }
+ return null;
+ }
+
+ function collectUsagesInObject(value, depth = 0, usages = [], seen = new WeakSet(), inheritedEventId = "") {
+ if (!value || depth > 8) return usages;
+ if (Array.isArray(value)) {
+ value.forEach((item) => collectUsagesInObject(item, depth + 1, usages, seen, inheritedEventId));
+ return usages;
+ }
+ if (typeof value !== "object") return usages;
+ if (seen.has(value)) return usages;
+ seen.add(value);
+ const eventId = stableEventId(value) || inheritedEventId;
+
+ const tokenStatus = value.last || value.lastUsage || value.lastTokenUsage || value.last_token_usage;
+ if (tokenStatus && (value.modelContextWindow || value.model_context_window || value.contextWindow || value.context_window)) {
+ const statusUsage = withEventId(normalizeUsage({
+ ...tokenStatus,
+ modelContextWindow: value.modelContextWindow ?? value.model_context_window,
+ contextWindow: value.contextWindow ?? value.context_window,
+ }), eventId);
+ if (statusUsage) {
+ usages.push(statusUsage);
+ return usages;
+ }
+ }
+
+ const directKeys = ["usage", "last", "lastUsage", "lastTokenUsage", "last_token_usage"];
+ const consumedKeys = new Set();
+ for (const key of directKeys) {
+ const direct = withEventId(normalizeUsage(value[key]), eventId);
+ if (direct) {
+ usages.push(direct);
+ consumedKeys.add(key);
+ }
+ }
+
+ const self = withEventId(normalizeUsage(value), eventId);
+ if (self) {
+ usages.push(self);
+ return usages;
+ }
+
+ for (const key of [
+ "response",
+ "data",
+ "body",
+ "message",
+ "result",
+ "event",
+ "params",
+ "tokenUsage",
+ "token_usage",
+ "contextUsage",
+ "context_usage",
+ "info",
+ ]) {
+ if (consumedKeys.has(key)) continue;
+ collectUsagesInObject(value[key], depth + 1, usages, seen, eventId);
+ }
+ return usages;
+ }
+
+ function extractJsonFragmentsFromSse(text) {
+ return String(text || "")
+ .split(/\r?\n/)
+ .map((line) => line.trim())
+ .filter((line) => line.startsWith("data:"))
+ .map((line) => line.slice(5).trim())
+ .filter((line) => line && line !== "[DONE]");
+ }
+
+ function extractUsages(payload) {
+ if (typeof payload === "string") {
+ try {
+ const parsed = JSON.parse(payload);
+ const usages = collectUsagesInObject(parsed);
+ if (usages.length) return usages;
+ } catch (_) {
+ // Treat non-JSON text as a possible SSE stream below.
+ }
+ const usages = [];
+ for (const fragment of extractJsonFragmentsFromSse(payload)) {
+ try {
+ collectUsagesInObject(JSON.parse(fragment), 0, usages);
+ } catch (_) {
+ // Ignore malformed stream fragments.
+ }
+ }
+ return usages;
+ }
+ return collectUsagesInObject(payload);
+ }
+
+ function extractUsage(payload) {
+ return extractUsages(payload)[0] || null;
+ }
+
+ function formatNumber(value) {
+ return normalizeNumber(value).toLocaleString("en-US");
+ }
+
+ function formatSeconds(elapsedMs) {
+ const seconds = Math.max(0, normalizeNumber(elapsedMs)) / 1000;
+ if (seconds >= 3600) return `${(seconds / 3600).toFixed(1)}h`;
+ if (seconds >= 60) return `${(seconds / 60).toFixed(1)}min`;
+ return `${seconds.toFixed(1)}s`;
+ }
+
+ function usageHasBreakdown(usage) {
+ return !!(
+ usage &&
+ (usage.hasBreakdown ||
+ usage.inputTokens ||
+ usage.outputTokens ||
+ usage.cachedTokens ||
+ usage.cacheReadTokens ||
+ usage.cacheCreationTokens)
+ );
+ }
+
+ function formatCacheDetails(usage) {
+ const cacheTokens = usage.cachedReadTokens || usage.cachedTokens || usage.cacheReadTokens || 0;
+ if (!cacheTokens) return [];
+ const details = [`缓存读 ${formatNumber(cacheTokens)}`];
+ const inputTokens = usage.inputTotalTokens || usage.inputTokens || 0;
+ if (inputTokens) {
+ const ratio = Math.min(100, Math.max(0, (cacheTokens / inputTokens) * 100));
+ details.push(`缓存命中率 ${ratio.toFixed(1)}%`);
+ }
+ if (usage.cacheCreationTokens) details.push(`缓存写 ${formatNumber(usage.cacheCreationTokens)}`);
+ return details;
+ }
+
+ function formatBadgeText(metric) {
+ if (metric?.status === "running") return "运行中 · 正在统计本次回复 token...";
+ const usage = metric?.usage || {};
+ const requestTotal = usage.requestTotalTokens || usage.totalTokens || 0;
+ const estimatedLabel = usage.totalEstimated ? "(估算)" : "";
+ const parts = [`本轮调用合计 ${formatNumber(requestTotal)}${estimatedLabel}`];
+ if (usageHasBreakdown(usage)) {
+ parts.push(
+ `输入 ${formatNumber(usage.inputTotalTokens || usage.inputTokens)}`,
+ `输出 ${formatNumber(usage.outputTotalTokens || usage.outputTokens)}`,
+ ...formatCacheDetails(usage),
+ );
+ } else {
+ parts.push("输入 -", "输出 -");
+ }
+ if (usage.contextLimit) {
+ const contextUsed = usage.contextUsed || usage.totalTokens;
+ const contextPercent = usage.contextLimit ? ` (${((contextUsed / usage.contextLimit) * 100).toFixed(1)}%)` : "";
+ parts.push(`上下文 ${formatNumber(contextUsed)}/${formatNumber(usage.contextLimit)}${contextPercent}`);
+ }
+ if (metric?.callCount >= 1) parts.push(`调用 ${formatNumber(metric.callCount)} 次`);
+ parts.push(`耗时 ${Number.isFinite(metric?.elapsedMs) && metric.elapsedMs > 0 ? formatSeconds(metric.elapsedMs) : "-"}`);
+ return parts.join(" · ");
+ }
+
+ function parseElapsedMs(text) {
+ const value = String(text || "");
+ const patterns = [
+ /(?:已处理|处理耗时|耗时|Processed)\s*(?:(\d+(?:\.\d+)?)\s*(?:m|min|分钟|分))?\s*(?:(\d+(?:\.\d+)?)\s*(?:s|sec|秒))?/gi,
+ /(?:已处理|处理耗时|耗时|Processed)\s*(\d+(?:\.\d+)?)\s*(?:s|sec|秒)?/gi,
+ ];
+ let best = 0;
+ for (const pattern of patterns) {
+ let match = pattern.exec(value);
+ while (match) {
+ const first = Number(match[1] || 0);
+ const second = Number(match[2] || 0);
+ const seconds = match.length > 2 ? first * 60 + second : first;
+ if (Number.isFinite(seconds) && seconds > best) best = seconds;
+ match = pattern.exec(value);
+ }
+ }
+ return best ? Math.round(best * 1000) : 0;
+ }
+
+ function nowMs() {
+ return window.performance?.now ? window.performance.now() : Date.now();
+ }
+
+ function isCodexApiUrl(url) {
+ const text = String(url || "");
+ return /\/(responses|chat\/completions|conversation|thread|api)\b/i.test(text) || /codex/i.test(text);
+ }
+
+ function requestUrl(input) {
+ if (typeof input === "string") return input;
+ if (input?.url) return input.url;
+ return String(input || "");
+ }
+
+ function normalizeConversationId(value) {
+ const text = String(value || "").trim();
+ if (!text || text === "__proto__" || text === "prototype" || text === "constructor") return "";
+ return /^[A-Za-z0-9_.:-]{3,180}$/.test(text) ? text : "";
+ }
+
+ function normalizeProjectId(value) {
+ return normalizeConversationId(value);
+ }
+
+ function parseObservedAt(value) {
+ if (typeof value === "number" && Number.isFinite(value)) return value;
+ const time = Date.parse(String(value || ""));
+ return Number.isFinite(time) ? time : nowMs();
+ }
+
+ function projectIdFromLocation() {
+ const locationText = `${window.location?.pathname || ""}${window.location?.search || ""}${window.location?.hash || ""}`;
+ const match = locationText.match(/(?:project|workspace)(?:\/|=|:|-)([A-Za-z0-9_.:-]+)/i);
+ return normalizeProjectId(match?.[1]);
+ }
+
+ function projectIdFromActiveRow() {
+ try {
+ const row = document.querySelector?.(
+ "[data-app-action-sidebar-project-active='true'],[data-project-id],[data-workspace-id]",
+ );
+ const id = row?.getAttribute?.("data-project-id")
+ || row?.getAttribute?.("data-workspace-id")
+ || row?.getAttribute?.("data-testid");
+ return normalizeProjectId(id);
+ } catch (_) {
+ return "";
+ }
+ }
+
+ function conversationIdFromLocation() {
+ const locationText = `${window.location?.pathname || ""}${window.location?.search || ""}${window.location?.hash || ""}`;
+ const match = locationText.match(/(?:session|conversation|thread)(?:\/|=|:|-)([A-Za-z0-9_.:-]+)/i)
+ || locationText.match(/\/([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})(?:[/?#]|$)/)
+ || locationText.match(/\/([A-Za-z0-9_-]{12,})(?:[/?#]|$)/);
+ return normalizeConversationId(match?.[1]);
+ }
+
+ function conversationIdFromActiveRow() {
+ try {
+ const row = document.querySelector?.(
+ "[data-app-action-sidebar-thread-active='true'],[aria-current='page'],[aria-current='true']",
+ );
+ const id = row?.getAttribute?.("data-app-action-sidebar-thread-id")
+ || row?.getAttribute?.("data-session-id")
+ || row?.getAttribute?.("data-testid");
+ return normalizeConversationId(id);
+ } catch (_) {
+ return "";
+ }
+ }
+
+ function currentConversationId() {
+ const live = conversationIdFromActiveRow() || conversationIdFromLocation();
+ return live || state.activeConversationId;
+ }
+
+ function currentProjectId() {
+ const live = projectIdFromActiveRow() || projectIdFromLocation();
+ return live || state.activeProjectId;
+ }
+
+ function scopeKeyFor(projectId, conversationId) {
+ const conversation = normalizeConversationId(conversationId);
+ if (!conversation) return "";
+ const project = normalizeProjectId(projectId);
+ return project ? `${project}:${conversation}` : conversation;
+ }
+
+ function currentScopeKey() {
+ return scopeKeyFor(currentProjectId(), currentConversationId());
+ }
+
+ function isSameOrMissingIdentity(currentValue, nextValue) {
+ return !currentValue || !nextValue || currentValue === nextValue;
+ }
+
+ function canAdoptScopeIdentity(turn, projectId, conversationId) {
+ if (!turn) return false;
+ return (
+ isSameOrMissingIdentity(turn.projectId, normalizeProjectId(projectId)) &&
+ isSameOrMissingIdentity(turn.conversationId, normalizeConversationId(conversationId))
+ );
+ }
+
+ function applyTurnScopeIdentity(turn, projectId, conversationId) {
+ if (!turn) return turn;
+ const nextProjectId = normalizeProjectId(projectId) || turn.projectId || "";
+ const nextConversationId = normalizeConversationId(conversationId) || turn.conversationId || "";
+ turn.projectId = nextProjectId;
+ turn.conversationId = nextConversationId;
+ turn.scopeKey = scopeKeyFor(nextProjectId, nextConversationId);
+ return turn;
+ }
+
+ function scopedMetric(metric) {
+ const projectId = normalizeProjectId(metric?.projectId) || currentProjectId();
+ const conversationId = normalizeConversationId(metric?.conversationId) || currentConversationId();
+ const scopeKey = scopeKeyFor(projectId, conversationId);
+ return conversationId ? { ...metric, projectId, conversationId, scopeKey } : metric;
+ }
+
+ function conversationMatchesActive(metric) {
+ const active = currentConversationId();
+ const metricConversationId = normalizeConversationId(metric?.conversationId);
+ if (active && metricConversationId !== active) return false;
+ const activeScope = currentScopeKey();
+ return activeScope && metric?.scopeKey ? metric.scopeKey === activeScope : true;
+ }
+
+ function aggregateLedgerEvents(events, scopeKey, conversationId, projectId) {
+ if (!events.length) return null;
+ const orderedEvents = events.slice().sort((left, right) => (left.observedAt || 0) - (right.observedAt || 0));
+ const usageEvents = [];
+ const contextEvents = [];
+ orderedEvents.forEach((event) => {
+ if (usageHasBreakdown(event.usage)) usageEvents.push(event);
+ else if (event.usage?.contextLimit || event.usage?.contextUsed) contextEvents.push(event);
+ });
+ const calls = [];
+ usageEvents.forEach((event) => {
+ const existing = calls.find((call) => {
+ const identity = strongCallIdentity(event);
+ const callIdentity = strongCallIdentity(call);
+ if (identity && callIdentity && identity === callIdentity) return true;
+ if (!sameUsageDetails(event, call)) return false;
+ if (event.source === call.source) return false;
+ return Math.abs((event.observedAt || 0) - (call.observedAt || 0)) <= CROSS_SOURCE_DEDUPE_WINDOW_MS;
+ });
+ if (existing) {
+ Object.assign(existing, mergeMetric(event, existing), {
+ observedAt: Math.min(existing.observedAt || event.observedAt || 0, event.observedAt || 0),
+ sourceSet: Array.from(new Set([...(existing.sourceSet || [existing.source]), event.source].filter(Boolean))),
+ });
+ } else {
+ calls.push({
+ ...event,
+ sourceSet: [event.source].filter(Boolean),
+ });
+ }
+ });
+ const usage = calls.reduce(
+ (total, event) => {
+ const item = event.usage || {};
+ total.inputTokens += item.inputTokens || 0;
+ total.inputTotalTokens += item.inputTotalTokens || item.inputTokens || 0;
+ total.outputTokens += item.outputTokens || 0;
+ total.outputTotalTokens += item.outputTotalTokens || item.outputTokens || 0;
+ total.totalTokens += item.totalTokens || item.inputTokens + item.outputTokens || 0;
+ total.requestTotalTokens += item.requestTotalTokens || item.totalTokens || item.inputTokens + item.outputTokens || 0;
+ total.cachedTokens += item.cachedTokens || 0;
+ total.cachedReadTokens += item.cachedReadTokens || item.cacheReadTokens || item.cachedTokens || 0;
+ total.cacheReadTokens += item.cacheReadTokens || 0;
+ total.cacheCreationTokens += item.cacheCreationTokens || 0;
+ total.totalEstimated = total.totalEstimated || !!item.totalEstimated;
+ return total;
+ },
+ {
+ inputTokens: 0,
+ inputTotalTokens: 0,
+ outputTokens: 0,
+ outputTotalTokens: 0,
+ totalTokens: 0,
+ requestTotalTokens: 0,
+ cachedTokens: 0,
+ cachedReadTokens: 0,
+ cacheReadTokens: 0,
+ cacheCreationTokens: 0,
+ totalEstimated: false,
+ },
+ );
+ const lastUsageEvent = calls[calls.length - 1] || orderedEvents[orderedEvents.length - 1];
+ const contextEvent = contextEvents[contextEvents.length - 1] || lastUsageEvent;
+ usage.hasBreakdown = calls.length > 0;
+ usage.contextUsed = contextEvent?.usage?.contextUsed || contextEvent?.usage?.totalTokens || lastUsageEvent?.usage?.contextUsed || usage.totalTokens;
+ usage.contextLimit = contextEvent?.usage?.contextLimit || lastUsageEvent?.usage?.contextLimit || 0;
+ const latest = orderedEvents[orderedEvents.length - 1];
+ return {
+ usage,
+ elapsedMs: Math.max(...orderedEvents.map((event) => event.elapsedMs || 0), 0),
+ source: "turn-aggregate",
+ projectId: projectId || latest?.projectId || "",
+ conversationId: conversationId || latest?.conversationId || "",
+ scopeKey: scopeKey || latest?.scopeKey || "",
+ turnId: latest?.turnId || "",
+ calls: calls.map((event) => ({ ...event, __usageCallKey: undefined })),
+ callCount: calls.length,
+ confidence: calls.some((event) => event.usage?.totalEstimated) ? "estimated" : "observed",
+ };
+ }
+
+ function syncLedgerTurnIdentity(turn) {
+ if (!turn?.id) return;
+ state.ledger.forEach((event) => {
+ if (event.turnId !== turn.id) return;
+ event.projectId = turn.projectId || event.projectId || "";
+ event.conversationId = turn.conversationId || event.conversationId || "";
+ event.scopeKey = turn.scopeKey || event.scopeKey || "";
+ });
+ }
+
+ function deriveTurnsFromLedger(activeScope, activeConversationId) {
+ const scopedEvents = state.ledger.filter((event) => {
+ if (!event?.turnId) return false;
+ if (activeScope) return event.scopeKey === activeScope;
+ return activeConversationId ? event.conversationId === activeConversationId : false;
+ });
+ if (!scopedEvents.length) return [];
+ const grouped = [];
+ const byTurnId = new Map();
+ scopedEvents.forEach((event) => {
+ if (!byTurnId.has(event.turnId)) {
+ const bucket = [];
+ byTurnId.set(event.turnId, bucket);
+ grouped.push(bucket);
+ }
+ byTurnId.get(event.turnId).push(event);
+ });
+ return grouped
+ .map((events) => {
+ const latest = events[events.length - 1];
+ return aggregateLedgerEvents(
+ events,
+ latest?.scopeKey || activeScope || "",
+ latest?.conversationId || activeConversationId || "",
+ latest?.projectId || "",
+ );
+ })
+ .filter((metric) => metric && (metric.callCount >= 1 || metric.usage?.contextLimit));
+ }
+
+ function deriveLatestMetricFromLedger(activeScope, activeConversationId) {
+ const turns = deriveTurnsFromLedger(activeScope, activeConversationId);
+ return turns.length ? turns[turns.length - 1] : null;
+ }
+
+ function adoptLedgerScopeIdentity(projectId, conversationId) {
+ const normalizedProjectId = normalizeProjectId(projectId);
+ const normalizedConversationId = normalizeConversationId(conversationId);
+ if (!normalizedProjectId || !normalizedConversationId) return false;
+ let changed = false;
+ state.ledger.forEach((event) => {
+ if (event.conversationId !== normalizedConversationId) return;
+ if (event.projectId && event.projectId !== normalizedProjectId) return;
+ const nextScopeKey = scopeKeyFor(normalizedProjectId, normalizedConversationId);
+ if (event.projectId !== normalizedProjectId || event.scopeKey !== nextScopeKey) {
+ event.projectId = normalizedProjectId;
+ event.scopeKey = nextScopeKey;
+ changed = true;
+ }
+ });
+ return changed;
+ }
+
+ function metricForActiveConversation() {
+ const active = currentConversationId();
+ const activeScope = currentScopeKey();
+ let completedMetric = null;
+ if (activeScope) {
+ completedMetric = deriveLatestMetricFromLedger(activeScope, active) || state.byScope[activeScope] || null;
+ } else {
+ completedMetric =
+ deriveLatestMetricFromLedger("", active)
+ || (active && state.byConversation[active])
+ || (conversationMatchesActive(state.lastMetric) ? state.lastMetric : null);
+ }
+ if (state.currentTurn && !state.currentTurn.calls.length && state.currentTurn.status === "running") {
+ if (
+ (!active || !state.currentTurn.conversationId || active === state.currentTurn.conversationId) &&
+ (!activeScope || !state.currentTurn.scopeKey || activeScope === state.currentTurn.scopeKey)
+ ) {
+ if (completedMetric?.callCount >= 1) return completedMetric;
+ return {
+ status: "running",
+ projectId: state.currentTurn.projectId || currentProjectId(),
+ conversationId: state.currentTurn.conversationId || active,
+ scopeKey: state.currentTurn.scopeKey || activeScope,
+ startedAt: state.currentTurn.startedAt,
+ elapsedMs: elapsedSinceTurnStarted(),
+ source: "turn-running",
+ };
+ }
+ }
+ return completedMetric;
+ }
+
+ function setActiveProjectId(projectId) {
+ const next = normalizeProjectId(projectId);
+ const previous = state.activeProjectId;
+ if (previous === next) return;
+ state.activeProjectId = next;
+ if (next && canAdoptScopeIdentity(state.currentTurn, next, state.currentTurn?.conversationId)) {
+ applyTurnScopeIdentity(state.currentTurn, next, state.currentTurn?.conversationId);
+ syncLedgerTurnIdentity(state.currentTurn);
+ }
+ if (next && currentConversationId() && adoptLedgerScopeIdentity(next, currentConversationId())) {
+ const restored = deriveLatestMetricFromLedger(scopeKeyFor(next, currentConversationId()), currentConversationId());
+ if (restored) publishMetric(restored, false);
+ }
+ scheduleRender();
+ }
+
+ function setActiveConversationId(conversationId) {
+ const next = normalizeConversationId(conversationId);
+ const previous = state.activeConversationId;
+ if (!next && state.currentTurn) {
+ scheduleRender();
+ return;
+ }
+ if (previous === next) return;
+ state.activeConversationId = next;
+ if (next && canAdoptScopeIdentity(state.currentTurn, state.currentTurn?.projectId, next)) {
+ applyTurnScopeIdentity(state.currentTurn, state.currentTurn?.projectId, next);
+ syncLedgerTurnIdentity(state.currentTurn);
+ }
+ if (next && currentProjectId() && adoptLedgerScopeIdentity(currentProjectId(), next)) {
+ const restored = deriveLatestMetricFromLedger(scopeKeyFor(currentProjectId(), next), next);
+ if (restored) publishMetric(restored, false);
+ }
+ if (typeof window.queueMicrotask === "function") {
+ window.queueMicrotask(() => {
+ restoreHistoryForConversation(next).catch(() => {});
+ });
+ } else {
+ Promise.resolve().then(() => restoreHistoryForConversation(next).catch(() => {}));
+ }
+ scheduleRender();
+ }
+
+ function metricKey(metric) {
+ const usage = metric?.usage || {};
+ return [
+ metric?.scopeKey || "",
+ metric?.conversationId || "",
+ metric?.source || "",
+ usage.totalTokens || 0,
+ usage.inputTokens || 0,
+ usage.outputTokens || 0,
+ usage.cachedTokens || 0,
+ usage.cacheReadTokens || 0,
+ usage.cacheCreationTokens || 0,
+ usage.contextUsed || 0,
+ usage.contextLimit || 0,
+ metric?.callCount || 0,
+ metric?.elapsedMs || 0,
+ ].join(":");
+ }
+
+ function usageCallKey(metric) {
+ const usage = metric?.usage || {};
+ return [
+ metric?.callId || metric?.eventId || metric?.requestId || metric?.responseId || "",
+ metric?.scopeKey || "",
+ metric?.conversationId || "",
+ usage.totalTokens || 0,
+ usage.inputTokens || 0,
+ usage.outputTokens || 0,
+ usage.cachedTokens || 0,
+ usage.cacheReadTokens || 0,
+ usage.cacheCreationTokens || 0,
+ ].join(":");
+ }
+
+ function createTurn(started = nowMs()) {
+ state.turnSeq += 1;
+ const projectId = currentProjectId();
+ const conversationId = currentConversationId();
+ return {
+ id: `${Date.now()}-${state.turnSeq}`,
+ startedAt: started,
+ lastUpdatedAt: started,
+ calls: [],
+ callKeys: new Set(),
+ contextUsage: null,
+ projectId,
+ conversationId,
+ scopeKey: scopeKeyFor(projectId, conversationId),
+ elapsedMs: 0,
+ status: "running",
+ };
+ }
+
+ function beginTurn(started = nowMs()) {
+ state.currentTurn = createTurn(started);
+ state.turnStartedAt = started;
+ state.pendingTurnStartAt = 0;
+ return state.currentTurn;
+ }
+
+ function ensureTurnStarted(started = nowMs()) {
+ if (
+ !state.currentTurn ||
+ state.pendingTurnStartAt ||
+ (!state.currentTurn.calls.length && started - state.currentTurn.lastUpdatedAt > TURN_IDLE_TIMEOUT_MS)
+ ) {
+ return beginTurn(started);
+ }
+ if (!state.turnStartedAt) state.turnStartedAt = state.currentTurn.startedAt || started;
+ return state.currentTurn;
+ }
+
+ function markTurnStarted(started = nowMs()) {
+ beginTurn(started);
+ scheduleRender();
+ }
+
+ function markUserTurnPending(started = nowMs()) {
+ state.pendingTurnStartAt = started;
+ }
+
+ function markNetworkTurnStarted(started = nowMs()) {
+ const turn = ensureTurnStarted(started);
+ if (!turn.calls.length) scheduleRender();
+ }
+
+ function elapsedSinceTurnStarted() {
+ return state.turnStartedAt ? nowMs() - state.turnStartedAt : 0;
+ }
+
+ function sameUsage(metric, other) {
+ const usage = metric?.usage || {};
+ const otherUsage = other?.usage || {};
+ if (metric?.scopeKey && other?.scopeKey && metric.scopeKey !== other.scopeKey) return false;
+ if (!usage.totalTokens || !otherUsage.totalTokens) return false;
+ if (usage.totalTokens !== otherUsage.totalTokens) return false;
+ if (metric.conversationId && other.conversationId && metric.conversationId !== other.conversationId) return false;
+ return true;
+ }
+
+ function sameUsageDetails(metric, other) {
+ const usage = metric?.usage || {};
+ const otherUsage = other?.usage || {};
+ return !!(
+ usage.totalTokens &&
+ otherUsage.totalTokens &&
+ usage.totalTokens === otherUsage.totalTokens &&
+ (usage.inputTokens || 0) === (otherUsage.inputTokens || 0) &&
+ (usage.outputTokens || 0) === (otherUsage.outputTokens || 0) &&
+ (usage.cachedTokens || 0) === (otherUsage.cachedTokens || 0) &&
+ (usage.cacheReadTokens || 0) === (otherUsage.cacheReadTokens || 0) &&
+ (usage.cacheCreationTokens || 0) === (otherUsage.cacheCreationTokens || 0)
+ );
+ }
+
+ function strongCallIdentity(metric) {
+ return metric?.callId || metric?.eventId || metric?.requestId || metric?.responseId || "";
+ }
+
+ function shouldDedupeCall(metric, existing) {
+ const identity = strongCallIdentity(metric);
+ const existingIdentity = strongCallIdentity(existing);
+ if (identity && existingIdentity && identity === existingIdentity) return true;
+ if (!sameUsageDetails(metric, existing)) return false;
+ if (metric.scopeKey && existing.scopeKey && metric.scopeKey !== existing.scopeKey) return false;
+ if (metric.source === existing.source) return false;
+ const elapsedDelta = Math.abs((metric.elapsedMs || 0) - (existing.elapsedMs || 0));
+ return elapsedDelta <= CROSS_SOURCE_DEDUPE_WINDOW_MS;
+ }
+
+ function mergeUsage(preferredUsage, fallbackUsage) {
+ const preferredHasBreakdown = usageHasBreakdown(preferredUsage);
+ const fallbackHasBreakdown = usageHasBreakdown(fallbackUsage);
+ const detailUsage = preferredHasBreakdown || !fallbackHasBreakdown ? preferredUsage : fallbackUsage;
+ const contextUsage = preferredUsage.contextLimit ? preferredUsage : fallbackUsage.contextLimit ? fallbackUsage : preferredUsage.contextUsed ? preferredUsage : fallbackUsage;
+ return {
+ inputTokens: detailUsage.inputTokens || 0,
+ inputTotalTokens: detailUsage.inputTotalTokens || detailUsage.inputTokens || 0,
+ outputTokens: detailUsage.outputTokens || 0,
+ outputTotalTokens: detailUsage.outputTotalTokens || detailUsage.outputTokens || 0,
+ totalTokens: detailUsage.totalTokens || contextUsage.totalTokens || 0,
+ requestTotalTokens: detailUsage.requestTotalTokens || detailUsage.totalTokens || contextUsage.totalTokens || 0,
+ cachedTokens: detailUsage.cachedTokens || 0,
+ cachedReadTokens: detailUsage.cachedReadTokens || detailUsage.cacheReadTokens || detailUsage.cachedTokens || 0,
+ cacheReadTokens: detailUsage.cacheReadTokens || 0,
+ cacheCreationTokens: detailUsage.cacheCreationTokens || 0,
+ totalEstimated: !!detailUsage.totalEstimated,
+ hasBreakdown: usageHasBreakdown(detailUsage),
+ contextUsed: contextUsage.contextUsed || contextUsage.totalTokens || detailUsage.totalTokens || 0,
+ contextLimit: contextUsage.contextLimit || detailUsage.contextLimit || 0,
+ };
+ }
+
+ function mergeMetric(preferred, fallback) {
+ return {
+ ...fallback,
+ ...preferred,
+ usage: mergeUsage(preferred.usage || {}, fallback.usage || {}),
+ elapsedMs: preferred.elapsedMs || fallback.elapsedMs || 0,
+ projectId: preferred.projectId || fallback.projectId || "",
+ conversationId: preferred.conversationId || fallback.conversationId || "",
+ scopeKey: preferred.scopeKey || fallback.scopeKey || "",
+ source: preferred.source || fallback.source,
+ };
+ }
+
+ function findMergeCandidate(metric) {
+ const matches = [...state.recent, ...readStoredDetails()].filter((item) => conversationMatchesActive(item) && sameUsageDetails(metric, item));
+ return matches.find((item) => usageHasBreakdown(item.usage)) || matches[0] || null;
+ }
+
+ function readStoredDetails() {
+ try {
+ const parsed = JSON.parse(window.sessionStorage?.getItem(STORAGE_KEY) || "[]");
+ return Array.isArray(parsed)
+ ? parsed.filter((item) => item?.usage && (item.callCount >= 1 || item.source === "turn-aggregate"))
+ : [];
+ } catch (_) {
+ return [];
+ }
+ }
+
+ function writeStoredDetails(metric) {
+ if (!usageHasBreakdown(metric?.usage)) return;
+ if (!(metric.callCount >= 1 || metric.source === "turn-aggregate")) return;
+ try {
+ const recent = [metric, ...readStoredDetails().filter((item) => !sameUsage(metric, item))].slice(0, RECENT_LIMIT);
+ window.sessionStorage?.setItem(STORAGE_KEY, JSON.stringify(recent));
+ } catch (_) {
+ // Storage can be unavailable in restricted renderer contexts.
+ }
+ }
+
+ function usageDebugSummary(usage) {
+ return {
+ inputTokens: usage.inputTokens || 0,
+ outputTokens: usage.outputTokens || 0,
+ totalTokens: usage.totalTokens || 0,
+ cachedTokens: usage.cachedTokens || usage.cacheReadTokens || 0,
+ contextLimit: usage.contextLimit || 0,
+ hasBreakdown: usageHasBreakdown(usage),
+ };
+ }
+
+ function appendLedgerEvent(kind, metric, extra = {}) {
+ const usage = metric?.usage || {};
+ state.eventSeq += 1;
+ const entry = {
+ id: `ledger-${state.eventSeq}`,
+ kind,
+ source: metric?.source || "",
+ observedAt: extra.observedAt ?? nowMs(),
+ projectId: extra.projectId ?? metric?.projectId ?? "",
+ conversationId: extra.conversationId ?? metric?.conversationId ?? "",
+ scopeKey: extra.scopeKey ?? metric?.scopeKey ?? "",
+ turnId: extra.turnId ?? "",
+ eventId: extra.eventId ?? metric?.eventId ?? "",
+ elapsedMs: metric?.elapsedMs || 0,
+ usage: metric?.usage || null,
+ rawSummary: {
+ inputTokens: usage.inputTokens || 0,
+ outputTokens: usage.outputTokens || 0,
+ totalTokens: usage.totalTokens || 0,
+ cachedTokens: usage.cachedReadTokens || usage.cachedTokens || usage.cacheReadTokens || 0,
+ contextLimit: usage.contextLimit || 0,
+ },
+ };
+ state.ledger.push(entry);
+ if (state.ledger.length > LEDGER_LIMIT) state.ledger = state.ledger.slice(-LEDGER_LIMIT);
+ }
+
+ function hasLedgerForConversation(conversationId) {
+ const normalizedConversationId = normalizeConversationId(conversationId);
+ return !!(normalizedConversationId && state.ledger.some((event) => event.conversationId === normalizedConversationId));
+ }
+
+ function appendHistoryLedgerEvent(item, fallbackConversationId, fallbackProjectId) {
+ const usage = normalizeUsage(item?.usage);
+ if (!usage) return false;
+ const conversationId = normalizeConversationId(item?.conversation_id || item?.conversationId || fallbackConversationId);
+ if (!conversationId) return false;
+ const projectId = normalizeProjectId(fallbackProjectId);
+ const scopeKey = scopeKeyFor(projectId, conversationId);
+ const turnId = String(item?.turn_id || item?.turnId || "");
+ const observedAt = parseObservedAt(item?.observed_at || item?.observedAt);
+ const duplicate = state.ledger.some(
+ (event) =>
+ event.turnId === turnId &&
+ event.conversationId === conversationId &&
+ event.observedAt === observedAt &&
+ (event.usage?.totalTokens || 0) === (usage.totalTokens || 0) &&
+ (event.usage?.inputTokens || 0) === (usage.inputTokens || 0) &&
+ (event.usage?.outputTokens || 0) === (usage.outputTokens || 0),
+ );
+ if (duplicate) return false;
+ appendLedgerEvent(
+ "usage",
+ {
+ usage,
+ elapsedMs: normalizeNumber(item?.elapsedMs || item?.elapsed_ms),
+ source: item?.source || "rollout-history",
+ conversationId,
+ projectId,
+ scopeKey,
+ },
+ {
+ observedAt,
+ turnId,
+ conversationId,
+ projectId,
+ scopeKey,
+ },
+ );
+ return true;
+ }
+
+ async function requestBridge(path, payload) {
+ const bridge = window.__codexSessionDeleteBridge;
+ if (typeof bridge === "function") return bridge(path, payload || {});
+ throw new Error("bridge unavailable");
+ }
+
+ async function restoreHistoryForConversation(conversationId, options = {}) {
+ const normalizedConversationId = normalizeConversationId(conversationId);
+ if (!normalizedConversationId) return null;
+ const restoreState = state.historyRestoreState[normalizedConversationId] || (state.historyRestoreState[normalizedConversationId] = {});
+ if (restoreState.promise) return restoreState.promise;
+ if (!options.force && (restoreState.completed || hasLedgerForConversation(normalizedConversationId))) {
+ return deriveLatestMetricFromLedger(currentScopeKey(), normalizedConversationId);
+ }
+ restoreState.promise = (async () => {
+ try {
+ const result = await requestBridge("/thread-usage-history", {
+ session_id: normalizedConversationId,
+ title: "",
+ });
+ if (!result || result.status !== "ok" || !Array.isArray(result.history)) {
+ return null;
+ }
+ const fallbackProjectId = currentProjectId();
+ let appended = 0;
+ result.history.forEach((item) => {
+ if (appendHistoryLedgerEvent(item, normalizedConversationId, fallbackProjectId)) appended += 1;
+ });
+ if (!fallbackProjectId && currentProjectId()) {
+ adoptLedgerScopeIdentity(currentProjectId(), normalizedConversationId);
+ }
+ restoreState.completed = true;
+ pushDebug({
+ type: "history-restore",
+ conversationId: normalizedConversationId,
+ appended,
+ source: "bridge",
+ });
+ const metric = deriveLatestMetricFromLedger(currentScopeKey(), normalizedConversationId)
+ || deriveLatestMetricFromLedger("", normalizedConversationId);
+ if (metric) publishMetric(metric, false);
+ return metric;
+ } catch (error) {
+ pushDebug({
+ type: "history-restore-failed",
+ conversationId: normalizedConversationId,
+ message: String(error?.message || error),
+ });
+ return null;
+ } finally {
+ restoreState.promise = null;
+ }
+ })();
+ return restoreState.promise;
+ }
+
+ function pushDebug(entry) {
+ state.debug.unshift({
+ at: new Date().toISOString(),
+ activeConversationId: currentConversationId(),
+ currentCallCount: state.currentTurn?.calls.length || 0,
+ pendingTurn: !!state.pendingTurnStartAt,
+ ...entry,
+ });
+ state.debug = state.debug.slice(0, DEBUG_LIMIT);
+ window.__codexTokenUsageDebug = state.debug.slice();
+ if (window.__codexTokenUsage) window.__codexTokenUsage.debug = state.debug.slice();
+ }
+
+ function aggregateTurnMetric(turn) {
+ const usage = turn.calls.reduce(
+ (total, call) => {
+ const item = call.usage || {};
+ total.inputTokens += item.inputTokens || 0;
+ total.inputTotalTokens += item.inputTotalTokens || item.inputTokens || 0;
+ total.outputTokens += item.outputTokens || 0;
+ total.outputTotalTokens += item.outputTotalTokens || item.outputTokens || 0;
+ total.totalTokens += item.totalTokens || item.inputTokens + item.outputTokens || 0;
+ total.requestTotalTokens += item.requestTotalTokens || item.totalTokens || item.inputTokens + item.outputTokens || 0;
+ total.cachedTokens += item.cachedTokens || 0;
+ total.cachedReadTokens += item.cachedReadTokens || item.cacheReadTokens || item.cachedTokens || 0;
+ total.cacheReadTokens += item.cacheReadTokens || 0;
+ total.cacheCreationTokens += item.cacheCreationTokens || 0;
+ total.totalEstimated = total.totalEstimated || !!item.totalEstimated;
+ return total;
+ },
+ {
+ inputTokens: 0,
+ inputTotalTokens: 0,
+ outputTokens: 0,
+ outputTotalTokens: 0,
+ totalTokens: 0,
+ requestTotalTokens: 0,
+ cachedTokens: 0,
+ cachedReadTokens: 0,
+ cacheReadTokens: 0,
+ cacheCreationTokens: 0,
+ totalEstimated: false,
+ },
+ );
+ const lastCallUsage = turn.calls[turn.calls.length - 1]?.usage || {};
+ const contextUsage = turn.contextUsage || (lastCallUsage.contextLimit ? lastCallUsage : null);
+ usage.hasBreakdown = turn.calls.length > 0;
+ usage.contextUsed = contextUsage?.contextUsed || contextUsage?.totalTokens || lastCallUsage.contextUsed || usage.totalTokens;
+ usage.contextLimit = contextUsage?.contextLimit || lastCallUsage.contextLimit || 0;
+ return {
+ usage,
+ elapsedMs: turn.elapsedMs,
+ source: "turn-aggregate",
+ projectId: turn.projectId,
+ conversationId: turn.conversationId,
+ scopeKey: turn.scopeKey,
+ turnId: turn.id,
+ calls: turn.calls.map((call) => ({ ...call, __usageCallKey: undefined })),
+ callCount: turn.calls.length,
+ confidence: turn.calls.some((call) => call.usage?.totalEstimated) ? "estimated" : "observed",
+ };
+ }
+
+ function rememberTurnMetric(metric) {
+ if (!metric?.scopeKey || !metric.turnId || metric.source !== "turn-aggregate") return;
+ const turns = state.turnsByScope[metric.scopeKey] || [];
+ const nextMetric = {
+ ...metric,
+ calls: (metric.calls || []).map((call) => ({ ...call })),
+ };
+ const existingIndex = turns.findIndex((item) => item.turnId === metric.turnId);
+ if (existingIndex >= 0) turns[existingIndex] = nextMetric;
+ else turns.push(nextMetric);
+ state.turnsByScope[metric.scopeKey] = turns.slice(-RECENT_LIMIT);
+ }
+
+ function exportUsage() {
+ const activeScope = currentScopeKey();
+ const currentTurn = state.currentTurn
+ ? {
+ id: state.currentTurn.id,
+ startedAt: state.currentTurn.startedAt,
+ lastUpdatedAt: state.currentTurn.lastUpdatedAt,
+ callCount: state.currentTurn.calls.length,
+ projectId: state.currentTurn.projectId,
+ conversationId: state.currentTurn.conversationId,
+ scopeKey: state.currentTurn.scopeKey,
+ }
+ : null;
+ const activeMetric = metricForActiveConversation();
+ return {
+ version: SCRIPT_VERSION,
+ activeProjectId: currentProjectId(),
+ activeConversationId: currentConversationId(),
+ activeScopeKey: activeScope,
+ last: activeMetric || state.lastMetric,
+ currentTurn,
+ calls: (state.currentTurn?.scopeKey === activeScope ? state.currentTurn.calls : activeMetric?.calls || []).map((call) => ({ ...call, __usageCallKey: undefined })),
+ ledgerEvents: state.ledger.slice().map((event) => ({ ...event })),
+ recent: state.recent.slice(),
+ debug: state.debug.slice(),
+ storedDetails: readStoredDetails(),
+ turns: deriveTurnsFromLedger(activeScope, currentConversationId()),
+ };
+ }
+
+ function publishMetric(metric, storeDetails = true) {
+ metric = scopedMetric(metric);
+ if (metric?.source !== "turn-running") {
+ const derived = deriveLatestMetricFromLedger(metric?.scopeKey || "", metric?.conversationId || "");
+ if (derived) metric = scopedMetric(derived);
+ }
+ const nextKey = metricKey(metric);
+ if (nextKey && nextKey === state.lastMetricKey) {
+ scheduleRender();
+ return;
+ }
+ state.lastMetricKey = nextKey;
+ state.lastMetric = {
+ ...metric,
+ id: `${Date.now()}-${Math.random().toString(16).slice(2)}`,
+ createdAt: new Date().toISOString(),
+ };
+ if (state.lastMetric.scopeKey) {
+ state.byScope[state.lastMetric.scopeKey] = state.lastMetric;
+ rememberTurnMetric(state.lastMetric);
+ }
+ if (state.lastMetric.conversationId) state.byConversation[state.lastMetric.conversationId] = state.lastMetric;
+ state.recent.unshift(state.lastMetric);
+ state.recent = state.recent.slice(0, RECENT_LIMIT);
+ window.__codexTokenUsage = {
+ version: SCRIPT_VERSION,
+ last: state.lastMetric,
+ currentTurn: state.currentTurn
+ ? {
+ id: state.currentTurn.id,
+ startedAt: state.currentTurn.startedAt,
+ lastUpdatedAt: state.currentTurn.lastUpdatedAt,
+ callCount: state.currentTurn.calls.length,
+ projectId: state.currentTurn.projectId,
+ conversationId: state.currentTurn.conversationId,
+ scopeKey: state.currentTurn.scopeKey,
+ }
+ : null,
+ recent: state.recent.slice(),
+ debug: state.debug.slice(),
+ export: exportUsage,
+ };
+ if (storeDetails) writeStoredDetails(state.lastMetric);
+ scheduleRender();
+ }
+
+ function rememberContextMetric(metric) {
+ metric = scopedMetric(metric);
+ const activeTurnMatches =
+ state.currentTurn?.calls.length &&
+ (!metric.scopeKey || !state.currentTurn.scopeKey || metric.scopeKey === state.currentTurn.scopeKey);
+ if (activeTurnMatches) {
+ appendLedgerEvent("context", metric, {
+ turnId: state.currentTurn.id,
+ projectId: state.currentTurn.projectId || metric.projectId,
+ conversationId: state.currentTurn.conversationId || metric.conversationId,
+ scopeKey: state.currentTurn.scopeKey || metric.scopeKey,
+ });
+ state.currentTurn.contextUsage = metric.usage;
+ applyTurnScopeIdentity(state.currentTurn, metric.projectId, metric.conversationId);
+ syncLedgerTurnIdentity(state.currentTurn);
+ state.currentTurn.elapsedMs = Math.max(state.currentTurn.elapsedMs || 0, metric.elapsedMs || 0);
+ state.currentTurn.lastUpdatedAt = nowMs();
+ publishMetric(aggregateTurnMetric(state.currentTurn), false);
+ return;
+ }
+ appendLedgerEvent("context", metric);
+ if (
+ state.lastMetric &&
+ (!metric.scopeKey || !state.lastMetric.scopeKey || metric.scopeKey === state.lastMetric.scopeKey) &&
+ nowMs() - (state.currentTurn?.lastUpdatedAt || 0) <= CONTEXT_MERGE_WINDOW_MS
+ ) {
+ publishMetric(mergeMetric(state.lastMetric, metric), false);
+ return;
+ }
+ publishMetric({ ...metric, callCount: 0 }, false);
+ }
+
+ function rememberUsageMetric(metric) {
+ metric = scopedMetric(metric);
+ const turn = ensureTurnStarted();
+ if (canAdoptScopeIdentity(turn, metric.projectId, metric.conversationId)) {
+ applyTurnScopeIdentity(turn, metric.projectId, metric.conversationId);
+ syncLedgerTurnIdentity(turn);
+ }
+ if (
+ (metric.conversationId && turn.conversationId && metric.conversationId !== turn.conversationId) ||
+ (metric.scopeKey && turn.scopeKey && metric.scopeKey !== turn.scopeKey)
+ ) {
+ beginTurn();
+ return rememberUsageMetric(metric);
+ }
+ appendLedgerEvent("usage", metric, {
+ turnId: turn.id,
+ projectId: turn.projectId || metric.projectId,
+ conversationId: turn.conversationId || metric.conversationId,
+ scopeKey: turn.scopeKey || metric.scopeKey,
+ });
+ const key = usageCallKey(metric);
+ const existing = turn.calls.find((call) => shouldDedupeCall(metric, call));
+ if (existing) {
+ const merged = mergeMetric(metric, existing);
+ Object.assign(existing, merged, { __usageCallKey: existing.__usageCallKey || key, dedupeReason: strongCallIdentity(metric) ? "identity" : "cross-source-window" });
+ } else {
+ const candidate = findMergeCandidate(metric);
+ if (candidate) {
+ metric = mergeMetric(metric, candidate);
+ }
+ turn.calls.push({ ...metric, __usageCallKey: key });
+ turn.callKeys.add(key);
+ }
+ applyTurnScopeIdentity(turn, metric.projectId, metric.conversationId);
+ syncLedgerTurnIdentity(turn);
+ turn.status = "complete";
+ turn.elapsedMs = Math.max(turn.elapsedMs || 0, metric.elapsedMs || elapsedSinceTurnStarted());
+ turn.lastUpdatedAt = nowMs();
+ publishMetric(aggregateTurnMetric(turn));
+ }
+
+ function rememberMetric(metric) {
+ if (!metric?.usage) return;
+ if (usageHasBreakdown(metric.usage)) {
+ rememberUsageMetric(metric);
+ } else {
+ rememberContextMetric(metric);
+ }
+ }
+
+ function rememberUsages(usages, baseMetric) {
+ let captured = false;
+ usages.forEach((usage) => {
+ rememberMetric({ ...baseMetric, usage, eventId: usage.eventId || "" });
+ captured = true;
+ });
+ return captured;
+ }
+
+ function processPayload(payload, source, conversationId, elapsedMs, url) {
+ const usages = extractUsages(payload);
+ pushDebug({
+ type: "payload",
+ source,
+ conversationId: conversationId || "",
+ url: url || "",
+ elapsedMs: elapsedMs || 0,
+ usageCount: usages.length,
+ usages: usages.map(usageDebugSummary),
+ });
+ return rememberUsages(usages, { elapsedMs, source, conversationId, url });
+ }
+
+ function parseResponseText(text, elapsedMs, url) {
+ processPayload(text, "network", "", elapsedMs, url);
+ }
+
+ function inspectPayload(payload, source, conversationId) {
+ return processPayload(payload, source, conversationId, elapsedSinceTurnStarted());
+ }
+
+ function inspectPayloadText(text, source, conversationId) {
+ return inspectPayload(text, source, conversationId);
+ }
+
+ function installFetchObserver() {
+ if (typeof window.fetch !== "function" || window.fetch.__codexTokenUsageWrapped === SCRIPT_VERSION) return;
+ const baseFetch = window.fetch.__codexTokenUsageOriginal || window.fetch;
+ const originalFetch = baseFetch.bind(window);
+ function wrappedFetch(input, init) {
+ const url = requestUrl(input);
+ const started = nowMs();
+ if (isCodexApiUrl(url)) markNetworkTurnStarted(started);
+ return originalFetch(input, init).then((response) => {
+ if (isCodexApiUrl(url) && response?.clone) {
+ response
+ .clone()
+ .text()
+ .then((text) => parseResponseText(text, nowMs() - started, url))
+ .catch(() => {});
+ }
+ return response;
+ });
+ }
+ wrappedFetch.__codexTokenUsageWrapped = SCRIPT_VERSION;
+ wrappedFetch.__codexTokenUsageOriginal = baseFetch;
+ window.fetch = wrappedFetch;
+ }
+
+ function installXhrObserver() {
+ const Xhr = window.XMLHttpRequest;
+ if (!Xhr || Xhr.prototype.__codexTokenUsageWrapped === SCRIPT_VERSION) return;
+ const originalOpen = Xhr.prototype.__codexTokenUsageOriginalOpen || Xhr.prototype.open;
+ const originalSend = Xhr.prototype.__codexTokenUsageOriginalSend || Xhr.prototype.send;
+ Xhr.prototype.open = function open(method, url, ...rest) {
+ this.__codexTokenUsageUrl = url;
+ return originalOpen.call(this, method, url, ...rest);
+ };
+ Xhr.prototype.send = function send(...args) {
+ const started = nowMs();
+ if (isCodexApiUrl(this.__codexTokenUsageUrl)) markNetworkTurnStarted(started);
+ this.addEventListener?.("loadend", () => {
+ const url = this.__codexTokenUsageUrl;
+ if (!isCodexApiUrl(url)) return;
+ try {
+ parseResponseText(this.responseText || "", nowMs() - started, url);
+ } catch (_) {
+ // Ignore unreadable XHR bodies.
+ }
+ });
+ return originalSend.apply(this, args);
+ };
+ Xhr.prototype.__codexTokenUsageOriginalOpen = originalOpen;
+ Xhr.prototype.__codexTokenUsageOriginalSend = originalSend;
+ Xhr.prototype.__codexTokenUsageWrapped = SCRIPT_VERSION;
+ }
+
+ function isEditableTarget(target) {
+ return !!(
+ target &&
+ (target.tagName === "TEXTAREA" ||
+ target.tagName === "INPUT" ||
+ target.isContentEditable ||
+ target.closest?.("textarea,input,[contenteditable='true']"))
+ );
+ }
+
+ function isSendTrigger(event) {
+ const target = event.target;
+ if (event.type === "submit") return true;
+ if (event.type === "keydown") {
+ return event.key === "Enter" && !event.shiftKey && isEditableTarget(target);
+ }
+ if (event.type === "click") {
+ const label = `${target?.getAttribute?.("aria-label") || ""} ${target?.textContent || ""}`;
+ return /^(发送|提交|Send|Submit)$|send|submit/i.test(label);
+ }
+ return false;
+ }
+
+ function installTurnPendingObserver() {
+ if (window.__codexTokenUsageTurnPendingObserver === SCRIPT_VERSION) return;
+ const handler = (event) => {
+ try {
+ if (!isSendTrigger(event)) return;
+ markUserTurnPending();
+ pushDebug({ type: "pending-turn", source: event.type });
+ } catch (_) {
+ // Keep page input handling untouched.
+ }
+ };
+ ["click", "submit", "keydown"].forEach((type) => {
+ document.addEventListener?.(type, handler, true);
+ });
+ window.__codexTokenUsageTurnPendingObserver = SCRIPT_VERSION;
+ }
+
+ function installPostMessageObserver() {
+ if (window.__codexTokenUsageMessageObserver === SCRIPT_VERSION) return;
+ window.addEventListener?.(
+ "message",
+ (event) => {
+ try {
+ inspectPayload(event.data, "post-message");
+ } catch (_) {
+ // Ignore unrelated window messages.
+ }
+ },
+ true,
+ );
+ window.__codexTokenUsageMessageObserver = SCRIPT_VERSION;
+ }
+
+ function installWebSocketObserver() {
+ if (typeof window.WebSocket !== "function" || window.__codexTokenUsageWebSocketWrapped === SCRIPT_VERSION) return;
+ const NativeWebSocket = window.__codexTokenUsageNativeWebSocket || window.WebSocket;
+
+ function TokenUsageWebSocket(...args) {
+ const socket = new NativeWebSocket(...args);
+ socket.addEventListener?.("message", (event) => {
+ try {
+ if (typeof event.data === "string") {
+ inspectPayloadText(event.data, "websocket");
+ } else if (event.data instanceof Blob && event.data.size <= 512000) {
+ event.data.text().then((text) => inspectPayloadText(text, "websocket")).catch(() => {});
+ }
+ } catch (_) {
+ // Keep socket delivery untouched.
+ }
+ });
+ return socket;
+ }
+
+ try {
+ TokenUsageWebSocket.prototype = NativeWebSocket.prototype;
+ Object.defineProperty(TokenUsageWebSocket, "CONNECTING", { value: NativeWebSocket.CONNECTING });
+ Object.defineProperty(TokenUsageWebSocket, "OPEN", { value: NativeWebSocket.OPEN });
+ Object.defineProperty(TokenUsageWebSocket, "CLOSING", { value: NativeWebSocket.CLOSING });
+ Object.defineProperty(TokenUsageWebSocket, "CLOSED", { value: NativeWebSocket.CLOSED });
+ } catch (_) {
+ // Constants are best-effort compatibility helpers.
+ }
+
+ window.WebSocket = TokenUsageWebSocket;
+ window.__codexTokenUsageNativeWebSocket = NativeWebSocket;
+ window.__codexTokenUsageWebSocketWrapped = SCRIPT_VERSION;
+ }
+
+ function normalizeContextReading(reading) {
+ if (!reading || typeof reading !== "object") return null;
+ const used = normalizeNumber(reading.used ?? reading.usedTokens ?? reading.used_tokens);
+ const limit = normalizeNumber(reading.limit ?? reading.contextWindow ?? reading.context_window);
+ if (!used && !limit) return null;
+ return {
+ usage: {
+ inputTokens: 0,
+ outputTokens: 0,
+ totalTokens: used,
+ cachedTokens: 0,
+ cacheReadTokens: 0,
+ cacheCreationTokens: 0,
+ hasBreakdown: false,
+ contextUsed: used,
+ contextLimit: limit,
+ },
+ elapsedMs: elapsedSinceTurnStarted(),
+ source: reading.source || "context-meter",
+ conversationId: reading.conversationId || "",
+ };
+ }
+
+ function rememberContextReading(reading) {
+ const metric = normalizeContextReading(reading);
+ if (metric) rememberMetric(metric);
+ }
+
+ function readContextMeterMetric() {
+ try {
+ const meterState = window.__codexContextMeter?.getState?.();
+ rememberContextReading(meterState?.lastReading);
+ } catch (_) {
+ // Ignore unavailable or changing third-party script state.
+ }
+ }
+
+ function installContextMeterObserver() {
+ const captureState = window.__codexContextMeterCaptureState;
+ if (captureState && captureState.__codexTokenUsageWrapped !== SCRIPT_VERSION) {
+ const originalInspectText = captureState.__codexTokenUsageOriginalInspectText || captureState.inspectText;
+ if (typeof originalInspectText === "function") {
+ captureState.inspectText = function codexTokenUsageInspectText(text, source, conversationId) {
+ const started = elapsedSinceTurnStarted();
+ try {
+ processPayload(text, source || "context-capture", conversationId, started);
+ } catch (_) {
+ // Keep the upstream context meter path intact.
+ }
+ return originalInspectText.apply(this, arguments);
+ };
+ }
+
+ const originalInspectValue = captureState.__codexTokenUsageOriginalInspectValue || captureState.inspectValue;
+ if (typeof originalInspectValue === "function") {
+ captureState.inspectValue = function codexTokenUsageInspectValue(value, source, conversationId) {
+ let reading = null;
+ try {
+ processPayload(value, source || "context-value", conversationId, elapsedSinceTurnStarted());
+ } catch (_) {
+ // Continue to the original inspector.
+ }
+ reading = originalInspectValue.apply(this, arguments);
+ rememberContextReading(reading);
+ return reading;
+ };
+ }
+ captureState.__codexTokenUsageOriginalInspectText = originalInspectText;
+ captureState.__codexTokenUsageOriginalInspectValue = originalInspectValue;
+ captureState.__codexTokenUsageWrapped = SCRIPT_VERSION;
+ }
+
+ readContextMeterMetric();
+ if (!state.contextPollTimer) {
+ state.contextPollTimer = window.setInterval?.(() => {
+ installContextMeterObserver();
+ readContextMeterMetric();
+ }, CONTEXT_POLL_INTERVAL_MS);
+ window.__codexTokenUsageContextPollTimer = state.contextPollTimer;
+ }
+ }
+
+ function ensureStyle() {
+ let style = document.getElementById?.(STYLE_ID);
+ if (!style) {
+ style = document.createElement("style");
+ style.id = STYLE_ID;
+ document.head?.appendChild(style);
+ }
+ style.textContent = `
+ .${BADGE_CLASS} {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ margin: 8px 0 0;
+ padding: 5px 9px;
+ border: 1px solid rgba(20, 184, 166, .3);
+ border-radius: 7px;
+ background: rgba(20, 184, 166, .08);
+ color: inherit;
+ font: 12px/1.35 ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
+ opacity: .9;
+ letter-spacing: 0;
+ }
+ .${BADGE_CLASS}[data-status="running"] {
+ border-color: rgba(245, 158, 11, .36);
+ background: rgba(245, 158, 11, .1);
+ }
+ .${BADGE_CLASS}[data-placement="message-actions"] {
+ display: flex;
+ width: fit-content;
+ margin: 6px 0 0;
+ }
+ main > .${BADGE_CLASS},
+ body > .${BADGE_CLASS} {
+ display: none !important;
+ }
+ `;
+ }
+
+ function visibleRect(node) {
+ if (!(node instanceof Element)) return null;
+ const rect = node.getBoundingClientRect();
+ if (!rect.width && !rect.height) return null;
+ return rect;
+ }
+
+ function isConversationActionButton(node) {
+ if (!(node instanceof Element)) return false;
+ const label = node.getAttribute("aria-label") || "";
+ return /^(复制|喜欢|不喜欢|从此处开始分叉|Copy|Good response|Bad response|Branch from here)$/i.test(label);
+ }
+
+ function isPrimaryConversationActionButton(node) {
+ if (!(node instanceof Element)) return false;
+ const label = node.getAttribute("aria-label") || "";
+ return /^(喜欢|不喜欢|从此处开始分叉|Good response|Bad response|Branch from here)$/i.test(label);
+ }
+
+ function scoreAssistantContainer(node) {
+ if (!(node instanceof Element)) return -1;
+ const rect = visibleRect(node);
+ if (!rect || rect.width < 240 || rect.height < 48) return -1;
+ const text = node.innerText || node.textContent || "";
+ if (!text || text.length < 20) return -1;
+ if (node.querySelector?.("textarea,[contenteditable='true']")) return -1;
+ if (/thread-scroll-container|main-surface|app-shell|timeline/i.test(String(node.className || ""))) return -1;
+
+ let score = 0;
+ if (node.querySelector?.("button[aria-label='复制'],button[aria-label='Copy']")) score += 6;
+ if (node.querySelector?.("button[aria-label='喜欢'],button[aria-label='不喜欢']")) score += 3;
+ if (/group flex min-w-0 flex-col/.test(String(node.className || ""))) score += 5;
+ if (node.querySelector?.("p,li,pre,code")) score += 2;
+ if (rect.height > 80) score += 1;
+ score -= Math.max(0, text.length / 2000);
+ return score;
+ }
+
+ function closestAssistantContainer(fromNode) {
+ let best = null;
+ let bestScore = -1;
+ for (let node = fromNode; node && node !== document.body; node = node.parentElement) {
+ const score = scoreAssistantContainer(node);
+ if (score > bestScore) {
+ best = node;
+ bestScore = score;
+ }
+ if (score >= 10) break;
+ }
+ return bestScore > 0 ? best : null;
+ }
+
+ function latestAssistantFromActionBar() {
+ const buttons = Array.from(document.querySelectorAll("button")).filter(isConversationActionButton);
+ const primaryButtons = buttons.filter(isPrimaryConversationActionButton);
+ const searchButtons = primaryButtons.length ? primaryButtons : buttons;
+ const visibleButtons = searchButtons.filter((button) => {
+ const rect = visibleRect(button);
+ return rect && rect.width > 0 && rect.height > 0;
+ });
+ for (let index = visibleButtons.length - 1; index >= 0; index -= 1) {
+ const container = closestAssistantContainer(visibleButtons[index]);
+ if (container) return container;
+ }
+ for (let index = searchButtons.length - 1; index >= 0; index -= 1) {
+ const container = closestAssistantContainer(searchButtons[index]);
+ if (container) return container;
+ }
+ return null;
+ }
+
+ function latestAssistantNode() {
+ const actionBarTarget = latestAssistantFromActionBar();
+ if (actionBarTarget) return actionBarTarget;
+
+ const selectors = [
+ '[data-message-author-role="assistant"]',
+ '[data-testid*="assistant"]',
+ 'article:has([data-message-author-role="assistant"])',
+ "main article",
+ "main [class*='message']",
+ ];
+ for (const selector of selectors) {
+ try {
+ const nodes = Array.from(document.querySelectorAll(selector)).filter((node) => node instanceof Element);
+ if (nodes.length) return nodes[nodes.length - 1];
+ } catch (_) {
+ // Some Chromium builds do not support every selector shape.
+ }
+ }
+ return null;
+ }
+
+ function elapsedFromAssistantNode(node) {
+ for (let current = node; current && current !== document.body; current = current.parentElement) {
+ const text = current.innerText || current.textContent || "";
+ if (text.length > 6000) break;
+ const elapsedMs = parseElapsedMs(text);
+ if (elapsedMs) return elapsedMs;
+ }
+ return 0;
+ }
+
+ function removeBadges() {
+ document.querySelectorAll?.(`.${BADGE_CLASS}`).forEach((node) => node.remove());
+ }
+
+ function renderMetric(metric = metricForActiveConversation()) {
+ if (!metric) {
+ removeBadges();
+ return;
+ }
+ if (!conversationMatchesActive(metric)) {
+ removeBadges();
+ return;
+ }
+ if (!metric) return;
+ ensureStyle();
+ const target = latestAssistantNode();
+ if (!target) return;
+ const displayMetric = {
+ ...metric,
+ elapsedMs: elapsedFromAssistantNode(target) || metric.elapsedMs,
+ };
+ document.querySelectorAll(`main > .${BADGE_CLASS}, body > .${BADGE_CLASS}`).forEach((node) => node.remove());
+ let badge = target.querySelector?.(`:scope > .${BADGE_CLASS}`);
+ if (!badge) {
+ badge = document.createElement("div");
+ badge.className = BADGE_CLASS;
+ target.appendChild(badge);
+ }
+ badge.dataset.metricId = displayMetric.id || "";
+ badge.dataset.status = displayMetric.status || "complete";
+ badge.dataset.conversationId = displayMetric.conversationId || "";
+ badge.dataset.version = SCRIPT_VERSION;
+ badge.dataset.placement = target === document.querySelector("main") ? "fallback" : "message-actions";
+ badge.textContent = formatBadgeText(displayMetric);
+ document.querySelectorAll(`.${BADGE_CLASS}`).forEach((node) => {
+ if (node !== badge) node.remove();
+ });
+ }
+
+ function scheduleRender() {
+ clearTimeout(window.__codexTokenUsageRenderTimer);
+ window.__codexTokenUsageRenderTimer = setTimeout(() => renderMetric(), 120);
+ }
+
+ function installDomObserver() {
+ if (!window.MutationObserver || window.__codexTokenUsageDomObserverVersion === SCRIPT_VERSION) return;
+ window.__codexTokenUsageDomObserver?.disconnect?.();
+ window.__codexTokenUsageDomObserver = new MutationObserver(() => {
+ const nextConversationId = conversationIdFromActiveRow() || conversationIdFromLocation();
+ if (nextConversationId && nextConversationId !== state.activeConversationId) setActiveConversationId(nextConversationId);
+ if (metricForActiveConversation()) scheduleRender();
+ });
+ const start = () => {
+ const root = document.querySelector("main") || document.body || document.documentElement;
+ if (root) window.__codexTokenUsageDomObserver.observe(root, { childList: true, subtree: true });
+ };
+ if (document.readyState === "loading") {
+ document.addEventListener("DOMContentLoaded", start, { once: true });
+ } else {
+ start();
+ }
+ window.__codexTokenUsageDomObserverVersion = SCRIPT_VERSION;
+ }
+
+ function installRouteObserver() {
+ if (window.__codexTokenUsageRouteObserver === SCRIPT_VERSION) return;
+ window.__codexTokenUsageRouteObserver = SCRIPT_VERSION;
+ const sync = () => {
+ setActiveConversationId(conversationIdFromActiveRow() || conversationIdFromLocation());
+ restoreHistoryForConversation(currentConversationId()).catch(() => {});
+ };
+ const originals = window.__codexTokenUsageRouteOriginals || {};
+ window.__codexTokenUsageRouteOriginals = originals;
+ const routeHistory = window.history;
+ ["pushState", "replaceState"].forEach((method) => {
+ const original = originals[method] || routeHistory?.[method];
+ originals[method] = original;
+ if (typeof original !== "function") return;
+ routeHistory[method] = function codexTokenUsagePatchedHistory(...args) {
+ const result = original.apply(routeHistory, args);
+ setTimeout(sync, 0);
+ return result;
+ };
+ });
+ window.addEventListener?.("popstate", sync, true);
+ window.addEventListener?.("hashchange", sync, true);
+ sync();
+ }
+
+ installFetchObserver();
+ installXhrObserver();
+ installTurnPendingObserver();
+ installPostMessageObserver();
+ installWebSocketObserver();
+ installContextMeterObserver();
+ installRouteObserver();
+ installDomObserver();
+ restoreHistoryForConversation(currentConversationId()).catch(() => {});
+
+ if (window.__CODEX_TOKEN_USAGE_SCRIPT_TEST__) {
+ window.__codexTokenUsageScriptTest = {
+ extractUsage,
+ formatBadgeText,
+ mergeMetric,
+ normalizeUsage,
+ normalizeContextReading,
+ parseElapsedMs,
+ processPayload,
+ rememberMetric,
+ markTurnStarted: markNetworkTurnStarted,
+ setActiveProjectId,
+ setActiveConversationId,
+ dispatchDocumentEvent: (type, event) => document.listeners?.[type]?.({ type, ...event }),
+ exportUsage,
+ getDisplayMetric: metricForActiveConversation,
+ getStoredDetails: readStoredDetails,
+ getTurnsForActiveConversation: () => deriveTurnsFromLedger(currentScopeKey(), currentConversationId()),
+ getTokenUsage: () => window.__codexTokenUsage,
+ restoreHistoryForConversation,
+ resetDerivedStatePreservingLedger: () => {
+ state.lastMetric = null;
+ state.lastMetricKey = "";
+ state.recent = [];
+ state.byConversation = Object.create(null);
+ state.byScope = Object.create(null);
+ state.turnsByScope = Object.create(null);
+ state.currentTurn = null;
+ state.turnStartedAt = 0;
+ },
+ };
+ }
+})();
diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md
index 5a28dbd..bb489a4 100644
--- a/THIRD_PARTY_NOTICES.md
+++ b/THIRD_PARTY_NOTICES.md
@@ -29,7 +29,7 @@ administrator variable.
Project: https://github.com/BigPizzaV3/CodexPlusPlus
-Pinned upstream version: v1.2.43.
+Pinned upstream version: v1.2.44.
License metadata: AGPL-3.0-only. A copy is provided at
`LICENSES/AGPL-3.0-only.txt`.
diff --git a/build-codex-one-click-installer.sh b/build-codex-one-click-installer.sh
index 550a293..63ab7da 100755
--- a/build-codex-one-click-installer.sh
+++ b/build-codex-one-click-installer.sh
@@ -18,9 +18,9 @@ CHECKSUMS_FILE="$DIST_ROOT/SHA256SUMS.txt"
DEVELOPER_ID="${DEVELOPER_ID_APPLICATION:-}"
ICON_MASTER="$ROOT/Resources/AppIcon/AppIcon-1024.png"
ICON_FILE="$ROOT/Resources/AppIcon/AppIcon.icns"
-CODEX_PLUS_VERSION='1.2.43'
+CODEX_PLUS_VERSION='1.2.44'
CODEX_PLUS_COMPATIBILITY_REVISION='cross-provider-content-v1'
-CODEX_PLUS_PATCH="$ROOT/patches/CodexPlusPlus/v1.2.43-cross-provider-history.patch"
+CODEX_PLUS_PATCH="$ROOT/patches/CodexPlusPlus/v1.2.44-cross-provider-history.patch"
die() {
printf 'build-codex-one-click-installer: %s\n' "$*" >&2
@@ -183,16 +183,22 @@ compile_gui x86_64 x86_64-apple-macos14.0
/bin/cp -cR "$PAYLOAD_ROOT" "$RESOURCES_DIR/offline-payloads" \
|| die "unable to clone offline payloads into the app bundle"
/bin/cp "$ROOT/Resources/installer-core.sh" "$RESOURCES_DIR/installer-core.sh"
+/bin/cp "$ROOT/scripts/install-skill-collections.sh" "$RESOURCES_DIR/install-skill-collections.sh"
+/bin/cp "$ROOT/skills/collections.json" "$RESOURCES_DIR/skill-collections.json"
+UNICODEX_SKILL_MANIFEST="$ROOT/skills/collections.json" \
+ UNICODEX_SKILL_DESTINATION="$RESOURCES_DIR/skill-collections" \
+ UNICODEX_SKILL_PREPARE_BUNDLE=1 \
+ "$ROOT/scripts/install-skill-collections.sh"
/bin/cp "$PAYLOAD_ROOT/model-catalog.json" "$RESOURCES_DIR/model-catalog.json"
/bin/cp "$ROOT/Resources/plugin-catalog.json" "$RESOURCES_DIR/plugin-catalog.json"
/bin/mkdir -p "$RESOURCES_DIR/CodexPlusPlus-Compatibility"
/bin/cp "$CODEX_PLUS_PATCH" \
- "$RESOURCES_DIR/CodexPlusPlus-Compatibility/v1.2.43-cross-provider-history.patch"
+ "$RESOURCES_DIR/CodexPlusPlus-Compatibility/v1.2.44-cross-provider-history.patch"
/usr/bin/tar -xOzf "$source_archive" "$codex_plus_provenance_member" \
> "$RESOURCES_DIR/CodexPlusPlus-Compatibility/CODEXKIT-PATCH.md"
/usr/bin/ditto "$ROOT/Resources/guides" "$RESOURCES_DIR/guides"
/usr/bin/ditto "$ROOT/Resources/licenses" "$RESOURCES_DIR/licenses"
-/bin/chmod 755 "$MACOS_DIR/CodexOneClickInstaller" "$RESOURCES_DIR/installer-support" "$RESOURCES_DIR/installer-core.sh"
+/bin/chmod 755 "$MACOS_DIR/CodexOneClickInstaller" "$RESOURCES_DIR/installer-support" "$RESOURCES_DIR/installer-core.sh" "$RESOURCES_DIR/install-skill-collections.sh"
/usr/bin/plutil -lint "$CONTENTS/Info.plist" >/dev/null
architectures="$(/usr/bin/lipo -archs "$MACOS_DIR/CodexOneClickInstaller")"
@@ -248,7 +254,7 @@ ln -s /Applications "$DMG_STAGE/Applications"
/bin/cp "$source_archive" "$DMG_STAGE/第三方许可与源码/$(/usr/bin/basename "$source_archive")"
/bin/cp "$CODEX_PLUS_PATCH" \
- "$DMG_STAGE/第三方许可与源码/v1.2.43-cross-provider-history.patch"
+ "$DMG_STAGE/第三方许可与源码/v1.2.44-cross-provider-history.patch"
/usr/bin/tar -xOzf "$source_archive" "$codex_plus_provenance_member" \
> "$DMG_STAGE/第三方许可与源码/CODEXKIT-PATCH.md"
license_member="$(/usr/bin/tar -tzf "$source_archive" | /usr/bin/awk '/\/(LICENSE|COPYING)(\.[A-Za-z0-9_-]+)?$/ {print; exit}')"
diff --git a/macos/online/build-online-dmg.sh b/macos/online/build-online-dmg.sh
index 4a4c1ab..609b62d 100644
--- a/macos/online/build-online-dmg.sh
+++ b/macos/online/build-online-dmg.sh
@@ -3,17 +3,27 @@ set -euo pipefail
ROOT="$(cd "$(dirname "$0")" && pwd)"
OUTPUT="${1:?output directory required}"
+CODEX_PLUS_ARM_DMG="${2:?arm64 Codex++ DMG required}"
+CODEX_PLUS_X64_DMG="${3:?x64 Codex++ DMG required}"
STAGE="$(/usr/bin/mktemp -d "${TMPDIR:-/tmp}/uni-codex-dmg.XXXXXX")"
trap '/bin/rm -rf "$STAGE"' EXIT
/bin/mkdir -p "$OUTPUT"
/bin/cp "$ROOT/install-unicodex.sh" "$STAGE/安装 Uni-codex.command"
+/bin/mkdir -p "$STAGE/codex-plus-plus"
+/bin/cp "$CODEX_PLUS_ARM_DMG" "$STAGE/codex-plus-plus/CodexPlusPlus-arm64.dmg"
+/bin/cp "$CODEX_PLUS_X64_DMG" "$STAGE/codex-plus-plus/CodexPlusPlus-x64.dmg"
+/bin/cp "$ROOT/../../scripts/install-skill-collections.sh" "$STAGE/install-skill-collections.sh"
+/bin/mkdir -p "$STAGE/skills"
+/bin/cp "$ROOT/../../skills/collections.json" "$STAGE/skills/collections.json"
/bin/chmod 755 "$STAGE/安装 Uni-codex.command"
-/usr/bin/ditto "$ROOT/../../LICENSE" "$STAGE/LICENSE.txt"
+/bin/chmod 755 "$STAGE/install-skill-collections.sh"
+if [[ -f "$ROOT/../../LICENSE" ]]; then
+ /usr/bin/ditto "$ROOT/../../LICENSE" "$STAGE/LICENSE.txt"
+fi
/usr/bin/hdiutil create -quiet -volname 'Uni-codex Online Installer' \
-srcfolder "$STAGE" -format UDZO "$OUTPUT/Uni-codex-macOS-Online.dmg"
(
cd "$OUTPUT"
/usr/bin/shasum -a 256 'Uni-codex-macOS-Online.dmg' > 'SHA256SUMS-macOS.txt'
)
-
diff --git a/macos/online/install-unicodex.sh b/macos/online/install-unicodex.sh
index 5c650fc..efe9c5d 100644
--- a/macos/online/install-unicodex.sh
+++ b/macos/online/install-unicodex.sh
@@ -5,6 +5,7 @@ umask 077
OPENAI_ARM_URL='https://persistent.oaistatic.com/codex-app-prod/Codex.dmg'
OPENAI_X64_URL='https://persistent.oaistatic.com/codex-app-prod/Codex-latest-x64.dmg'
CODEX_PLUS_REPOSITORY='BigPizzaV3/CodexPlusPlus'
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
WORK_ROOT="$(/usr/bin/mktemp -d "${TMPDIR:-/tmp}/uni-codex.XXXXXX")"
MOUNTS=()
@@ -63,14 +64,19 @@ openai_dmg="$WORK_ROOT/Codex.dmg"
download "$openai_url" "$openai_dmg"
install_app_from_dmg "$openai_dmg" 'ChatGPT.app'
-printf '正在查询 Codex++ GitHub Release…\n'
-release_json="$WORK_ROOT/codex-plus.json"
-download "https://api.github.com/repos/$CODEX_PLUS_REPOSITORY/releases/latest" "$release_json"
-version="$(/usr/bin/python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["tag_name"].lstrip("v"))' "$release_json")"
arch="$(/usr/bin/uname -m)"
[[ "$arch" == x86_64 ]] && asset_arch=x64 || asset_arch=arm64
-asset_name="CodexPlusPlus-$version-macos-$asset_arch.dmg"
-asset_url="$(/usr/bin/python3 - "$release_json" "$asset_name" <<'PY'
+BUNDLED_CODEX_PLUS_DMG="$SCRIPT_DIR/codex-plus-plus/CodexPlusPlus-$asset_arch.dmg"
+if [[ -s "$BUNDLED_CODEX_PLUS_DMG" ]]; then
+ codex_plus_dmg="$BUNDLED_CODEX_PLUS_DMG"
+ printf '使用安装包内置的 Codex++ (%s)…\n' "$asset_arch"
+else
+ printf '安装包未包含 Codex++,正在查询官方 GitHub Release…\n'
+ release_json="$WORK_ROOT/codex-plus.json"
+ download "https://api.github.com/repos/$CODEX_PLUS_REPOSITORY/releases/latest" "$release_json"
+ version="$(/usr/bin/python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["tag_name"].lstrip("v"))' "$release_json")"
+ asset_name="CodexPlusPlus-$version-macos-$asset_arch.dmg"
+ asset_url="$(/usr/bin/python3 - "$release_json" "$asset_name" <<'PY'
import json, sys
data = json.load(open(sys.argv[1]))
matches = [a['browser_download_url'] for a in data['assets'] if a['name'] == sys.argv[2]]
@@ -79,8 +85,9 @@ if len(matches) != 1:
print(matches[0])
PY
)"
-codex_plus_dmg="$WORK_ROOT/$asset_name"
-download "$asset_url" "$codex_plus_dmg"
+ codex_plus_dmg="$WORK_ROOT/$asset_name"
+ download "$asset_url" "$codex_plus_dmg"
+fi
codex_plus_mount="$(mount_dmg "$codex_plus_dmg")"
for app_name in 'Codex++.app' 'Codex++ 管理工具.app'; do
app="$codex_plus_mount/$app_name"
@@ -90,4 +97,8 @@ for app_name in 'Codex++.app' 'Codex++ 管理工具.app'; do
done
/usr/bin/hdiutil detach "$codex_plus_mount" >/dev/null
+printf '正在安装科研技能合集…\n'
+UNICODEX_SKILL_MANIFEST="$SCRIPT_DIR/skills/collections.json" \
+ "$SCRIPT_DIR/install-skill-collections.sh"
+
printf 'Uni-codex 安装完成。\n'
diff --git a/patches/CodexPlusPlus/v1.2.44-cross-provider-history.patch b/patches/CodexPlusPlus/v1.2.44-cross-provider-history.patch
new file mode 100644
index 0000000..519a662
--- /dev/null
+++ b/patches/CodexPlusPlus/v1.2.44-cross-provider-history.patch
@@ -0,0 +1,480 @@
+diff --git a/apps/codex-plus-manager/src-tauri/src/commands.rs b/apps/codex-plus-manager/src-tauri/src/commands.rs
+index fa76094..22aff5a 100644
+--- a/apps/codex-plus-manager/src-tauri/src/commands.rs
++++ b/apps/codex-plus-manager/src-tauri/src/commands.rs
+@@ -454,6 +454,12 @@ pub fn startup_options() -> CommandResult {
+ },
+ )
+ }
++#[tauri::command]
++pub fn take_content_compatibility_notice()
++-> Result