oc-codex-multi-auth 6.21.0 → 6.22.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/LICENSE +21 -21
- package/README.md +729 -571
- package/assets/icon.svg +7 -7
- package/assets/opencode-logo-ornate-dark.svg +18 -18
- package/assets/readme-hero.svg +31 -31
- package/config/README.md +177 -177
- package/config/minimal-opencode.json +14 -14
- package/config/opencode-legacy.json +1496 -1496
- package/config/opencode-modern.json +555 -555
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +291 -53
- package/dist/index.js.map +1 -1
- package/dist/lib/account-display.d.ts +46 -0
- package/dist/lib/account-display.d.ts.map +1 -1
- package/dist/lib/account-display.js +260 -0
- package/dist/lib/account-display.js.map +1 -1
- package/dist/lib/accounts.d.ts +4 -0
- package/dist/lib/accounts.d.ts.map +1 -1
- package/dist/lib/accounts.js +20 -15
- package/dist/lib/accounts.js.map +1 -1
- package/dist/lib/auth/login-runner.d.ts.map +1 -1
- package/dist/lib/auth/login-runner.js +79 -2
- package/dist/lib/auth/login-runner.js.map +1 -1
- package/dist/lib/auto-update-checker.d.ts +17 -1
- package/dist/lib/auto-update-checker.d.ts.map +1 -1
- package/dist/lib/auto-update-checker.js +49 -3
- package/dist/lib/auto-update-checker.js.map +1 -1
- package/dist/lib/cli.d.ts +1 -0
- package/dist/lib/cli.d.ts.map +1 -1
- package/dist/lib/cli.js +9 -3
- package/dist/lib/cli.js.map +1 -1
- package/dist/lib/codex-usage.d.ts +4 -3
- package/dist/lib/codex-usage.d.ts.map +1 -1
- package/dist/lib/codex-usage.js +13 -11
- package/dist/lib/codex-usage.js.map +1 -1
- package/dist/lib/config.d.ts +75 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +152 -1
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/context-overflow.js +10 -10
- package/dist/lib/desktop-notifications.js +6 -6
- package/dist/lib/oauth-success.js +202 -202
- package/dist/lib/plan-allotment.d.ts +72 -0
- package/dist/lib/plan-allotment.d.ts.map +1 -0
- package/dist/lib/plan-allotment.js +142 -0
- package/dist/lib/plan-allotment.js.map +1 -0
- package/dist/lib/plugin-origin.d.ts +77 -0
- package/dist/lib/plugin-origin.d.ts.map +1 -0
- package/dist/lib/plugin-origin.js +301 -0
- package/dist/lib/plugin-origin.js.map +1 -0
- package/dist/lib/prompts/codex-opencode-bridge.js +67 -67
- package/dist/lib/prompts/codex.js +75 -75
- package/dist/lib/quota-display.d.ts +40 -0
- package/dist/lib/quota-display.d.ts.map +1 -0
- package/dist/lib/quota-display.js +45 -0
- package/dist/lib/quota-display.js.map +1 -0
- package/dist/lib/quota-notifications.d.ts +2 -1
- package/dist/lib/quota-notifications.d.ts.map +1 -1
- package/dist/lib/quota-notifications.js +34 -14
- package/dist/lib/quota-notifications.js.map +1 -1
- package/dist/lib/quota-overview.d.ts +253 -0
- package/dist/lib/quota-overview.d.ts.map +1 -0
- package/dist/lib/quota-overview.js +707 -0
- package/dist/lib/quota-overview.js.map +1 -0
- package/dist/lib/request/retry-budget.d.ts +26 -0
- package/dist/lib/request/retry-budget.d.ts.map +1 -1
- package/dist/lib/request/retry-budget.js +43 -0
- package/dist/lib/request/retry-budget.js.map +1 -1
- package/dist/lib/schemas.d.ts +67 -0
- package/dist/lib/schemas.d.ts.map +1 -1
- package/dist/lib/schemas.js +53 -0
- package/dist/lib/schemas.js.map +1 -1
- package/dist/lib/storage/backup.d.ts +33 -0
- package/dist/lib/storage/backup.d.ts.map +1 -1
- package/dist/lib/storage/backup.js +43 -7
- package/dist/lib/storage/backup.js.map +1 -1
- package/dist/lib/storage/credential-snapshots.d.ts +78 -0
- package/dist/lib/storage/credential-snapshots.d.ts.map +1 -0
- package/dist/lib/storage/credential-snapshots.js +302 -0
- package/dist/lib/storage/credential-snapshots.js.map +1 -0
- package/dist/lib/storage/export-import.d.ts.map +1 -1
- package/dist/lib/storage/export-import.js +7 -0
- package/dist/lib/storage/export-import.js.map +1 -1
- package/dist/lib/storage/flagged.d.ts.map +1 -1
- package/dist/lib/storage/flagged.js +23 -1
- package/dist/lib/storage/flagged.js.map +1 -1
- package/dist/lib/storage/load-save.d.ts +4 -0
- package/dist/lib/storage/load-save.d.ts.map +1 -1
- package/dist/lib/storage/load-save.js +50 -0
- package/dist/lib/storage/load-save.js.map +1 -1
- package/dist/lib/storage/paths.d.ts +1 -0
- package/dist/lib/storage/paths.d.ts.map +1 -1
- package/dist/lib/storage/paths.js +1 -1
- package/dist/lib/storage/paths.js.map +1 -1
- package/dist/lib/storage/test-home-guard.d.ts +35 -0
- package/dist/lib/storage/test-home-guard.d.ts.map +1 -0
- package/dist/lib/storage/test-home-guard.js +59 -0
- package/dist/lib/storage/test-home-guard.js.map +1 -0
- package/dist/lib/tools/codex-dashboard.d.ts.map +1 -1
- package/dist/lib/tools/codex-dashboard.js +3 -2
- package/dist/lib/tools/codex-dashboard.js.map +1 -1
- package/dist/lib/tools/codex-doctor.d.ts.map +1 -1
- package/dist/lib/tools/codex-doctor.js +16 -0
- package/dist/lib/tools/codex-doctor.js.map +1 -1
- package/dist/lib/tools/codex-health.d.ts.map +1 -1
- package/dist/lib/tools/codex-health.js +10 -2
- package/dist/lib/tools/codex-health.js.map +1 -1
- package/dist/lib/tools/codex-label.d.ts.map +1 -1
- package/dist/lib/tools/codex-label.js +1 -0
- package/dist/lib/tools/codex-label.js.map +1 -1
- package/dist/lib/tools/codex-limits.d.ts.map +1 -1
- package/dist/lib/tools/codex-limits.js +13 -9
- package/dist/lib/tools/codex-limits.js.map +1 -1
- package/dist/lib/tools/codex-list.d.ts.map +1 -1
- package/dist/lib/tools/codex-list.js +28 -3
- package/dist/lib/tools/codex-list.js.map +1 -1
- package/dist/lib/tools/codex-note.d.ts.map +1 -1
- package/dist/lib/tools/codex-note.js +4 -1
- package/dist/lib/tools/codex-note.js.map +1 -1
- package/dist/lib/tools/codex-pool.d.ts.map +1 -1
- package/dist/lib/tools/codex-pool.js +6 -2
- package/dist/lib/tools/codex-pool.js.map +1 -1
- package/dist/lib/tools/codex-refresh.d.ts.map +1 -1
- package/dist/lib/tools/codex-refresh.js +4 -1
- package/dist/lib/tools/codex-refresh.js.map +1 -1
- package/dist/lib/tools/codex-remove.d.ts.map +1 -1
- package/dist/lib/tools/codex-remove.js +1 -0
- package/dist/lib/tools/codex-remove.js.map +1 -1
- package/dist/lib/tools/codex-reset.d.ts.map +1 -1
- package/dist/lib/tools/codex-reset.js +13 -7
- package/dist/lib/tools/codex-reset.js.map +1 -1
- package/dist/lib/tools/codex-status.d.ts.map +1 -1
- package/dist/lib/tools/codex-status.js +25 -2
- package/dist/lib/tools/codex-status.js.map +1 -1
- package/dist/lib/tools/codex-switch.d.ts.map +1 -1
- package/dist/lib/tools/codex-switch.js +1 -0
- package/dist/lib/tools/codex-switch.js.map +1 -1
- package/dist/lib/tools/codex-tag.d.ts.map +1 -1
- package/dist/lib/tools/codex-tag.js +4 -1
- package/dist/lib/tools/codex-tag.js.map +1 -1
- package/dist/lib/tools/codex-warm.d.ts.map +1 -1
- package/dist/lib/tools/codex-warm.js +1 -0
- package/dist/lib/tools/codex-warm.js.map +1 -1
- package/dist/lib/tools/index.d.ts +9 -0
- package/dist/lib/tools/index.d.ts.map +1 -1
- package/dist/lib/tools/index.js.map +1 -1
- package/dist/lib/tui-quota-cache.d.ts +35 -0
- package/dist/lib/tui-quota-cache.d.ts.map +1 -1
- package/dist/lib/tui-quota-cache.js +76 -11
- package/dist/lib/tui-quota-cache.js.map +1 -1
- package/dist/lib/tui-quota-overview.d.ts +55 -0
- package/dist/lib/tui-quota-overview.d.ts.map +1 -0
- package/dist/lib/tui-quota-overview.js +209 -0
- package/dist/lib/tui-quota-overview.js.map +1 -0
- package/dist/lib/tui-status.d.ts +55 -0
- package/dist/lib/tui-status.d.ts.map +1 -1
- package/dist/lib/tui-status.js +155 -14
- package/dist/lib/tui-status.js.map +1 -1
- package/dist/lib/ui/auth-menu.d.ts +2 -0
- package/dist/lib/ui/auth-menu.d.ts.map +1 -1
- package/dist/lib/ui/auth-menu.js +10 -6
- package/dist/lib/ui/auth-menu.js.map +1 -1
- package/dist/tui.d.ts +55 -0
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +511 -29
- package/dist/tui.js.map +1 -1
- package/package.json +155 -155
- package/scripts/audit-dev-allowlist.js +114 -114
- package/scripts/clean-dist.js +27 -27
- package/scripts/copy-oauth-success.js +47 -47
- package/scripts/install-oc-codex-multi-auth-core.js +2003 -1499
- package/scripts/install-oc-codex-multi-auth.js +37 -37
- package/scripts/test-all-models.sh +260 -260
- package/scripts/validate-model-map.sh +97 -97
package/dist/tui.js
CHANGED
|
@@ -1,13 +1,67 @@
|
|
|
1
|
-
import { getCodexTuiMaskEmail, getCodexTuiMaskEmailInQuotaDetails, loadPluginConfig, } from "./lib/config.js";
|
|
1
|
+
import { getCodexTuiMaskEmail, getCodexTuiMaskEmailInQuotaDetails, getQuotaDisplay, getQuotaStatus, loadPluginConfig, } from "./lib/config.js";
|
|
2
|
+
import { PROVIDER_ID } from "./lib/constants.js";
|
|
3
|
+
import { fetchTuiQuotaOverview, mergeOverviewWithLatestAccount, toQuotaOverviewAccounts, } from "./lib/tui-quota-overview.js";
|
|
2
4
|
import { createUsageAccountFingerprint, ensureCodexUsageAccessToken, fetchCodexUsage, formatUsageWindowLabel, getUsageLeftPercent, hasUsageWindow, parseCodexUsagePayload, resolveCodexUsageAccountId, resolveCodexUsageActiveAccount, } from "./lib/codex-usage.js";
|
|
3
|
-
import { formatPromptStatusText, formatQuotaDetailsText, resolveQuotaPromptTone, } from "./lib/tui-status.js";
|
|
4
|
-
import { createTuiQuotaSnapshot, getTuiQuotaCachePath, isFreshTuiQuotaSnapshot, isTuiQuotaSnapshot, readTuiQuotaSnapshot, writeTuiQuotaSnapshot, } from "./lib/tui-quota-cache.js";
|
|
5
|
+
import { formatPromptStatusText, formatQuotaDetailsText, formatQuotaOverviewStatusLines, formatQuotaResetsStatusLines, resolveQuotaOverviewTone, resolveQuotaPromptTone, } from "./lib/tui-status.js";
|
|
6
|
+
import { createTuiQuotaSnapshot, getTuiQuotaCachePath, getTuiQuotaOverviewCachePath, isFreshTuiQuotaSnapshot, isTuiQuotaSnapshot, readTuiQuotaSnapshot, writeTuiQuotaSnapshot, } from "./lib/tui-quota-cache.js";
|
|
5
7
|
import { loadAccounts } from "./lib/storage.js";
|
|
6
8
|
const CACHE_KEY = "oc-codex-multi-auth:tui-status:v2";
|
|
7
9
|
const REFRESH_INTERVAL_MS = 5 * 60 * 1000;
|
|
8
10
|
const EVENT_REFRESH_DEBOUNCE_MS = 750;
|
|
9
11
|
const ACCOUNT_POLL_INTERVAL_MS = 1_000;
|
|
12
|
+
// The status line is the one surface that reads this configuration once and
|
|
13
|
+
// then renders it for the rest of the session, so it is the only one where an
|
|
14
|
+
// edit would otherwise need a restart to be seen. `loadPluginConfig` already
|
|
15
|
+
// re-reads the file on every call and only skips re-parsing when the bytes are
|
|
16
|
+
// unchanged, so polling it costs one read of a file measured in hundreds of
|
|
17
|
+
// bytes.
|
|
18
|
+
const CONFIG_POLL_INTERVAL_MS = 2_000;
|
|
19
|
+
// Re-reading three numbers off the laid-out node, which is cheap enough to do
|
|
20
|
+
// often and has to be: the first pass happens before any layout exists, and
|
|
21
|
+
// until it is picked up the line is budgeting against the whole terminal.
|
|
22
|
+
const LAYOUT_MEASURE_INTERVAL_MS = 500;
|
|
10
23
|
let inFlightRefresh;
|
|
24
|
+
let inFlightOverview;
|
|
25
|
+
async function refreshQuotaOverviewInner(api) {
|
|
26
|
+
try {
|
|
27
|
+
const now = Date.now();
|
|
28
|
+
const snapshot = await fetchTuiQuotaOverview({
|
|
29
|
+
cachePath: getTuiQuotaOverviewCachePath(api.state.path.state),
|
|
30
|
+
now,
|
|
31
|
+
});
|
|
32
|
+
if (!snapshot || snapshot.accounts.length === 0) {
|
|
33
|
+
return { type: "unavailable" };
|
|
34
|
+
}
|
|
35
|
+
const merged = mergeOverviewWithLatestAccount(snapshot, await readSharedQuotaStatus(api));
|
|
36
|
+
return {
|
|
37
|
+
type: "ready",
|
|
38
|
+
accounts: toQuotaOverviewAccounts(merged),
|
|
39
|
+
// Judged on the poll, not on the merged account: one account read a
|
|
40
|
+
// moment ago does not make a five-minute-old reading of the other six
|
|
41
|
+
// current.
|
|
42
|
+
stale: !isFreshTuiQuotaSnapshot(merged, now),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return { type: "unavailable" };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Coalesce concurrent refreshes.
|
|
51
|
+
*
|
|
52
|
+
* Every session event that can move a quota schedules one of these, and
|
|
53
|
+
* without this guard a burst of tool completions would start several passes
|
|
54
|
+
* over the whole pool at once. The pass itself is already cheap while the
|
|
55
|
+
* cache is fresh - it reads one file - so the events can stay wired to it.
|
|
56
|
+
*/
|
|
57
|
+
function refreshQuotaOverview(api) {
|
|
58
|
+
if (inFlightOverview)
|
|
59
|
+
return inFlightOverview;
|
|
60
|
+
inFlightOverview = refreshQuotaOverviewInner(api).finally(() => {
|
|
61
|
+
inFlightOverview = undefined;
|
|
62
|
+
});
|
|
63
|
+
return inFlightOverview;
|
|
64
|
+
}
|
|
11
65
|
function isStoredQuotaStatus(value) {
|
|
12
66
|
return isTuiQuotaSnapshot(value);
|
|
13
67
|
}
|
|
@@ -215,7 +269,242 @@ export function shouldRefreshQuotaForEvent(event) {
|
|
|
215
269
|
return false;
|
|
216
270
|
}
|
|
217
271
|
}
|
|
218
|
-
function
|
|
272
|
+
export function readPromptStatusOptions() {
|
|
273
|
+
const pluginConfig = loadPluginConfig();
|
|
274
|
+
return {
|
|
275
|
+
maskEmail: getCodexTuiMaskEmail(pluginConfig),
|
|
276
|
+
maskEmailInQuotaDetails: getCodexTuiMaskEmailInQuotaDetails(pluginConfig),
|
|
277
|
+
quotaDisplay: getQuotaDisplay(pluginConfig),
|
|
278
|
+
quotaStatus: getQuotaStatus(pluginConfig),
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Whether two readings of the configuration would render the same line.
|
|
283
|
+
*
|
|
284
|
+
* Compared field by field rather than by identity: every poll builds a fresh
|
|
285
|
+
* object, so identity always differs and pushing it into a signal would
|
|
286
|
+
* re-render the status line every two seconds forever.
|
|
287
|
+
*/
|
|
288
|
+
export function samePromptStatusOptions(left, right) {
|
|
289
|
+
const leftStatus = left.quotaStatus;
|
|
290
|
+
const rightStatus = right.quotaStatus;
|
|
291
|
+
return (left.maskEmail === right.maskEmail &&
|
|
292
|
+
left.maskEmailInQuotaDetails === right.maskEmailInQuotaDetails &&
|
|
293
|
+
left.quotaDisplay === right.quotaDisplay &&
|
|
294
|
+
leftStatus.screens.length === rightStatus.screens.length &&
|
|
295
|
+
leftStatus.screens.every((screen, position) => screen === rightStatus.screens[position]) &&
|
|
296
|
+
leftStatus.rotateMs === rightStatus.rotateMs &&
|
|
297
|
+
leftStatus.layout === rightStatus.layout &&
|
|
298
|
+
leftStatus.accountNames === rightStatus.accountNames &&
|
|
299
|
+
leftStatus.order === rightStatus.order &&
|
|
300
|
+
leftStatus.multipliers === rightStatus.multipliers &&
|
|
301
|
+
leftStatus.allotment === rightStatus.allotment &&
|
|
302
|
+
leftStatus.resetTimes === rightStatus.resetTimes &&
|
|
303
|
+
leftStatus.resetCredits === rightStatus.resetCredits &&
|
|
304
|
+
leftStatus.recovery === rightStatus.recovery &&
|
|
305
|
+
leftStatus.rows === rightStatus.rows &&
|
|
306
|
+
leftStatus.showFor === rightStatus.showFor);
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Columns on the prompt's bottom row that belong to the model label beside
|
|
310
|
+
* this line, plus the gap between them.
|
|
311
|
+
*
|
|
312
|
+
* A constant rather than the label's measured width, and that is the whole
|
|
313
|
+
* point. The row lays both boxes out by their content, so a label with no room
|
|
314
|
+
* left is SHRUNK to whatever this line did not take - measuring it would make
|
|
315
|
+
* the budget a function of the line's own length, and every render would
|
|
316
|
+
* ratchet it further down. The row's width is the one number on that row that
|
|
317
|
+
* this line cannot influence.
|
|
318
|
+
*
|
|
319
|
+
* 40 is measured against the real TUI: "Build - Big Pickle OpenCode Zen" is 31
|
|
320
|
+
* characters, and at 80 columns a 48-character line was ellipsized through its
|
|
321
|
+
* middle AND pushed the label onto a second row.
|
|
322
|
+
*/
|
|
323
|
+
const PROMPT_LABEL_RESERVED_CHARS = 40;
|
|
324
|
+
const MAX_LAYOUT_WALK_DEPTH = 8;
|
|
325
|
+
function asLayoutNode(value) {
|
|
326
|
+
return typeof value === "object" && value !== null ? value : undefined;
|
|
327
|
+
}
|
|
328
|
+
function layoutSize(value) {
|
|
329
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0
|
|
330
|
+
? value
|
|
331
|
+
: undefined;
|
|
332
|
+
}
|
|
333
|
+
function layoutChildren(node) {
|
|
334
|
+
if (typeof node.getChildren !== "function")
|
|
335
|
+
return [];
|
|
336
|
+
try {
|
|
337
|
+
const children = node.getChildren();
|
|
338
|
+
if (!Array.isArray(children))
|
|
339
|
+
return [];
|
|
340
|
+
return children
|
|
341
|
+
.map((child) => asLayoutNode(child))
|
|
342
|
+
.filter((child) => Boolean(child));
|
|
343
|
+
}
|
|
344
|
+
catch {
|
|
345
|
+
return [];
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Measure the row this line sits on, by walking up to it.
|
|
350
|
+
*
|
|
351
|
+
* The walk looks for the nearest ancestor laid out as a row with something else
|
|
352
|
+
* in it - the prompt's bottom row, where the model label is the something else.
|
|
353
|
+
* That row stretches to the prompt's inner width, which is the number the
|
|
354
|
+
* terminal width fails to be whenever a sidebar is open, and the only number on
|
|
355
|
+
* that row this line does not influence.
|
|
356
|
+
*
|
|
357
|
+
* Every hop is guarded and the whole thing returns nothing rather than a guess:
|
|
358
|
+
* before the first layout pass there are no widths to read, and a host that
|
|
359
|
+
* rearranges its prompt must degrade to the width heuristic rather than compute
|
|
360
|
+
* a budget from numbers that no longer mean what they did.
|
|
361
|
+
*/
|
|
362
|
+
export function measureStatusSlot(node) {
|
|
363
|
+
const chain = [];
|
|
364
|
+
let current = asLayoutNode(node);
|
|
365
|
+
for (let depth = 0; current && depth < MAX_LAYOUT_WALK_DEPTH; depth += 1) {
|
|
366
|
+
chain.push(current);
|
|
367
|
+
current = asLayoutNode(current.parent);
|
|
368
|
+
}
|
|
369
|
+
for (const [position, ancestor] of chain.entries()) {
|
|
370
|
+
if (position === 0)
|
|
371
|
+
continue;
|
|
372
|
+
if (ancestor.primaryAxis !== "row")
|
|
373
|
+
continue;
|
|
374
|
+
const rowWidth = layoutSize(ancestor.width);
|
|
375
|
+
if (!rowWidth)
|
|
376
|
+
continue;
|
|
377
|
+
const children = layoutChildren(ancestor);
|
|
378
|
+
if (children.length < 2)
|
|
379
|
+
continue;
|
|
380
|
+
const mine = chain[position - 1];
|
|
381
|
+
if (!mine || !children.includes(mine))
|
|
382
|
+
continue;
|
|
383
|
+
const availableChars = rowWidth - PROMPT_LABEL_RESERVED_CHARS;
|
|
384
|
+
if (availableChars < 1)
|
|
385
|
+
continue;
|
|
386
|
+
return { availableChars };
|
|
387
|
+
}
|
|
388
|
+
return {};
|
|
389
|
+
}
|
|
390
|
+
function toQuotaOverviewOptions(options, now) {
|
|
391
|
+
// Spelled out rather than spread: `QuotaStatusConfig` calls the free/used
|
|
392
|
+
// wording `quotaDisplay` and its own screen list `screens`, while
|
|
393
|
+
// `QuotaOverviewOptions.mode` IS the wording - spreading one over the other
|
|
394
|
+
// silently renders every percentage as headroom.
|
|
395
|
+
return {
|
|
396
|
+
mode: options.quotaDisplay,
|
|
397
|
+
layout: options.quotaStatus.layout,
|
|
398
|
+
names: options.quotaStatus.accountNames,
|
|
399
|
+
order: options.quotaStatus.order,
|
|
400
|
+
multipliers: options.quotaStatus.multipliers,
|
|
401
|
+
allotment: options.quotaStatus.allotment,
|
|
402
|
+
resetTimes: options.quotaStatus.resetTimes,
|
|
403
|
+
resetCredits: options.quotaStatus.resetCredits,
|
|
404
|
+
recovery: options.quotaStatus.recovery,
|
|
405
|
+
maskEmail: options.maskEmail,
|
|
406
|
+
now,
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* The pool-wide status line's data pipeline.
|
|
411
|
+
*
|
|
412
|
+
* Kept apart from the active-account one rather than branching inside it:
|
|
413
|
+
* that one maintains a serving-account fingerprint, a snapshot revision and a
|
|
414
|
+
* one-second identity poll, all of which exist to answer "which account is
|
|
415
|
+
* this" - the question this mode is built to stop asking.
|
|
416
|
+
*/
|
|
417
|
+
function createOverviewQuotaController(api, solid) {
|
|
418
|
+
const [state, setState] = solid.createSignal({
|
|
419
|
+
type: "loading",
|
|
420
|
+
});
|
|
421
|
+
const refresh = () => {
|
|
422
|
+
void refreshQuotaOverview(api).then(setState, () => {
|
|
423
|
+
setState({ type: "unavailable" });
|
|
424
|
+
});
|
|
425
|
+
};
|
|
426
|
+
let refreshTimeout;
|
|
427
|
+
const scheduleRefresh = () => {
|
|
428
|
+
if (refreshTimeout)
|
|
429
|
+
clearTimeout(refreshTimeout);
|
|
430
|
+
refreshTimeout = setTimeout(() => {
|
|
431
|
+
refreshTimeout = undefined;
|
|
432
|
+
refresh();
|
|
433
|
+
}, EVENT_REFRESH_DEBOUNCE_MS);
|
|
434
|
+
};
|
|
435
|
+
refresh();
|
|
436
|
+
const interval = setInterval(refresh, REFRESH_INTERVAL_MS);
|
|
437
|
+
const disposers = [
|
|
438
|
+
api.event.on("message.updated", (event) => {
|
|
439
|
+
if (shouldRefreshQuotaForEvent(event))
|
|
440
|
+
scheduleRefresh();
|
|
441
|
+
}),
|
|
442
|
+
api.event.on("message.part.updated", (event) => {
|
|
443
|
+
if (shouldRefreshQuotaForEvent(event))
|
|
444
|
+
scheduleRefresh();
|
|
445
|
+
}),
|
|
446
|
+
api.event.on("session.idle", (event) => {
|
|
447
|
+
if (shouldRefreshQuotaForEvent(event))
|
|
448
|
+
scheduleRefresh();
|
|
449
|
+
}),
|
|
450
|
+
api.event.on("session.status", (event) => {
|
|
451
|
+
if (shouldRefreshQuotaForEvent(event))
|
|
452
|
+
scheduleRefresh();
|
|
453
|
+
}),
|
|
454
|
+
api.event.on("session.error", (event) => {
|
|
455
|
+
if (shouldRefreshQuotaForEvent(event))
|
|
456
|
+
scheduleRefresh();
|
|
457
|
+
}),
|
|
458
|
+
];
|
|
459
|
+
return {
|
|
460
|
+
screens: ["overview", "resets"],
|
|
461
|
+
lines(screen, options, layout) {
|
|
462
|
+
const current = state();
|
|
463
|
+
if (current.type !== "ready") {
|
|
464
|
+
// Blank while the first pass runs; a placeholder swapped out a
|
|
465
|
+
// moment later is exactly the flicker this mode removes. The
|
|
466
|
+
// reset screen stays blank either way - it has nothing to add
|
|
467
|
+
// to a pool nobody has read yet.
|
|
468
|
+
if (screen === "resets")
|
|
469
|
+
return [];
|
|
470
|
+
return current.type === "loading" ? [] : ["limits ?"];
|
|
471
|
+
}
|
|
472
|
+
const render = screen === "resets"
|
|
473
|
+
? formatQuotaResetsStatusLines
|
|
474
|
+
: formatQuotaOverviewStatusLines;
|
|
475
|
+
return render({
|
|
476
|
+
accounts: current.accounts,
|
|
477
|
+
options: toQuotaOverviewOptions(options, Date.now()),
|
|
478
|
+
width: api.renderer.width,
|
|
479
|
+
availableChars: layout.availableChars,
|
|
480
|
+
maxRows: layout.maxRows,
|
|
481
|
+
});
|
|
482
|
+
},
|
|
483
|
+
tone(screen) {
|
|
484
|
+
const current = state();
|
|
485
|
+
if (current.type === "ready") {
|
|
486
|
+
// The reset screen only appears once nothing has headroom left,
|
|
487
|
+
// so the pool tone it would otherwise carry is always danger and
|
|
488
|
+
// says nothing. Warning is the honest colour for "here is the
|
|
489
|
+
// thing you can still do about it".
|
|
490
|
+
if (screen === "resets") {
|
|
491
|
+
return current.stale ? "stale" : "warning";
|
|
492
|
+
}
|
|
493
|
+
return resolveQuotaOverviewTone(current.accounts, current.stale);
|
|
494
|
+
}
|
|
495
|
+
return current.type === "loading" ? "unknown" : "warning";
|
|
496
|
+
},
|
|
497
|
+
dispose() {
|
|
498
|
+
clearInterval(interval);
|
|
499
|
+
if (refreshTimeout)
|
|
500
|
+
clearTimeout(refreshTimeout);
|
|
501
|
+
for (const dispose of disposers)
|
|
502
|
+
dispose();
|
|
503
|
+
},
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
/** The serving-account pipeline: the status line's original behaviour. */
|
|
507
|
+
function createActiveQuotaController(api, solid) {
|
|
219
508
|
const [quota, setQuota] = solid.createSignal({
|
|
220
509
|
type: "loading",
|
|
221
510
|
});
|
|
@@ -303,29 +592,217 @@ function createPromptStatus(api, solid, options) {
|
|
|
303
592
|
if (shouldRefreshQuotaForEvent(event))
|
|
304
593
|
scheduleRefresh();
|
|
305
594
|
});
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
clearTimeout(refreshTimeout);
|
|
311
|
-
disposeMessageUpdated();
|
|
312
|
-
disposeMessagePartUpdated();
|
|
313
|
-
disposeSessionIdle();
|
|
314
|
-
disposeSessionStatus();
|
|
315
|
-
disposeSessionError();
|
|
316
|
-
});
|
|
317
|
-
const node = solid.createElement("text");
|
|
318
|
-
solid.spread(node, {
|
|
319
|
-
get content() {
|
|
320
|
-
return formatPromptStatusText({
|
|
595
|
+
return {
|
|
596
|
+
screens: ["active"],
|
|
597
|
+
lines(_screen, options) {
|
|
598
|
+
const text = formatPromptStatusText({
|
|
321
599
|
quota: quota(),
|
|
322
600
|
width: api.renderer.width,
|
|
323
601
|
maskEmail: options.maskEmail,
|
|
602
|
+
quotaDisplay: options.quotaDisplay,
|
|
324
603
|
});
|
|
604
|
+
return text ? [text] : [];
|
|
605
|
+
},
|
|
606
|
+
tone() {
|
|
607
|
+
return resolveQuotaPromptTone(quota());
|
|
608
|
+
},
|
|
609
|
+
dispose() {
|
|
610
|
+
clearInterval(interval);
|
|
611
|
+
clearInterval(accountInterval);
|
|
612
|
+
if (refreshTimeout)
|
|
613
|
+
clearTimeout(refreshTimeout);
|
|
614
|
+
disposeMessageUpdated();
|
|
615
|
+
disposeMessagePartUpdated();
|
|
616
|
+
disposeSessionIdle();
|
|
617
|
+
disposeSessionStatus();
|
|
618
|
+
disposeSessionError();
|
|
619
|
+
},
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* The status-line node, which outlives any change to how it is configured.
|
|
624
|
+
*
|
|
625
|
+
* Both the shape of the line and the pipeline behind it come from a file the
|
|
626
|
+
* user edits while sessions are open, so this polls that file and swaps the
|
|
627
|
+
* pipeline underneath a node the renderer keeps mounted. The alternative -
|
|
628
|
+
* re-registering the slot - would ask the renderer to replace a live node, and
|
|
629
|
+
* a mode change is exactly when it must not blink.
|
|
630
|
+
*/
|
|
631
|
+
/**
|
|
632
|
+
* The pipelines a set of screens needs, and which screen each one answers.
|
|
633
|
+
*
|
|
634
|
+
* `resets` is served by the pool pipeline rather than one of its own: it needs
|
|
635
|
+
* every account's windows and banked credits, which is exactly what that
|
|
636
|
+
* pipeline already gathers, and a second poller reading the same endpoint for
|
|
637
|
+
* the same numbers would double the request cost to say the same thing.
|
|
638
|
+
*/
|
|
639
|
+
function createQuotaControllers(api, solid, screens) {
|
|
640
|
+
const controllers = [];
|
|
641
|
+
if (screens.includes("active")) {
|
|
642
|
+
controllers.push(createActiveQuotaController(api, solid));
|
|
643
|
+
}
|
|
644
|
+
if (screens.includes("overview") || screens.includes("resets")) {
|
|
645
|
+
controllers.push(createOverviewQuotaController(api, solid));
|
|
646
|
+
}
|
|
647
|
+
return controllers;
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* Whether the account pool is worth naming for the model in front of the user.
|
|
651
|
+
*
|
|
652
|
+
* Unknown counts as yes. A line the user asked for should not disappear
|
|
653
|
+
* because a fresh session has no message to read a provider off yet, and being
|
|
654
|
+
* told about a pool one model does not draw from is a smaller wrong than being
|
|
655
|
+
* told nothing while it runs out.
|
|
656
|
+
*/
|
|
657
|
+
export function showsQuotaForSession(params) {
|
|
658
|
+
if (params.audience === "always")
|
|
659
|
+
return true;
|
|
660
|
+
return !params.providerID || params.providerID === PROVIDER_ID;
|
|
661
|
+
}
|
|
662
|
+
function getString(value) {
|
|
663
|
+
return typeof value === "string" && value.trim().length > 0
|
|
664
|
+
? value.trim()
|
|
665
|
+
: undefined;
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* The provider actually serving this session, newest statement first.
|
|
669
|
+
*
|
|
670
|
+
* A message records what ran, which is the only reliable answer once a session
|
|
671
|
+
* has moved off the configured default; the configured model is the fallback
|
|
672
|
+
* for a session that has not run anything yet.
|
|
673
|
+
*/
|
|
674
|
+
function resolveSessionProviderId(api, sessionID) {
|
|
675
|
+
if (sessionID) {
|
|
676
|
+
try {
|
|
677
|
+
const messages = api.state.session.messages(sessionID);
|
|
678
|
+
for (let position = messages.length - 1; position >= 0; position -= 1) {
|
|
679
|
+
const message = messages[position];
|
|
680
|
+
if (message?.role !== "assistant")
|
|
681
|
+
continue;
|
|
682
|
+
const providerID = getString(message.providerID);
|
|
683
|
+
if (providerID)
|
|
684
|
+
return providerID;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
catch {
|
|
688
|
+
// The session may not be in the store yet; fall through to config.
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
const configured = getString(api.state.config.model);
|
|
692
|
+
if (!configured)
|
|
693
|
+
return undefined;
|
|
694
|
+
const slashIndex = configured.indexOf("/");
|
|
695
|
+
return slashIndex > 0 ? configured.slice(0, slashIndex) : undefined;
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* The status-line node, which outlives any change to how it is configured.
|
|
699
|
+
*
|
|
700
|
+
* Both the shape of the line and the pipelines behind it come from a file the
|
|
701
|
+
* user edits while sessions are open, so this polls that file and swaps the
|
|
702
|
+
* pipelines underneath a node the renderer keeps mounted. The alternative -
|
|
703
|
+
* re-registering the slot - would ask the renderer to replace a live node, and
|
|
704
|
+
* a screen change is exactly when it must not blink.
|
|
705
|
+
*/
|
|
706
|
+
function createPromptStatus(api, solid, initialOptions, sessionID) {
|
|
707
|
+
const [options, setOptions] = solid.createSignal(initialOptions);
|
|
708
|
+
const [metrics, setMetrics] = solid.createSignal({});
|
|
709
|
+
const [rotation, setRotation] = solid.createSignal(0);
|
|
710
|
+
let controllers = createQuotaControllers(api, solid, initialOptions.quotaStatus.screens);
|
|
711
|
+
let controllerScreens = initialOptions.quotaStatus.screens;
|
|
712
|
+
let rotationInterval;
|
|
713
|
+
const node = solid.createElement("text");
|
|
714
|
+
const restartRotation = (screens) => {
|
|
715
|
+
if (rotationInterval)
|
|
716
|
+
clearInterval(rotationInterval);
|
|
717
|
+
rotationInterval = undefined;
|
|
718
|
+
if (screens.length < 2)
|
|
719
|
+
return;
|
|
720
|
+
rotationInterval = setInterval(() => {
|
|
721
|
+
setRotation((tick) => tick + 1);
|
|
722
|
+
}, options().quotaStatus.rotateMs);
|
|
723
|
+
};
|
|
724
|
+
restartRotation(controllerScreens);
|
|
725
|
+
const remeasure = () => {
|
|
726
|
+
const next = measureStatusSlot(node);
|
|
727
|
+
if (next.availableChars === metrics().availableChars)
|
|
728
|
+
return;
|
|
729
|
+
setMetrics(next);
|
|
730
|
+
};
|
|
731
|
+
const measureInterval = setInterval(remeasure, LAYOUT_MEASURE_INTERVAL_MS);
|
|
732
|
+
const configInterval = setInterval(() => {
|
|
733
|
+
const next = readPromptStatusOptions();
|
|
734
|
+
if (samePromptStatusOptions(options(), next))
|
|
735
|
+
return;
|
|
736
|
+
const nextScreens = next.quotaStatus.screens;
|
|
737
|
+
const screensChanged = nextScreens.length !== controllerScreens.length ||
|
|
738
|
+
nextScreens.some((screen, position) => screen !== controllerScreens[position]);
|
|
739
|
+
// The swap happens BEFORE the signal that re-renders against it. The
|
|
740
|
+
// other order leaves the line blank until something else happens to
|
|
741
|
+
// change: the render triggered by `setOptions` would read the outgoing
|
|
742
|
+
// pipelines, find none of them serving the incoming screens, subscribe
|
|
743
|
+
// to nothing, and never hear the new pipeline resolve.
|
|
744
|
+
if (screensChanged) {
|
|
745
|
+
for (const controller of controllers)
|
|
746
|
+
controller.dispose();
|
|
747
|
+
controllers = createQuotaControllers(api, solid, nextScreens);
|
|
748
|
+
controllerScreens = nextScreens;
|
|
749
|
+
setRotation(0);
|
|
750
|
+
}
|
|
751
|
+
setOptions(next);
|
|
752
|
+
restartRotation(nextScreens);
|
|
753
|
+
}, CONFIG_POLL_INTERVAL_MS);
|
|
754
|
+
solid.onCleanup(() => {
|
|
755
|
+
clearInterval(configInterval);
|
|
756
|
+
clearInterval(measureInterval);
|
|
757
|
+
if (rotationInterval)
|
|
758
|
+
clearInterval(rotationInterval);
|
|
759
|
+
for (const controller of controllers)
|
|
760
|
+
controller.dispose();
|
|
761
|
+
});
|
|
762
|
+
/**
|
|
763
|
+
* The screen on show right now, picked from the ones that have something to
|
|
764
|
+
* say. A screen with nothing to render is skipped rather than shown blank,
|
|
765
|
+
* which is what lets `resets` sit in the rotation permanently and only
|
|
766
|
+
* appear on the day it matters.
|
|
767
|
+
*/
|
|
768
|
+
const current = () => {
|
|
769
|
+
const currentOptions = options();
|
|
770
|
+
if (!showsQuotaForSession({
|
|
771
|
+
audience: currentOptions.quotaStatus.showFor,
|
|
772
|
+
providerID: resolveSessionProviderId(api, sessionID),
|
|
773
|
+
})) {
|
|
774
|
+
return undefined;
|
|
775
|
+
}
|
|
776
|
+
const layout = {
|
|
777
|
+
availableChars: metrics().availableChars,
|
|
778
|
+
maxRows: currentOptions.quotaStatus.rows,
|
|
779
|
+
};
|
|
780
|
+
const rendered = [];
|
|
781
|
+
for (const screen of currentOptions.quotaStatus.screens) {
|
|
782
|
+
const controller = controllers.find((candidate) => candidate.screens.includes(screen));
|
|
783
|
+
if (!controller)
|
|
784
|
+
continue;
|
|
785
|
+
const lines = controller.lines(screen, currentOptions, layout);
|
|
786
|
+
if (lines.length > 0)
|
|
787
|
+
rendered.push({ screen, lines });
|
|
788
|
+
}
|
|
789
|
+
if (rendered.length === 0)
|
|
790
|
+
return undefined;
|
|
791
|
+
return rendered[Math.abs(rotation()) % rendered.length];
|
|
792
|
+
};
|
|
793
|
+
solid.spread(node, {
|
|
794
|
+
get content() {
|
|
795
|
+
// A newline is how one text node becomes two rows: the renderer
|
|
796
|
+
// measures the content it was given, so the node grows to match
|
|
797
|
+
// instead of the slot having to add and remove child nodes.
|
|
798
|
+
return current()?.lines.join("\n") ?? "";
|
|
325
799
|
},
|
|
326
800
|
get fg() {
|
|
327
|
-
const
|
|
328
|
-
const
|
|
801
|
+
const screen = current()?.screen;
|
|
802
|
+
const controller = screen
|
|
803
|
+
? controllers.find((candidate) => candidate.screens.includes(screen))
|
|
804
|
+
: undefined;
|
|
805
|
+
const tone = screen && controller ? controller.tone(screen) : "unknown";
|
|
329
806
|
if (tone === "danger")
|
|
330
807
|
return api.theme.current.error;
|
|
331
808
|
if (tone === "warning" || tone === "stale") {
|
|
@@ -335,18 +812,27 @@ function createPromptStatus(api, solid, options) {
|
|
|
335
812
|
return api.theme.current.success;
|
|
336
813
|
return api.theme.current.textMuted;
|
|
337
814
|
},
|
|
815
|
+
// The host centres this slot against a model label that wraps to two
|
|
816
|
+
// rows on a narrow terminal, which put a one-row line on the bottom
|
|
817
|
+
// row and left the top one empty. Reading starts at the top.
|
|
818
|
+
alignSelf: "flex-start",
|
|
338
819
|
selectable: false,
|
|
339
820
|
truncate: true,
|
|
340
821
|
wrapMode: "none",
|
|
341
822
|
}, false);
|
|
342
823
|
return node;
|
|
343
824
|
}
|
|
344
|
-
function showQuotaDetails(api
|
|
825
|
+
function showQuotaDetails(api) {
|
|
826
|
+
// Read at open time, not at startup: the dialog is one keystroke away from
|
|
827
|
+
// the line it explains, and the two disagreeing about `used` vs `free`
|
|
828
|
+
// after an edit would be worse than either being stale alone.
|
|
829
|
+
const options = readPromptStatusOptions();
|
|
345
830
|
void refreshQuotaStatus(api).then((status) => {
|
|
346
831
|
api.ui.dialog.replace(() => api.ui.DialogAlert({
|
|
347
832
|
title: "Codex quota",
|
|
348
833
|
message: formatQuotaDetailsText(status, Date.now(), {
|
|
349
834
|
maskEmail: options.maskEmail && options.maskEmailInQuotaDetails,
|
|
835
|
+
quotaDisplay: options.quotaDisplay,
|
|
350
836
|
}),
|
|
351
837
|
onConfirm: () => api.ui.dialog.clear(),
|
|
352
838
|
}));
|
|
@@ -361,11 +847,7 @@ function showQuotaDetails(api, options) {
|
|
|
361
847
|
const module = {
|
|
362
848
|
id: "oc-codex-multi-auth.status",
|
|
363
849
|
async tui(api) {
|
|
364
|
-
const
|
|
365
|
-
const promptOptions = {
|
|
366
|
-
maskEmail: getCodexTuiMaskEmail(pluginConfig),
|
|
367
|
-
maskEmailInQuotaDetails: getCodexTuiMaskEmailInQuotaDetails(pluginConfig),
|
|
368
|
-
};
|
|
850
|
+
const promptOptions = readPromptStatusOptions();
|
|
369
851
|
const [{ createElement, spread }, { createSignal, onCleanup }] = await Promise.all([import("@opentui/solid"), import("solid-js")]);
|
|
370
852
|
const solid = {
|
|
371
853
|
createElement,
|
|
@@ -375,7 +857,7 @@ const module = {
|
|
|
375
857
|
};
|
|
376
858
|
api.slots.register({
|
|
377
859
|
slots: {
|
|
378
|
-
session_prompt_right: () => createPromptStatus(api, solid, promptOptions),
|
|
860
|
+
session_prompt_right: (_ctx, props) => createPromptStatus(api, solid, promptOptions, props.session_id),
|
|
379
861
|
},
|
|
380
862
|
});
|
|
381
863
|
const disposeCommand = api.command.register(() => [
|
|
@@ -384,7 +866,7 @@ const module = {
|
|
|
384
866
|
value: "codex.quota.details",
|
|
385
867
|
description: "Show active account usage, reset times, source, and last refresh.",
|
|
386
868
|
category: "Codex",
|
|
387
|
-
onSelect: () => showQuotaDetails(api
|
|
869
|
+
onSelect: () => showQuotaDetails(api),
|
|
388
870
|
},
|
|
389
871
|
]);
|
|
390
872
|
api.lifecycle.onDispose(disposeCommand);
|