codex-ringside 0.1.1 → 0.3.0
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 +16 -0
- package/README.md +82 -47
- package/README.zh-CN.md +90 -48
- package/bin/codex-ringside.cjs +23 -1
- package/native/bridge.swift +4 -0
- package/package.json +9 -3
- package/src/agent.cjs +3 -0
- package/src/card.js +258 -40
- package/src/install.cjs +18 -6
- package/src/launchd.cjs +15 -1
- package/src/limits.cjs +30 -2
- package/src/standalone.cjs +30 -0
- package/src/update.cjs +271 -0
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
|
|
|
@@ -151,6 +152,7 @@ function loadSettings(root) {
|
|
|
151
152
|
|
|
152
153
|
async function main() {
|
|
153
154
|
const { startWatcher } = require('./startup/watch.cjs');
|
|
155
|
+
const { startAutoUpdater } = require('./update.cjs');
|
|
154
156
|
const root = path.resolve(__dirname, '..');
|
|
155
157
|
const settings = loadSettings(root);
|
|
156
158
|
const abort = new AbortController();
|
|
@@ -165,6 +167,7 @@ async function main() {
|
|
|
165
167
|
process.once('SIGINT', shutdown);
|
|
166
168
|
process.once('SIGTERM', shutdown);
|
|
167
169
|
|
|
170
|
+
startAutoUpdater({ root, signal: abort.signal, log });
|
|
168
171
|
await runAgent({ appPath: settings.app, root, signal: abort.signal });
|
|
169
172
|
await watcher.exited;
|
|
170
173
|
}
|
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 =
|
|
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', '
|
|
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', '
|
|
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:
|
|
71
|
-
padding: 12px 14px; border: 1px solid color-mix(in srgb, currentColor 12%, transparent); border-radius:
|
|
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
|
|
74
|
-
|
|
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-
|
|
85
|
-
#codex-ringside-tooltip .ringside-
|
|
86
|
-
#codex-ringside-tooltip .ringside-
|
|
87
|
-
|
|
88
|
-
|
|
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(
|
|
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
|
|
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.
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
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
|
-
|
|
166
|
-
|
|
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
|
-
|
|
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
|
|
249
|
-
if (event.key
|
|
250
|
-
if (
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
|
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
|
@@ -5,6 +5,7 @@ const path = require('node:path');
|
|
|
5
5
|
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
|
+
const { inspectBundle, copyBundle } = require('./standalone.cjs');
|
|
8
9
|
|
|
9
10
|
const SWIFTC = '/usr/bin/swiftc';
|
|
10
11
|
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
|
|
@@ -29,10 +30,12 @@ function writeJson(file, data) {
|
|
|
29
30
|
fs.writeFileSync(file, `${JSON.stringify(data, null, 2)}\n`, { mode: 0o600 });
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
function preflight(settings) {
|
|
33
|
+
function preflight(settings, repoRoot, current) {
|
|
33
34
|
if (process.platform !== 'darwin') throw new Error('只支持 macOS');
|
|
34
35
|
const app = fs.realpathSync(settings.app);
|
|
35
36
|
resolveCodexBin(app);
|
|
37
|
+
const bundle = inspectBundle(repoRoot);
|
|
38
|
+
if (bundle) return { app, node: path.join(current.installDir, 'runtime/node'), bundle };
|
|
36
39
|
if (!isExecutable(SWIFTC)) throw new Error('需要 Xcode Command Line Tools:xcode-select --install');
|
|
37
40
|
return { app, node: resolveNode() };
|
|
38
41
|
}
|
|
@@ -98,12 +101,13 @@ function selectSettings(current, legacy, env, observe) {
|
|
|
98
101
|
?? readJson(path.join(legacy.installDir, 'settings.json')) ?? {};
|
|
99
102
|
return {
|
|
100
103
|
app: env.CODEX_RINGSIDE_APP || env.CODEX_PLUS_APP || saved.app || DEFAULT_APP,
|
|
101
|
-
allowClientRestart: observe === undefined ? saved.allowClientRestart !== false : !observe
|
|
104
|
+
allowClientRestart: observe === undefined ? saved.allowClientRestart !== false : !observe,
|
|
105
|
+
autoUpdate: saved.autoUpdate !== false
|
|
102
106
|
};
|
|
103
107
|
}
|
|
104
108
|
|
|
105
109
|
function copyUserData(source, stage, destination) {
|
|
106
|
-
for (const name of ['background.json', 'background', 'user-images']) {
|
|
110
|
+
for (const name of ['background.json', 'background', 'user-images', 'update-state.json']) {
|
|
107
111
|
copyIfPresent(path.join(source, name), path.join(stage, name));
|
|
108
112
|
}
|
|
109
113
|
const file = path.join(stage, 'background.json');
|
|
@@ -123,8 +127,10 @@ function copyUserData(source, stage, destination) {
|
|
|
123
127
|
async function install(options = {}) {
|
|
124
128
|
const { current, legacy, backupRoot, repoRoot, services } = { ...defaults, ...options };
|
|
125
129
|
return withLock(current, async () => {
|
|
130
|
+
// Recheck under the install lock: an update downloaded before uninstall/reinstall is stale.
|
|
131
|
+
options.beforeInstall?.();
|
|
126
132
|
const settings = selectSettings(current, legacy, options.env ?? process.env, options.observe);
|
|
127
|
-
const checked = await (options.preflight ?? preflight)(settings);
|
|
133
|
+
const checked = await (options.preflight ?? preflight)(settings, repoRoot, current);
|
|
128
134
|
settings.app = checked.app;
|
|
129
135
|
const stage = fs.mkdtempSync(`${current.installDir}.staging-`);
|
|
130
136
|
const locations = [current, legacy];
|
|
@@ -134,8 +140,13 @@ async function install(options = {}) {
|
|
|
134
140
|
let touched = false;
|
|
135
141
|
try {
|
|
136
142
|
fs.cpSync(path.join(repoRoot, 'src'), path.join(stage, 'src'), { recursive: true });
|
|
143
|
+
fs.mkdirSync(path.join(stage, 'bin'));
|
|
144
|
+
fs.copyFileSync(path.join(repoRoot, 'bin/codex-ringside.cjs'), path.join(stage, 'bin/codex-ringside.cjs'));
|
|
145
|
+
if (checked.bundle) fs.copyFileSync(path.join(repoRoot, 'bin/codex-ringside'), path.join(stage, 'bin/codex-ringside'));
|
|
137
146
|
for (const name of ['package.json', 'LICENSE', 'NOTICE']) fs.copyFileSync(path.join(repoRoot, name), path.join(stage, name));
|
|
138
|
-
|
|
147
|
+
if (checked.bundle) copyBundle(repoRoot, stage);
|
|
148
|
+
else await (options.compile ?? compile)(repoRoot, stage);
|
|
149
|
+
options.beforeInstall?.();
|
|
139
150
|
fs.mkdirSync(backupRoot, { recursive: true, mode: 0o700 });
|
|
140
151
|
backup = fs.mkdtempSync(path.join(backupRoot, 'install-'));
|
|
141
152
|
snapshots = capture(locations, backup);
|
|
@@ -194,6 +205,7 @@ async function install(options = {}) {
|
|
|
194
205
|
async function uninstall(options = {}) {
|
|
195
206
|
const { current, legacy, trashRoot, services } = { ...defaults, ...options };
|
|
196
207
|
return withLock(current, async () => {
|
|
208
|
+
await services.bootout(launchd.UPDATE_LABEL);
|
|
197
209
|
const locations = [current, legacy];
|
|
198
210
|
const running = locations.map(location => !!services.printJob(location.label));
|
|
199
211
|
const items = locations.flatMap((location, i) => launchd.trashItems(location)
|
|
@@ -219,4 +231,4 @@ async function uninstall(options = {}) {
|
|
|
219
231
|
});
|
|
220
232
|
}
|
|
221
233
|
|
|
222
|
-
module.exports = { install, uninstall, selectSettings };
|
|
234
|
+
module.exports = { install, uninstall, selectSettings, withLock };
|
package/src/launchd.cjs
CHANGED
|
@@ -5,6 +5,7 @@ const path = require('node:path');
|
|
|
5
5
|
const { execFileSync } = require('node:child_process');
|
|
6
6
|
|
|
7
7
|
const LABEL = 'com.codexringside.agent';
|
|
8
|
+
const UPDATE_LABEL = 'com.codexringside.update';
|
|
8
9
|
const home = os.homedir();
|
|
9
10
|
const paths = {
|
|
10
11
|
installDir: path.join(home, 'Library/Application Support/CodexRingside'),
|
|
@@ -59,6 +60,19 @@ function agentConfig({ node, installDir = paths.installDir, logDir = paths.logDi
|
|
|
59
60
|
};
|
|
60
61
|
}
|
|
61
62
|
|
|
63
|
+
// An independent, one-shot job survives bootout of the agent it is upgrading.
|
|
64
|
+
function updateConfig({ node, installDir = paths.installDir, logDir = paths.logDir }) {
|
|
65
|
+
return {
|
|
66
|
+
Label: UPDATE_LABEL,
|
|
67
|
+
ProgramArguments: [node, path.join(installDir, 'src/update.cjs'), '--auto'],
|
|
68
|
+
RunAtLoad: true,
|
|
69
|
+
ProcessType: 'Background',
|
|
70
|
+
EnvironmentVariables: { NODE_OPTIONS: '', NODE_PATH: '' },
|
|
71
|
+
StandardOutPath: path.join(logDir, 'update.log'),
|
|
72
|
+
StandardErrorPath: path.join(logDir, 'update.err.log')
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
62
76
|
const launchctl = (...args) => execFileSync('/bin/launchctl', args, {
|
|
63
77
|
encoding: 'utf8', timeout: 15000, stdio: ['ignore', 'pipe', 'pipe']
|
|
64
78
|
});
|
|
@@ -88,4 +102,4 @@ function writePlist(config, file = paths.plist) {
|
|
|
88
102
|
execFileSync('/usr/bin/plutil', ['-lint', file], { stdio: 'ignore' });
|
|
89
103
|
}
|
|
90
104
|
|
|
91
|
-
module.exports = { LABEL, LEGACY_LABEL, legacyPaths, agentConfig, bootout, bootstrap, paths, plistXml, printJob, trashItems, writePlist };
|
|
105
|
+
module.exports = { LABEL, UPDATE_LABEL, LEGACY_LABEL, legacyPaths, agentConfig, updateConfig, bootout, bootstrap, paths, plistXml, printJob, trashItems, writePlist };
|
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
|
|
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
|
-
|
|
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
|
}
|