codex-ringside 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.1 (2026-11-03)
4
+
5
+ - Bundle a wallpaper and background preset for fresh installations (cover, 134% zoom, top-left alignment, zero transparency and blur). Preserve existing images, edits and disabled backgrounds on upgrades, reinstalls and Codex Plus migrations.
6
+ - Verify preset inclusion in npm packages and standalone bundles, real image preparation, settings preservation and failed-install rollback.
7
+
8
+ ## 0.3.0 (2026-11-03)
9
+
10
+ - Replace the native sidebar usage icon with Ringside meters in place, preserving its usage menu, management actions and keyboard controls. Restore the native icon when the add-on is removed.
11
+ - Show plan, credit balance (including zero/unlimited), reset times and reset cards in the hover details. Clear all account-specific values on expiry, disconnection and account changes.
12
+ - Preserve the original background-settings action on ring click, Enter and Space, including interception of the native pointerdown menu. Keep the native usage menu under a separate hover-details action and ArrowDown; right-click and Shift+F10 still open background settings.
13
+ - Add Chromium interaction coverage for native replacement, remounting, cleanup, menu actions, background settings and stale/credit-only states.
14
+
3
15
  ## 0.2.0 (2026-10-31)
4
16
 
5
17
  - Automatically check stable GitHub releases every six hours, verify and install newer standalone bundles, and retry failures after one hour. Preserve settings, back up and roll back failed updates without closing the desktop client.
package/README.md CHANGED
@@ -4,7 +4,9 @@ English · [简体中文](README.zh-CN.md)
4
4
 
5
5
  Usage rings and customizable backgrounds for the Codex desktop sidebar on macOS.
6
6
 
7
- See your remaining allowance at a glance, hover for reset times, and click a ring to configure a wallpaper, image or glass background. Image controls include cropping, positioning, zoom, blur and color adjustments.
7
+ Rings replace the native sidebar usage icon in place. Hover for your plan, remaining allowance, credit balance, reset times and reset cards; click to open the original background settings and configure a wallpaper, image or glass background. Image controls include cropping, positioning, zoom, blur and color adjustments. The hover details' usage-menu button preserves the native menu and its management actions.
8
+
9
+ Credit balances are grouped and rounded to whole credits, including zero; unlimited balances are labeled explicitly, and missing balances are omitted. Credit-only accounts show a balance summary instead of a percentage ring. Account changes, connection failures and data older than 150 seconds clear old account values. On older clients without a compatible usage button, a separate ring opens background settings on click. Removing the add-on restores the native icon.
8
10
 
9
11
  **Unofficial community add-on. Not affiliated with or endorsed by OpenAI.** Parts are adapted from [Codex Usage Card](https://github.com/Lorie7813/codex-usage-card); see [NOTICE](NOTICE).
10
12
 
@@ -102,7 +104,9 @@ Reinstalling preserves observe mode; `--observe` explicitly disables startup han
102
104
 
103
105
  ## Background controls
104
106
 
105
- Click a ring to open settings. Panel changes apply immediately; CLI changes apply within five seconds.
107
+ A fresh installation automatically applies the bundled wallpaper: image source, cover layout, 134% zoom, top-left alignment, zero transparency and blur, 100% brightness/contrast/saturation, and no crop. The image is stored inside the add-on's directory; no separate download or file selection is needed. Upgrades, reinstalls and Codex Plus migrations preserve existing images and settings, including leaving backgrounds off for users who never enabled them. You can change the image, adjust the controls or turn the background off at any time. Framing varies with window proportions; the client's light/dark appearance setting is preserved.
108
+
109
+ Click a ring, or press Enter or Space while focused, to open background settings. The hover details' background settings button, right-click and Shift+F10 also work. Open the native usage menu through the separate button in the hover details or ArrowDown while the ring is focused; this entry is hidden on clients without a compatible native usage button. Panel changes apply immediately; CLI changes apply within five seconds.
106
110
 
107
111
  | Source | Behavior |
108
112
  | --- | --- |
@@ -132,13 +136,27 @@ Options without a source keep the current source. `background` without arguments
132
136
  - Background cleanup uses host DOM heuristics. Terminals, code blocks, canvases and embedded web content are left alone; an unfamiliar page may retain opaque areas.
133
137
  - Host updates can break rate-limit reading, placement or theme integration. Disable the background or uninstall if problems arise.
134
138
 
139
+ ### Recommended wallpaper sites
140
+
141
+ Find a background image on these sites:
142
+
143
+ | Site | Highlights |
144
+ | --- | --- |
145
+ | [Wallhaven](https://wallhaven.cc/) | A broad wallpaper collection with keyword, resolution and aspect-ratio filters |
146
+ | [WallpaperHub](https://www.wallpaperhub.app/) | Windows, Surface and Fluent Design wallpapers in sizes for different devices |
147
+ | [Peapix](https://peapix.com/) | Bing daily wallpapers and Windows Spotlight images, including landscapes, architecture and nature photography |
148
+ | [Unsplash](https://unsplash.com/wallpapers) | HD photography wallpapers covering nature, travel, animals and more |
149
+ | [Pexels](https://www.pexels.com/search/desktop%20wallpaper/) | A photography library with desktop, 4K and dark wallpaper categories |
150
+
151
+ Download an image to your Mac, click a sidebar ring, then choose the `image` source and select the file. You can also set it as your macOS desktop wallpaper and choose the `wallpaper` source to align the background with your desktop.
152
+
135
153
  ## Status and troubleshooting
136
154
 
137
155
  ```bash
138
156
  "$HOME/Library/Application Support/CodexRingside/bin/codex-ringside" status
139
157
  ```
140
158
 
141
- Look for `agent: "running"`, `cdp.windows[].card.placed`, and `cdp.windows[].background.applied`. `nativeGlass: false` means the host disables native glass. `startup.event` explains skipped handoffs. Status may contain private paths and window URLs; redact it before sharing.
159
+ Look for `agent: "running"`, `cdp.windows[].card.placed`, and `cdp.windows[].background.applied`. `card.replacesNative` confirms replacement of the native usage icon; `card.credits` and `card.plan` show current allowance details. `nativeGlass: false` means the host disables native glass. `startup.event` explains skipped handoffs. Status may contain private paths and window URLs; redact it before sharing.
142
160
 
143
161
  If no ring appears, verify the signed-in host, application path and whether port 39222 is occupied. Node and Command Line Tools checks apply only to npm/source installations. A missing release asset means that version does not contain a standalone bundle; use 0.2.0 or later. A checksum or download failure stops before changing the installed app; retry when the network is available. Installing while the host is open does not trigger a restart. Save your work and reopen its original icon when convenient. Check `~/Library/Logs/CodexRingside` for errors.
144
162
 
package/README.zh-CN.md CHANGED
@@ -4,19 +4,21 @@
4
4
 
5
5
  ![背景设置](https://raw.githubusercontent.com/tccpc/codex-ringside/main/docs/images/background-settings.png)
6
6
 
7
- 在 Codex 桌面端的侧栏显示剩余额度圆环,悬停可看重置时间;点击圆环可以自定义 Codex 的背景:图片、壁纸或毛玻璃,透明度和模糊自由调节,图片还能裁剪、缩放和调整布局。仅支持 macOS。
7
+ 用剩余额度圆环替换 Codex 桌面端侧栏的官方用量图标,悬停可看套餐、余额和重置时间,点击圆环打开原有背景设置。可以自定义图片、壁纸或毛玻璃,透明度和模糊自由调节,图片还能裁剪、缩放和调整布局。悬停详情中的“用量菜单”保留官方“管理用量”等操作。仅支持 macOS。
8
8
 
9
9
  这是非官方插件,与 OpenAI 无关。原理参考 [Codex Usage Card](https://github.com/Lorie7813/codex-usage-card),署名见 [NOTICE](NOTICE)。
10
10
 
11
11
  ## 工作原理
12
12
 
13
13
  - **额度**:启动 Codex 内置的 `codex app-server`,通过 stdio 调用 `account/rateLimits/read`,每 60 秒刷新一次。
14
- - **显示**:通过本机调试端口 `127.0.0.1:39222` 把卡片注入到 `nav[data-app-navigation-rail]`。
14
+ - **显示**:通过本机调试端口 `127.0.0.1:39222` 在侧栏官方用量按钮内替换图标,保留原生菜单、键盘操作和“管理用量”等账号功能。找不到兼容的官方按钮时,回退为独立圆环;卸载会恢复原图标。
15
15
  - **背景**:同样通过调试端口,把 Codex 外壳层的背景清空,在页面最底层放一张图片。窗口属性由主进程控制,插件改不了,所以“透明”透出的是 Codex 自带的系统毛玻璃,壁纸来源显示的是对齐到屏幕位置的桌面壁纸。
16
16
  - **启动**:Codex 的 `app.asar` 受完整性校验保护,不能修改,只能让它带调试端口启动。后台任务发现你从原图标打开 Codex 时,会在安全条件都满足的情况下有序退出,再带端口重开一次。
17
17
 
18
18
  账号有几个额度窗口就显示几个圆环。圆环是分段刻度样式,环内显示剩余百分比和窗口名称(“5小时”或“周”)。额度充足时为青绿渐变,50% 及以下转为黄橙,低于 10% 转为橙红。
19
19
 
20
+ 悬停详情显示套餐、各窗口的剩余百分比及重置时间、可用重置卡,以及接口提供的额度余额。余额按整数分组显示,例如 `12,346`;零余额显示 `0`,无限额度显示“不限额”,未返回余额时不猜测数值。只有余额、没有百分比窗口时,侧栏显示余额摘要。连接失败、数据超过 150 秒未更新或账号切换时,会清除旧数值并提示重试。重置、购买和管理操作继续由官方菜单处理。
21
+
20
22
  ## 安装
21
23
 
22
24
  一键安装需要 **macOS 13 或更高版本**(Apple Silicon 或 Intel),以及已登录的兼容桌面客户端。**无需自行安装 Node.js、npm、Homebrew 或 Xcode Command Line Tools。** 安装包自带专用 Node 和预编译 helper。开发验证使用 `/Applications/ChatGPT.app`,版本 `26.1027.30824`;其他客户端版本依赖相同内部结构,未逐一验证。命令行和设置面板目前使用中文。
@@ -111,7 +113,9 @@ npx codex-ringside@latest install
111
113
 
112
114
  ## 自定义背景
113
115
 
114
- 点击侧栏的圆环打开设置面板。先选来源,再用效果调外观,所有调节都立即生效。
116
+ 首次安装会自动应用安装包内置的壁纸:图片来源、铺满布局、缩放 134%、左上对齐、透明度和模糊均为 0,亮度、对比度和饱和度均为 100%,不裁剪。图片保存在插件自己的目录内,无需另行下载或选择。升级、重装和从 Codex Plus 迁移都会保留已有图片和设置;之前未启用背景的用户也会继续保持关闭。可随时在面板中换图、调整参数或选择“关闭”。窗口比例不同会影响取景,客户端的浅色/深色模式保持用户自己的选择。
117
+
118
+ 直接点击侧栏圆环打开原有背景设置面板,聚焦后按 Enter 或空格也可以打开。悬停面板中的“背景设置”、右键和 Shift+F10 仍可使用。官方用量菜单通过悬停面板中的“用量菜单”打开,也可以聚焦圆环后按向下键;没有兼容的官方按钮时,这个菜单入口隐藏。先选来源,再用效果调外观,所有调节都立即生效。
115
119
 
116
120
  面板顶部还能切换 Codex 的**外观模式**:跟随系统、浅色、深色。它和设置页的“模式”是同一个设置,改动会保存,并在两边同步。这里用的是 Codex 内部的设置函数,找不到时控件自动隐藏。
117
121
 
@@ -163,6 +167,20 @@ npx codex-ringside@latest install
163
167
  - **切换有延迟**:切换桌面空间后,壁纸最多 60 秒才更新;拖动窗口时壁纸有轻微跟随延迟。
164
168
  - **页面自动适配**:内容区里方角、无边框、占满区域的不透明层会被当作页面背景清除,设置页这类圆角卡片变成半透明磨砂,文字依然清楚。终端、代码块和内嵌网页不处理。设计不同的页面可能漏掉,这时自带背景的地方保持原样。
165
169
 
170
+ ### 壁纸网站推荐
171
+
172
+ 可以从这些网站挑选背景图片:
173
+
174
+ | 网站 | 推荐内容 |
175
+ |---|---|
176
+ | [Wallhaven](https://wallhaven.cc/) | 综合壁纸图库,可按关键词、分辨率和宽高比筛选 |
177
+ | [WallpaperHub](https://www.wallpaperhub.app/) | Windows、Surface 和 Fluent Design 风格壁纸,提供多种设备尺寸 |
178
+ | [Peapix](https://peapix.com/) | Bing 每日壁纸与 Windows Spotlight 图片,适合寻找风景、建筑和自然摄影 |
179
+ | [Unsplash](https://unsplash.com/wallpapers) | 摄影类高清壁纸,涵盖自然、旅行、动物等主题 |
180
+ | [Pexels](https://www.pexels.com/search/desktop%20wallpaper/) | 摄影图库,提供桌面、4K 和深色壁纸等分类 |
181
+
182
+ 下载图片到本机后,点击侧栏圆环,在背景设置面板中选择 `image` 图片来源并选取文件。也可以先将图片设为 macOS 桌面壁纸,再选择 `wallpaper` 壁纸来源,让背景与桌面对齐。
183
+
166
184
  ## 查看状态
167
185
 
168
186
  下载失败或 SHA-256 校验失败时,安装器会在更改现有安装前停止,可在网络恢复后重试。发行版缺少安装包时,请选择 0.2.0 或更高版本。通过一键入口安装后,无需检查系统 Node 或编译工具。
@@ -171,7 +189,7 @@ npx codex-ringside@latest install
171
189
  "$HOME/Library/Application Support/CodexRingside/bin/codex-ringside" status
172
190
  ```
173
191
 
174
- 重点看 `cdp.windows[].card.placed`。背景看 `background`(保存的设置)和 `cdp.windows[].background`:`applied` 表示已生效,`nativeGlass` 为 `false` 表示 Codex 此刻关闭了毛玻璃。输出可能包含本机路径,分享前请先脱敏。
192
+ 重点看 `cdp.windows[].card.placed`,`replacesNative` 为 `true` 表示已替换官方用量图标;`credits` 和 `plan` 显示当前余额和套餐。背景看 `background`(保存的设置)和 `cdp.windows[].background`:`applied` 表示已生效,`nativeGlass` 为 `false` 表示 Codex 此刻关闭了毛玻璃。输出可能包含本机路径,分享前请先脱敏。
175
193
 
176
194
  ## 卸载
177
195
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-ringside",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Usage rings and customizable backgrounds for the Codex desktop app. Unofficial macOS add-on.",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",
@@ -15,6 +15,7 @@
15
15
  ],
16
16
  "scripts": {
17
17
  "test": "node --test test/*.test.cjs",
18
+ "test:card": "node --test test/card.browser.cjs",
18
19
  "install-agent": "node bin/codex-ringside.cjs install",
19
20
  "uninstall-agent": "node bin/codex-ringside.cjs uninstall",
20
21
  "status": "node bin/codex-ringside.cjs status",
@@ -48,10 +49,15 @@
48
49
  "bin/*.cjs",
49
50
  "src/**/*.cjs",
50
51
  "src/**/*.js",
52
+ "src/presets/default-background.json",
53
+ "src/presets/wallpaper.png",
51
54
  "native/bridge.swift",
52
55
  "README*.md",
53
56
  "LICENSE",
54
57
  "NOTICE",
55
58
  "CHANGELOG.md"
56
- ]
59
+ ],
60
+ "devDependencies": {
61
+ "playwright": "1.64.1"
62
+ }
57
63
  }
package/src/agent.cjs CHANGED
@@ -65,6 +65,7 @@ async function runAgent({ appPath, root, signal }) {
65
65
  seenAuth = stamp;
66
66
  log('account file changed, reconnecting');
67
67
  dropClient();
68
+ showUnavailable();
68
69
  nextRead = 0;
69
70
  }
70
71
 
package/src/card.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // Runs inside the Codex renderer. Must stay self-contained: cdp.cjs injects this file as text.
2
2
  function installCodexRingsideCard() {
3
- const VERSION = 13;
3
+ const VERSION = 17;
4
4
  const KEY = '__codexRingsideCard';
5
5
  const STALE_AFTER_MS = 150000;
6
6
  const TICKS = 30;
@@ -18,14 +18,14 @@ function installCodexRingsideCard() {
18
18
  const card = document.createElement('div');
19
19
  card.id = 'codex-ringside-card';
20
20
  card.tabIndex = 0;
21
- card.setAttribute('role', 'group');
21
+ card.setAttribute('role', 'button');
22
22
  card.setAttribute('aria-label', 'Codex 剩余额度');
23
- card.setAttribute('aria-describedby', 'codex-ringside-tooltip');
24
23
  card.setAttribute('aria-haspopup', 'dialog');
25
24
 
26
25
  const tooltip = document.createElement('div');
27
26
  tooltip.id = 'codex-ringside-tooltip';
28
- tooltip.setAttribute('role', 'tooltip');
27
+ tooltip.setAttribute('role', 'dialog');
28
+ tooltip.setAttribute('aria-label', 'Codex 剩余额度');
29
29
  tooltip.hidden = true;
30
30
 
31
31
  const style = document.createElement('style');
@@ -47,12 +47,16 @@ function installCodexRingsideCard() {
47
47
  /* The spread also grows the corners, so the outer radius is 4 + 3 = 7px; the width matches the rail buttons' 36px. */
48
48
  #codex-ringside-card:hover { background: var(--ringside-hover); box-shadow: 0 0 0 3px var(--ringside-hover); }
49
49
  #codex-ringside-card:active { transform: scale(.96); }
50
+ /* Keep the native menu available, with Ringside's original background-settings click. */
51
+ [data-codex-ringside-host] { height: auto !important; min-height: 36px; padding: 3px !important; }
52
+ [data-codex-ringside-host] > :not(#codex-ringside-card) { display: none !important; }
53
+ [data-codex-ringside-host] > #codex-ringside-card { margin: 0; pointer-events: none; }
50
54
  @media (prefers-reduced-motion: reduce) { #codex-ringside-card { transition: none; } }
51
55
  html.dark #codex-ringside-card, html[data-theme="dark"] #codex-ringside-card { ${dark} }
52
56
  @media (prefers-color-scheme: dark) {
53
57
  html:not(.light):not([data-theme="light"]) #codex-ringside-card { ${dark} }
54
58
  }
55
- #codex-ringside-card[hidden], #codex-ringside-tooltip[hidden] { display: none !important; }
59
+ #codex-ringside-card[hidden], #codex-ringside-tooltip[hidden], #codex-ringside-tooltip [hidden] { display: none !important; }
56
60
  #codex-ringside-card:focus-visible { outline: 1px solid var(--ringside-text); outline-offset: 2px; }
57
61
  #codex-ringside-card .ringside-meter { position: relative; flex: 0 0 auto; width: 30px; height: 30px; }
58
62
  #codex-ringside-card .ringside-meter[data-tone="normal"] { --ringside-glow: #34e08280; }
@@ -67,11 +71,12 @@ function installCodexRingsideCard() {
67
71
  #codex-ringside-card .ringside-number { font-size: 8px; line-height: 9px; font-weight: 600; letter-spacing: -.3px; white-space: nowrap; }
68
72
  #codex-ringside-card .ringside-label { font-size: 6.5px; line-height: 8px; color: var(--ringside-label); white-space: nowrap; }
69
73
  #codex-ringside-tooltip {
70
- box-sizing: border-box; position: fixed; z-index: 2147483000; min-width: 170px; max-width: min(320px, calc(100vw - 16px));
71
- padding: 12px 14px; border: 1px solid color-mix(in srgb, currentColor 12%, transparent); border-radius: 10px;
74
+ box-sizing: border-box; position: fixed; z-index: 2147483000; min-width: 204px; max-width: min(320px, calc(100vw - 16px));
75
+ padding: 12px 14px; border: 1px solid color-mix(in srgb, currentColor 12%, transparent); border-radius: 12px;
72
76
  background: var(--color-surface-elevated-secondary, #f8f8f7); color: var(--color-text-primary, #303630);
73
- box-shadow: 0 4px 18px #0002; pointer-events: none; -webkit-app-region: no-drag;
74
- font: 12px/1.5 -apple-system, BlinkMacSystemFont, sans-serif;
77
+ box-shadow: 0 6px 22px #00000024; -webkit-app-region: no-drag;
78
+ max-height: calc(100vh - 16px); overflow: auto;
79
+ font: 12px/1.45 -apple-system, BlinkMacSystemFont, sans-serif;
75
80
  }
76
81
  html.dark #codex-ringside-tooltip, html[data-theme="dark"] #codex-ringside-tooltip {
77
82
  background: var(--color-surface-elevated-secondary, #2c2e2c); color: var(--color-text-primary, #edf0ed);
@@ -81,21 +86,53 @@ function installCodexRingsideCard() {
81
86
  background: var(--color-surface-elevated-secondary, #2c2e2c); color: var(--color-text-primary, #edf0ed);
82
87
  }
83
88
  }
84
- #codex-ringside-tooltip .ringside-title { font-weight: 600; }
85
- #codex-ringside-tooltip .ringside-sections { display: flex; gap: 20px; margin-top: 8px; }
86
- #codex-ringside-tooltip .ringside-section-percent { margin: 2px 0; font-size: 22px; line-height: 26px; font-weight: 700; font-variant-numeric: tabular-nums; }
87
- #codex-ringside-tooltip .ringside-dim { opacity: .68; font-size: 11px; white-space: nowrap; }
88
- #codex-ringside-tooltip .ringside-footer { margin-top: 10px; padding-top: 8px; border-top: 1px solid color-mix(in srgb, currentColor 10%, transparent); }
89
+ #codex-ringside-tooltip .ringside-header { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
90
+ #codex-ringside-tooltip .ringside-title { font-weight: 600; font-size: 12.5px; }
91
+ #codex-ringside-tooltip .ringside-badge {
92
+ padding: 1px 7px; border-radius: 999px; font-size: 10.5px; line-height: 16px; font-weight: 500;
93
+ background: color-mix(in srgb, currentColor 9%, transparent); color: color-mix(in srgb, currentColor 80%, transparent); white-space: nowrap;
94
+ }
95
+ #codex-ringside-tooltip .ringside-sections { display: flex; flex-wrap: wrap; gap: 10px 20px; margin-top: 10px; }
96
+ #codex-ringside-tooltip .ringside-section { flex: 1 1 auto; min-width: 72px; }
97
+ #codex-ringside-tooltip .ringside-section-percent {
98
+ margin: 2px 0; font-size: 24px; line-height: 28px; font-weight: 700; letter-spacing: -.02em; font-variant-numeric: tabular-nums;
99
+ }
100
+ #codex-ringside-tooltip .ringside-dim { opacity: .68; font-size: 11px; }
101
+ #codex-ringside-tooltip .ringside-message { margin-top: 8px; }
102
+ #codex-ringside-tooltip .ringside-footer { margin-top: 10px; padding-top: 9px; border-top: 1px solid color-mix(in srgb, currentColor 10%, transparent); }
103
+ #codex-ringside-tooltip .ringside-meta { display: flex; flex-direction: column; gap: 6px; }
104
+ #codex-ringside-tooltip .ringside-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; }
105
+ #codex-ringside-tooltip .ringside-row[data-label]::before { content: attr(data-label); opacity: .68; font-size: 11px; }
106
+ #codex-ringside-tooltip .ringside-credit-balance { font-size: 14px; line-height: 18px; font-weight: 600; font-variant-numeric: tabular-nums; }
107
+ #codex-ringside-tooltip .ringside-meta-value { font-size: 12px; line-height: 16px; font-weight: 500; opacity: .9; font-variant-numeric: tabular-nums; }
108
+ #codex-ringside-tooltip .ringside-sr { font-size: 0; opacity: 0; }
109
+ #codex-ringside-tooltip .ringside-actions { display: flex; gap: 6px; }
110
+ #codex-ringside-tooltip button {
111
+ flex: 1; padding: 5px 10px; border: 1px solid color-mix(in srgb, currentColor 14%, transparent); border-radius: 6px;
112
+ font: 500 11.5px/1.35 -apple-system, BlinkMacSystemFont, sans-serif; text-align: center; white-space: nowrap;
113
+ color: inherit; background: color-mix(in srgb, currentColor 4%, transparent); cursor: pointer;
114
+ transition: background-color 120ms ease, border-color 120ms ease;
115
+ }
116
+ #codex-ringside-tooltip button:hover, #codex-ringside-tooltip button:focus-visible {
117
+ background: color-mix(in srgb, currentColor 11%, transparent);
118
+ border-color: color-mix(in srgb, currentColor 22%, transparent);
119
+ }
120
+ #codex-ringside-tooltip button:focus-visible { outline: 1px solid currentColor; outline-offset: 1px; }
89
121
  `;
90
122
 
91
123
  let value = { rings: [], message: '正在读取 Codex 剩余额度', note: null, stale: false, updatedAt: null };
92
124
  let rail = null;
125
+ let nativeTrigger = null;
126
+ let eventTarget = null;
93
127
  let placementTimer = null;
94
128
  let hoverTimer = null;
129
+ let hideTimer = null;
130
+ let suppressFocusTooltip = false;
131
+ let forwardingNativeAction = false;
95
132
  let disposed = false;
96
133
 
97
- const element = (className, text) => {
98
- const node = document.createElement('div');
134
+ const element = (className, text, tag = 'div') => {
135
+ const node = document.createElement(tag);
99
136
  node.className = className;
100
137
  if (text != null) node.textContent = text;
101
138
  return node;
@@ -106,6 +143,25 @@ function installCodexRingsideCard() {
106
143
  return rect.width > 0 && rect.height > 0 && getComputedStyle(node).visibility !== 'hidden';
107
144
  };
108
145
  const percentText = percent => (Number.isFinite(percent) ? `${percent}%` : '—');
146
+ const details = element('ringside-details');
147
+ const actions = element('ringside-actions ringside-footer');
148
+ const settingsButton = document.createElement('button');
149
+ settingsButton.type = 'button';
150
+ settingsButton.textContent = '背景设置';
151
+ settingsButton.addEventListener('click', togglePanel);
152
+ const usageButton = document.createElement('button');
153
+ usageButton.type = 'button';
154
+ usageButton.textContent = '用量菜单';
155
+ usageButton.setAttribute('aria-haspopup', 'menu');
156
+ usageButton.addEventListener('click', openUsageMenu);
157
+ actions.append(settingsButton, usageButton);
158
+ tooltip.append(details, actions);
159
+
160
+ function renderActions() {
161
+ settingsButton.hidden = !window.__codexRingsideBackground?.togglePanel;
162
+ usageButton.hidden = !nativeTrigger;
163
+ actions.hidden = settingsButton.hidden && usageButton.hidden;
164
+ }
109
165
 
110
166
  // Filled ticks run clockwise from 12 o'clock; the unused part of the circle stays a solid track.
111
167
  function ringSvg(percent, tone) {
@@ -151,19 +207,53 @@ function installCodexRingsideCard() {
151
207
  if (ring.resetsAt) section.append(element('ringside-dim', `${ring.resetsAt} 重置`));
152
208
  sections.append(section);
153
209
  }
154
- const lines = [element('ringside-title', 'Codex 剩余额度')];
210
+ const header = element('ringside-header');
211
+ header.append(element('ringside-title', 'Codex 剩余额度'));
212
+ if (value.plan) header.append(element('ringside-badge', `${value.plan} 套餐`));
213
+ const lines = [header];
155
214
  if (sections.childElementCount) lines.push(sections);
156
- if (value.message) lines.push(element('ringside-dim', value.message));
157
- if (value.note) {
158
- const footer = element('ringside-dim ringside-footer', value.note);
159
- lines.push(footer);
215
+ if (value.credits || value.note) {
216
+ const meta = element('ringside-meta ringside-footer');
217
+ if (value.credits) {
218
+ const credits = element('ringside-row');
219
+ credits.append(element('ringside-dim', '额度余额'), element('ringside-credit-balance', value.credits.label));
220
+ meta.append(credits);
221
+ }
222
+ if (value.note) {
223
+ const parts = /^([^::]+)([::]\s*)(.+)$/.exec(value.note);
224
+ if (parts) {
225
+ const note = element('ringside-row');
226
+ note.dataset.label = parts[1];
227
+ const val = element('ringside-meta-value', parts[3], 'span');
228
+ val.prepend(element('ringside-sr', `${parts[1]}${parts[2]}`, 'span'));
229
+ note.append(val);
230
+ meta.append(note);
231
+ } else {
232
+ meta.append(element('ringside-dim', value.note));
233
+ }
234
+ }
235
+ lines.push(meta);
160
236
  }
161
- tooltip.replaceChildren(...lines);
237
+ if (value.message) lines.push(element('ringside-dim ringside-message', value.message));
238
+ details.replaceChildren(...lines);
239
+ renderActions();
162
240
  }
163
241
 
164
242
  function render() {
165
- const rings = value.rings?.length ? value.rings : [{ label: '额度', name: '额度', percent: null, tone: 'muted' }];
166
- card.replaceChildren(...rings.map(buildMeter));
243
+ if (!value.rings?.length && value.credits) {
244
+ const balance = element('ringside-center');
245
+ const compact = value.credits.unlimited ? '∞' : new Intl.NumberFormat('zh-CN', {
246
+ notation: 'compact', maximumFractionDigits: 1
247
+ }).format(value.credits.balance);
248
+ balance.append(element('ringside-number', compact), element('ringside-label', '额度'));
249
+ const meter = element('ringside-meter');
250
+ meter.setAttribute('aria-label', `额度余额:${value.credits.label}`);
251
+ meter.append(balance);
252
+ card.replaceChildren(meter);
253
+ } else {
254
+ const rings = value.rings?.length ? value.rings : [{ label: '额度', name: '额度', percent: null, tone: 'muted' }];
255
+ card.replaceChildren(...rings.map(buildMeter));
256
+ }
167
257
  renderTooltip();
168
258
  if (!tooltip.hidden) positionTooltip();
169
259
  }
@@ -174,6 +264,9 @@ function installCodexRingsideCard() {
174
264
  value = {
175
265
  ...value,
176
266
  stale: true,
267
+ credits: null,
268
+ plan: null,
269
+ note: null,
177
270
  message: '额度数据已过期,正在等待重新连接',
178
271
  rings: (value.rings ?? []).map(ring => ({ ...ring, percent: null, tone: 'muted', resetsAt: null }))
179
272
  };
@@ -184,31 +277,121 @@ function installCodexRingsideCard() {
184
277
  const panelOpen = () => document.getElementById('codex-ringside-panel')?.hidden === false;
185
278
  function togglePanel() {
186
279
  hideTooltip();
187
- window.__codexRingsideBackground?.togglePanel(card);
280
+ window.__codexRingsideBackground?.togglePanel(nativeTrigger ?? card);
281
+ }
282
+
283
+ function openUsageMenu() {
284
+ if (!nativeTrigger?.isConnected) return;
285
+ if (panelOpen()) window.__codexRingsideBackground?.togglePanel(nativeTrigger);
286
+ hideTooltip();
287
+ // Current clients open on pointerdown; older triggers may open on click. Pass both
288
+ // through the untouched native handlers, only for this explicit menu action.
289
+ forwardingNativeAction = true;
290
+ try {
291
+ nativeTrigger.focus();
292
+ const pointer = { bubbles: true, cancelable: true, button: 0, pointerType: 'mouse', isPrimary: true };
293
+ nativeTrigger.dispatchEvent(new PointerEvent('pointerdown', { ...pointer, buttons: 1 }));
294
+ nativeTrigger.dispatchEvent(new PointerEvent('pointerup', { ...pointer, buttons: 0 }));
295
+ nativeTrigger.click();
296
+ } finally {
297
+ forwardingNativeAction = false;
298
+ hideTooltip();
299
+ }
188
300
  }
189
301
 
190
302
  function positionTooltip() {
191
- const rect = card.getBoundingClientRect();
303
+ const rect = (nativeTrigger ?? card).getBoundingClientRect();
192
304
  tooltip.style.left = `${Math.max(8, Math.min(rect.right + 12, innerWidth - tooltip.offsetWidth - 8))}px`;
193
305
  const top = rect.top + rect.height / 2 - tooltip.offsetHeight / 2;
194
306
  tooltip.style.top = `${Math.max(8, Math.min(top, innerHeight - tooltip.offsetHeight - 8))}px`;
195
307
  }
196
308
  function hideTooltip() {
197
309
  clearTimeout(hoverTimer);
310
+ clearTimeout(hideTimer);
198
311
  hoverTimer = null;
199
312
  tooltip.hidden = true;
200
313
  }
201
314
  function showTooltip() {
202
315
  clearTimeout(hoverTimer);
203
- if (disposed || document.hidden || !visible(card) || panelOpen()) return;
316
+ clearTimeout(hideTimer);
317
+ if (disposed || suppressFocusTooltip || document.hidden || !visible(card) || panelOpen()
318
+ || nativeTrigger?.getAttribute('aria-expanded') === 'true') return;
319
+ renderActions();
204
320
  tooltip.hidden = false;
205
321
  positionTooltip();
206
322
  }
207
323
  function scheduleTooltip() {
208
324
  clearTimeout(hoverTimer);
325
+ clearTimeout(hideTimer);
209
326
  hoverTimer = setTimeout(showTooltip, 350);
210
327
  }
211
328
 
329
+ function scheduleHide() {
330
+ clearTimeout(hoverTimer);
331
+ clearTimeout(hideTimer);
332
+ hideTimer = setTimeout(() => {
333
+ if (!tooltip.contains(document.activeElement)) hideTooltip();
334
+ }, 180);
335
+ }
336
+
337
+ // Exact accessible labels cover old builds; the indicator + description covers other locales.
338
+ // Never select the profile menu or a navigation destination merely because it has an SVG.
339
+ function findNativeTrigger(container) {
340
+ return [...(container?.querySelectorAll('button[aria-haspopup="menu"]') ?? [])].find(button =>
341
+ visible(button) && !button.hasAttribute('data-sidebar-destination') && (
342
+ /^(usage|用量|使用量)$/i.test(button.getAttribute('aria-label')?.trim() ?? '')
343
+ || (button.hasAttribute('aria-description') && button.querySelector('[data-transition-phase]'))
344
+ )) ?? null;
345
+ }
346
+
347
+ const suppressNativeTooltip = event => event.stopPropagation();
348
+ const onBlur = event => {
349
+ if (!tooltip.contains(event.relatedTarget) && !eventTarget?.contains(event.relatedTarget)) hideTooltip();
350
+ };
351
+ const interceptBackground = event => {
352
+ if (forwardingNativeAction || !window.__codexRingsideBackground?.togglePanel) return false;
353
+ event.preventDefault();
354
+ event.stopPropagation();
355
+ return true;
356
+ };
357
+ const onPointerDown = event => {
358
+ // Radix opens before click. Stop the left pointerdown as well as the click so
359
+ // the original menu cannot steal focus or cover the background settings.
360
+ if (event.button === 0 && interceptBackground(event)) eventTarget?.focus();
361
+ };
362
+ const onClick = event => {
363
+ if (interceptBackground(event)) togglePanel();
364
+ else hideTooltip();
365
+ };
366
+ const onContextMenu = event => {
367
+ if (!window.__codexRingsideBackground?.togglePanel) return;
368
+ event.preventDefault();
369
+ event.stopPropagation();
370
+ togglePanel();
371
+ };
372
+ const onKeyDown = event => {
373
+ if (event.key === 'ContextMenu' || (event.shiftKey && event.key === 'F10')) return onContextMenu(event);
374
+ if (!['Enter', ' ', 'ArrowDown'].includes(event.key)) return;
375
+ if (event.key === 'ArrowDown') {
376
+ if (nativeTrigger && panelOpen()) window.__codexRingsideBackground?.togglePanel(nativeTrigger);
377
+ hideTooltip(); // ArrowDown still reaches the native usage-menu handler.
378
+ } else if (interceptBackground(event)) {
379
+ if (!event.repeat) togglePanel();
380
+ }
381
+ };
382
+ function bindTarget(target) {
383
+ if (eventTarget === target) return;
384
+ const listeners = { mouseenter: scheduleTooltip, mouseleave: scheduleHide, focus: showTooltip,
385
+ blur: onBlur, pointerdown: onPointerDown, keydown: onKeyDown, click: onClick, contextmenu: onContextMenu,
386
+ pointerover: suppressNativeTooltip, pointermove: suppressNativeTooltip, focusin: suppressNativeTooltip };
387
+ for (const [name, handler] of Object.entries(listeners)) {
388
+ const capture = ['pointerdown', 'keydown', 'click', 'contextmenu'].includes(name);
389
+ eventTarget?.removeEventListener(name, handler, capture);
390
+ target?.addEventListener(name, handler, capture);
391
+ }
392
+ eventTarget = target;
393
+ }
394
+
212
395
  function place() {
213
396
  if (disposed || !document.body) return;
214
397
  if (!style.isConnected) (document.head ?? document.documentElement).appendChild(style);
@@ -219,6 +402,28 @@ function installCodexRingsideCard() {
219
402
  rail = nextRail;
220
403
  if (rail) resizeObserver.observe(rail);
221
404
  }
405
+ const nextTrigger = findNativeTrigger(rail);
406
+ if (nativeTrigger !== nextTrigger) {
407
+ nativeTrigger?.removeAttribute('data-codex-ringside-host');
408
+ nativeTrigger = nextTrigger;
409
+ hideTooltip();
410
+ }
411
+ if (nativeTrigger) {
412
+ if (!nativeTrigger.hasAttribute('data-codex-ringside-host')) nativeTrigger.setAttribute('data-codex-ringside-host', '');
413
+ card.removeAttribute('tabindex');
414
+ card.setAttribute('role', 'group');
415
+ card.removeAttribute('aria-haspopup');
416
+ card.hidden = false;
417
+ if (card.parentElement !== nativeTrigger) nativeTrigger.append(card);
418
+ bindTarget(nativeTrigger);
419
+ if (nativeTrigger.getAttribute('aria-expanded') === 'true') hideTooltip();
420
+ else if (!tooltip.hidden) positionTooltip();
421
+ return;
422
+ }
423
+ card.tabIndex = 0;
424
+ card.setAttribute('role', 'button');
425
+ card.setAttribute('aria-haspopup', 'dialog');
426
+ bindTarget(card);
222
427
  // Own a sibling just before the rail's footer; never move React-owned nodes.
223
428
  const footer = rail && [...rail.children]
224
429
  .filter(child => child !== card && visible(child) && getComputedStyle(child).position !== 'absolute')
@@ -245,20 +450,26 @@ function installCodexRingsideCard() {
245
450
  if (records.some(record => !ownNode(record.target))) schedulePlacement();
246
451
  });
247
452
  const resizeObserver = new ResizeObserver(schedulePlacement);
248
- const onKeyDown = event => {
249
- if (event.key === 'Escape') hideTooltip();
250
- if (event.key !== 'Enter' && event.key !== ' ') return;
251
- event.preventDefault();
252
- togglePanel();
453
+ const onEscape = event => {
454
+ if (event.key !== 'Escape' || tooltip.hidden) return;
455
+ if (tooltip.contains(document.activeElement)) {
456
+ suppressFocusTooltip = true;
457
+ eventTarget?.focus();
458
+ suppressFocusTooltip = false;
459
+ }
460
+ hideTooltip();
461
+ };
462
+ const onOutsidePointer = event => {
463
+ if (!tooltip.contains(event.target)) hideTooltip();
253
464
  };
254
465
 
255
- observer.observe(document.documentElement, { childList: true, subtree: true });
256
- card.addEventListener('mouseenter', scheduleTooltip);
257
- card.addEventListener('mouseleave', hideTooltip);
258
- card.addEventListener('focus', showTooltip);
259
- card.addEventListener('blur', hideTooltip);
260
- card.addEventListener('keydown', onKeyDown);
261
- card.addEventListener('click', togglePanel);
466
+ observer.observe(document.documentElement, { childList: true, subtree: true, attributes: true,
467
+ attributeFilter: ['aria-expanded', 'aria-label', 'aria-description', 'hidden'] });
468
+ tooltip.addEventListener('mouseenter', () => clearTimeout(hideTimer));
469
+ tooltip.addEventListener('mouseleave', scheduleHide);
470
+ tooltip.addEventListener('focusout', onBlur);
471
+ document.addEventListener('keydown', onEscape);
472
+ document.addEventListener('pointerdown', onOutsidePointer, true);
262
473
  window.addEventListener('resize', schedulePlacement);
263
474
  document.addEventListener('visibilitychange', hideTooltip);
264
475
 
@@ -274,7 +485,10 @@ function installCodexRingsideCard() {
274
485
  status() {
275
486
  return {
276
487
  version: VERSION,
277
- placed: card.parentElement === rail && !card.hidden && visible(card),
488
+ placed: rail?.contains(card) === true && !card.hidden && visible(card),
489
+ replacesNative: nativeTrigger?.contains(card) === true,
490
+ credits: value.credits ?? null,
491
+ plan: value.plan ?? null,
278
492
  rings: value.rings,
279
493
  message: value.message,
280
494
  stale: value.stale ?? false,
@@ -289,6 +503,10 @@ function installCodexRingsideCard() {
289
503
  clearTimeout(placementTimer);
290
504
  clearInterval(freshnessTimer);
291
505
  hideTooltip();
506
+ bindTarget(null);
507
+ nativeTrigger?.removeAttribute('data-codex-ringside-host');
508
+ document.removeEventListener('keydown', onEscape);
509
+ document.removeEventListener('pointerdown', onOutsidePointer, true);
292
510
  window.removeEventListener('resize', schedulePlacement);
293
511
  document.removeEventListener('visibilitychange', hideTooltip);
294
512
  card.remove();
package/src/install.cjs CHANGED
@@ -6,6 +6,7 @@ const { execFileSync } = require('node:child_process');
6
6
  const launchd = require('./launchd.cjs');
7
7
  const { DEFAULT_APP, isExecutable, resolveCodexBin, resolveNode } = require('./resolve.cjs');
8
8
  const { inspectBundle, copyBundle } = require('./standalone.cjs');
9
+ const { normalizeBackground } = require('./background.cjs');
9
10
 
10
11
  const SWIFTC = '/usr/bin/swiftc';
11
12
  const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
@@ -116,7 +117,8 @@ function copyUserData(source, stage, destination) {
116
117
  if (typeof config?.image === 'string') {
117
118
  const relative = path.relative(source, config.image);
118
119
  if (relative && !relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative)) {
119
- const name = path.join('user-images', `imported${path.extname(config.image)}`);
120
+ const name = relative.startsWith(`user-images${path.sep}`)
121
+ ? relative : path.join('user-images', `imported${path.extname(config.image)}`);
120
122
  copyIfPresent(config.image, path.join(stage, name));
121
123
  config.image = path.join(destination, name);
122
124
  writeJson(file, config);
@@ -124,6 +126,16 @@ function copyUserData(source, stage, destination) {
124
126
  }
125
127
  }
126
128
 
129
+ function seedBackground(stage, destination) {
130
+ const presets = path.join(stage, 'src/presets');
131
+ const preset = readJson(path.join(presets, 'default-background.json'));
132
+ if (preset?.source !== 'image' || preset.image !== 'wallpaper.png') throw new Error('安装包缺少有效的默认背景预设');
133
+ const image = path.join('user-images', 'default-background.png');
134
+ fs.mkdirSync(path.join(stage, 'user-images'), { recursive: true, mode: 0o700 });
135
+ fs.copyFileSync(path.join(presets, preset.image), path.join(stage, image));
136
+ writeJson(path.join(stage, 'background.json'), normalizeBackground({ ...preset, image: path.join(destination, image) }));
137
+ }
138
+
127
139
  async function install(options = {}) {
128
140
  const { current, legacy, backupRoot, repoRoot, services } = { ...defaults, ...options };
129
141
  return withLock(current, async () => {
@@ -146,6 +158,9 @@ async function install(options = {}) {
146
158
  for (const name of ['package.json', 'LICENSE', 'NOTICE']) fs.copyFileSync(path.join(repoRoot, name), path.join(stage, name));
147
159
  if (checked.bundle) copyBundle(repoRoot, stage);
148
160
  else await (options.compile ?? compile)(repoRoot, stage);
161
+ // Older installations may have no background.json because they never enabled a background.
162
+ // Only a genuinely new install gets the preset; upgrading must retain that implicit opt-out.
163
+ if (!locations.some(location => fs.existsSync(location.installDir))) seedBackground(stage, current.installDir);
149
164
  options.beforeInstall?.();
150
165
  fs.mkdirSync(backupRoot, { recursive: true, mode: 0o700 });
151
166
  backup = fs.mkdtempSync(path.join(backupRoot, 'install-'));
@@ -156,7 +171,8 @@ async function install(options = {}) {
156
171
  for (const location of locations) await services.bootout(location.label);
157
172
 
158
173
  // Copy after stopping writers. Existing Ringside data takes precedence over legacy data.
159
- const dataRoot = fs.existsSync(path.join(current.installDir, 'settings.json')) ? current.installDir : legacy.installDir;
174
+ const hasCurrentData = ['settings.json', 'background.json'].some(name => fs.existsSync(path.join(current.installDir, name)));
175
+ const dataRoot = hasCurrentData ? current.installDir : legacy.installDir;
160
176
  copyUserData(dataRoot, stage, current.installDir);
161
177
  const cooldown = Math.max(...locations.map(location => {
162
178
  const value = readJson(path.join(location.installDir, 'state.json'))?.lastAttemptAt;
package/src/limits.cjs CHANGED
@@ -70,6 +70,8 @@ function mergeSnapshot(current, patch) {
70
70
  return {
71
71
  ...base,
72
72
  ...patch,
73
+ credits: patch.credits === undefined ? base?.credits ?? null
74
+ : isRecord(patch.credits) ? { ...base?.credits, ...patch.credits } : null,
73
75
  primary: mergeWindow(base?.primary, patch.primary),
74
76
  secondary: mergeWindow(base?.secondary, patch.secondary)
75
77
  };
@@ -81,7 +83,10 @@ function mergeResponse(current, patch) {
81
83
  if (!isRecord(patch)) return null;
82
84
  const changedAccount = hasField(patch, 'accountId') && hasField(current, 'accountId')
83
85
  && patch.accountId !== current.accountId;
84
- const base = changedAccount ? {} : current ?? {};
86
+ const oldPlan = pickCodexSnapshot(current)?.planType;
87
+ const newPlan = pickCodexSnapshot(patch)?.planType;
88
+ const changedPlan = oldPlan && newPlan && oldPlan !== newPlan;
89
+ const base = changedAccount || changedPlan ? {} : current ?? {};
85
90
  const merged = { ...base, ...patch };
86
91
  merged.rateLimits = base.rateLimits ?? null;
87
92
  if (isRecord(base.rateLimitsByLimitId)) merged.rateLimitsByLimitId = base.rateLimitsByLimitId;
@@ -122,13 +127,33 @@ function formatUsage(response) {
122
127
  const percent = remainingPercent(window.usedPercent);
123
128
  return { label: labels.short, name: labels.long, percent, tone: tone(percent), resetsAt: formatResetTime(window.resetsAt) };
124
129
  });
130
+ const credits = formatCredits(snapshot?.credits);
125
131
  return {
126
132
  rings,
127
- message: rings.length ? null : '暂时无法读取 Codex 剩余额度',
133
+ plan: formatPlan(snapshot?.planType),
134
+ credits,
135
+ message: rings.length || credits ? null : '暂时无法读取 Codex 剩余额度',
128
136
  note: resetCreditsNote(response)
129
137
  };
130
138
  }
131
139
 
140
+ function formatPlan(plan) {
141
+ if (typeof plan !== 'string' || !plan.trim()) return null;
142
+ const labels = { free: 'Free', go: 'Go', plus: 'Plus', pro: 'Pro', team: 'Team', business: 'Business',
143
+ enterprise: 'Enterprise', edu: 'Edu' };
144
+ return Object.hasOwn(labels, plan) ? labels[plan] : plan;
145
+ }
146
+
147
+ function formatCredits(credits) {
148
+ if (!isRecord(credits)) return null;
149
+ if (credits.unlimited === true) return { unlimited: true, balance: null, label: '不限额' };
150
+ const raw = credits.balance;
151
+ if (typeof raw !== 'number' && (typeof raw !== 'string' || !raw.trim())) return null;
152
+ const balance = Number(raw);
153
+ if (!Number.isFinite(balance) || balance < 0) return null;
154
+ return { unlimited: false, balance, label: new Intl.NumberFormat('zh-CN', { maximumFractionDigits: 0 }).format(balance) };
155
+ }
156
+
132
157
  function resetCreditsNote(response) {
133
158
  const count = Number(response?.rateLimitResetCredits?.availableCount);
134
159
  return Number.isFinite(count) && count > 0 ? `重置卡:${Math.floor(count)} 张可用` : null;
@@ -139,6 +164,9 @@ function unavailable(value, message = '暂时无法读取 Codex 剩余额度,
139
164
  ...value,
140
165
  message,
141
166
  stale: true,
167
+ credits: null,
168
+ plan: null,
169
+ note: null,
142
170
  rings: (value?.rings ?? []).map(ring => ({ ...ring, percent: null, tone: 'muted', resetsAt: null }))
143
171
  };
144
172
  }
@@ -0,0 +1,14 @@
1
+ {
2
+ "source": "image",
3
+ "image": "wallpaper.png",
4
+ "transparency": 0,
5
+ "blur": 0,
6
+ "zoom": 134,
7
+ "x": 0,
8
+ "y": 0,
9
+ "brightness": 100,
10
+ "contrast": 100,
11
+ "saturation": 100,
12
+ "layout": "cover",
13
+ "crop": null
14
+ }
Binary file