oc-codex-multi-auth 6.20.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 -568
- 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 +567 -102
- 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 +263 -1
- package/dist/lib/account-display.js.map +1 -1
- package/dist/lib/accounts/persistence.d.ts +3 -1
- package/dist/lib/accounts/persistence.d.ts.map +1 -1
- package/dist/lib/accounts/persistence.js +27 -3
- package/dist/lib/accounts/persistence.js.map +1 -1
- package/dist/lib/accounts/stale-state.d.ts +1 -0
- package/dist/lib/accounts/stale-state.d.ts.map +1 -1
- package/dist/lib/accounts/stale-state.js +30 -0
- package/dist/lib/accounts/stale-state.js.map +1 -1
- package/dist/lib/accounts/warm-recovery.d.ts +8 -0
- package/dist/lib/accounts/warm-recovery.d.ts.map +1 -0
- package/dist/lib/accounts/warm-recovery.js +70 -0
- package/dist/lib/accounts/warm-recovery.js.map +1 -0
- package/dist/lib/accounts/warm-request.d.ts +2 -0
- package/dist/lib/accounts/warm-request.d.ts.map +1 -1
- package/dist/lib/accounts/warm-request.js +4 -2
- package/dist/lib/accounts/warm-request.js.map +1 -1
- package/dist/lib/accounts.d.ts +5 -1
- package/dist/lib/accounts.d.ts.map +1 -1
- package/dist/lib/accounts.js +22 -17
- 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/circuit-breaker.d.ts +5 -0
- package/dist/lib/circuit-breaker.d.ts.map +1 -1
- package/dist/lib/circuit-breaker.js +23 -6
- package/dist/lib/circuit-breaker.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-reset.d.ts +7 -1
- package/dist/lib/codex-reset.d.ts.map +1 -1
- package/dist/lib/codex-reset.js +9 -2
- package/dist/lib/codex-reset.js.map +1 -1
- package/dist/lib/codex-usage.d.ts +7 -4
- package/dist/lib/codex-usage.d.ts.map +1 -1
- package/dist/lib/codex-usage.js +48 -13
- package/dist/lib/codex-usage.js.map +1 -1
- package/dist/lib/config.d.ts +77 -1
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +190 -15
- 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/parallel-probe.d.ts.map +1 -1
- package/dist/lib/parallel-probe.js +2 -1
- package/dist/lib/parallel-probe.js.map +1 -1
- 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 +43 -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/fetch-helpers.js +22 -18
- package/dist/lib/request/fetch-helpers.js.map +1 -1
- package/dist/lib/request/response-handler.d.ts.map +1 -1
- package/dist/lib/request/response-handler.js +85 -43
- package/dist/lib/request/response-handler.js.map +1 -1
- 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/rotation.d.ts +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 +5 -0
- package/dist/lib/storage/load-save.d.ts.map +1 -1
- package/dist/lib/storage/load-save.js +61 -0
- package/dist/lib/storage/load-save.js.map +1 -1
- package/dist/lib/storage/normalize.d.ts.map +1 -1
- package/dist/lib/storage/normalize.js +38 -2
- package/dist/lib/storage/normalize.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/state.d.ts +1 -0
- package/dist/lib/storage/state.d.ts.map +1 -1
- package/dist/lib/storage/state.js +12 -0
- package/dist/lib/storage/state.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 +26 -10
- 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 +53 -8
- 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 +2 -1
- package/dist/lib/tools/codex-warm.d.ts.map +1 -1
- package/dist/lib/tools/codex-warm.js +49 -5
- package/dist/lib/tools/codex-warm.js.map +1 -1
- package/dist/lib/tools/index.d.ts +10 -1
- 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 +79 -12
- 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 +59 -1
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +535 -43
- 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 -1469
- 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
|
}
|
|
@@ -82,10 +136,10 @@ function getSharedQuotaCachePath(api) {
|
|
|
82
136
|
async function readSharedQuotaStatus(api, fingerprint) {
|
|
83
137
|
const cachePath = getSharedQuotaCachePath(api);
|
|
84
138
|
const snapshot = await readTuiQuotaSnapshot(cachePath);
|
|
85
|
-
if (snapshot
|
|
139
|
+
if (snapshot && (fingerprint === undefined || snapshot.fingerprint === fingerprint))
|
|
86
140
|
return snapshot;
|
|
87
141
|
const fallbackSnapshot = await readTuiQuotaSnapshot();
|
|
88
|
-
return fallbackSnapshot
|
|
142
|
+
return fallbackSnapshot && (fingerprint === undefined || fallbackSnapshot.fingerprint === fingerprint)
|
|
89
143
|
? fallbackSnapshot
|
|
90
144
|
: undefined;
|
|
91
145
|
}
|
|
@@ -98,24 +152,28 @@ async function writeSharedQuotaStatus(api, status) {
|
|
|
98
152
|
// not affect the OpenCode TUI.
|
|
99
153
|
}
|
|
100
154
|
}
|
|
101
|
-
async function
|
|
155
|
+
export async function resolveQuotaPollFingerprint(api) {
|
|
102
156
|
const storage = await loadAccounts();
|
|
103
|
-
|
|
157
|
+
if (!storage || storage.accounts.length === 0)
|
|
158
|
+
return undefined;
|
|
159
|
+
const shared = await readSharedQuotaStatus(api);
|
|
160
|
+
if (shared?.source === "headers" &&
|
|
161
|
+
isFreshTuiQuotaSnapshot(shared) &&
|
|
162
|
+
storage.accounts.some((account) => createUsageAccountFingerprint(account) === shared.fingerprint)) {
|
|
163
|
+
return shared.fingerprint;
|
|
164
|
+
}
|
|
165
|
+
const selection = resolveCodexUsageActiveAccount(storage);
|
|
104
166
|
return selection
|
|
105
167
|
? createUsageAccountFingerprint(selection.account)
|
|
106
168
|
: undefined;
|
|
107
169
|
}
|
|
108
|
-
async function refreshQuotaStatusInner(api) {
|
|
170
|
+
export async function refreshQuotaStatusInner(api) {
|
|
109
171
|
try {
|
|
110
172
|
const storage = await loadAccounts();
|
|
111
173
|
if (!storage || storage.accounts.length === 0) {
|
|
112
174
|
return { type: "missing" };
|
|
113
175
|
}
|
|
114
|
-
const
|
|
115
|
-
if (!selection)
|
|
116
|
-
return { type: "missing" };
|
|
117
|
-
const fingerprint = createUsageAccountFingerprint(selection.account);
|
|
118
|
-
const shared = await readSharedQuotaStatus(api, fingerprint);
|
|
176
|
+
const latestShared = await readSharedQuotaStatus(api);
|
|
119
177
|
const now = Date.now();
|
|
120
178
|
// A shared snapshot only proves freshness while requests flow: the
|
|
121
179
|
// request path pushes it per response, but during an idle gap nothing
|
|
@@ -123,10 +181,16 @@ async function refreshQuotaStatusInner(api) {
|
|
|
123
181
|
// server-side. Trust it as current within one refresh interval;
|
|
124
182
|
// otherwise fall through to a live /wham/usage read and keep the
|
|
125
183
|
// snapshot only as a stale fallback.
|
|
126
|
-
if (
|
|
127
|
-
|
|
128
|
-
|
|
184
|
+
if (latestShared?.source === "headers" && isFreshTuiQuotaSnapshot(latestShared, now) &&
|
|
185
|
+
storage.accounts.some((account) => createUsageAccountFingerprint(account) === latestShared.fingerprint)) {
|
|
186
|
+
writeStoredQuotaStatus(api, latestShared);
|
|
187
|
+
return toCompactQuotaStatus(latestShared, false);
|
|
129
188
|
}
|
|
189
|
+
const selection = resolveCodexUsageActiveAccount(storage);
|
|
190
|
+
if (!selection)
|
|
191
|
+
return { type: "missing" };
|
|
192
|
+
const fingerprint = createUsageAccountFingerprint(selection.account);
|
|
193
|
+
const shared = await readSharedQuotaStatus(api, fingerprint);
|
|
130
194
|
const stored = readStoredQuotaStatus(api, fingerprint);
|
|
131
195
|
const cached = shared && (!stored || shared.fetchedAt >= stored.fetchedAt)
|
|
132
196
|
? shared
|
|
@@ -205,7 +269,242 @@ export function shouldRefreshQuotaForEvent(event) {
|
|
|
205
269
|
return false;
|
|
206
270
|
}
|
|
207
271
|
}
|
|
208
|
-
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) {
|
|
209
508
|
const [quota, setQuota] = solid.createSignal({
|
|
210
509
|
type: "loading",
|
|
211
510
|
});
|
|
@@ -244,7 +543,7 @@ function createPromptStatus(api, solid, options) {
|
|
|
244
543
|
return;
|
|
245
544
|
cachePollInFlight = true;
|
|
246
545
|
void (async () => {
|
|
247
|
-
const fingerprint = await
|
|
546
|
+
const fingerprint = await resolveQuotaPollFingerprint(api);
|
|
248
547
|
if (!fingerprint) {
|
|
249
548
|
if (currentFingerprint)
|
|
250
549
|
applyQuota({ type: "missing" });
|
|
@@ -293,29 +592,217 @@ function createPromptStatus(api, solid, options) {
|
|
|
293
592
|
if (shouldRefreshQuotaForEvent(event))
|
|
294
593
|
scheduleRefresh();
|
|
295
594
|
});
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
clearTimeout(refreshTimeout);
|
|
301
|
-
disposeMessageUpdated();
|
|
302
|
-
disposeMessagePartUpdated();
|
|
303
|
-
disposeSessionIdle();
|
|
304
|
-
disposeSessionStatus();
|
|
305
|
-
disposeSessionError();
|
|
306
|
-
});
|
|
307
|
-
const node = solid.createElement("text");
|
|
308
|
-
solid.spread(node, {
|
|
309
|
-
get content() {
|
|
310
|
-
return formatPromptStatusText({
|
|
595
|
+
return {
|
|
596
|
+
screens: ["active"],
|
|
597
|
+
lines(_screen, options) {
|
|
598
|
+
const text = formatPromptStatusText({
|
|
311
599
|
quota: quota(),
|
|
312
600
|
width: api.renderer.width,
|
|
313
601
|
maskEmail: options.maskEmail,
|
|
602
|
+
quotaDisplay: options.quotaDisplay,
|
|
314
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") ?? "";
|
|
315
799
|
},
|
|
316
800
|
get fg() {
|
|
317
|
-
const
|
|
318
|
-
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";
|
|
319
806
|
if (tone === "danger")
|
|
320
807
|
return api.theme.current.error;
|
|
321
808
|
if (tone === "warning" || tone === "stale") {
|
|
@@ -325,18 +812,27 @@ function createPromptStatus(api, solid, options) {
|
|
|
325
812
|
return api.theme.current.success;
|
|
326
813
|
return api.theme.current.textMuted;
|
|
327
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",
|
|
328
819
|
selectable: false,
|
|
329
820
|
truncate: true,
|
|
330
821
|
wrapMode: "none",
|
|
331
822
|
}, false);
|
|
332
823
|
return node;
|
|
333
824
|
}
|
|
334
|
-
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();
|
|
335
830
|
void refreshQuotaStatus(api).then((status) => {
|
|
336
831
|
api.ui.dialog.replace(() => api.ui.DialogAlert({
|
|
337
832
|
title: "Codex quota",
|
|
338
833
|
message: formatQuotaDetailsText(status, Date.now(), {
|
|
339
834
|
maskEmail: options.maskEmail && options.maskEmailInQuotaDetails,
|
|
835
|
+
quotaDisplay: options.quotaDisplay,
|
|
340
836
|
}),
|
|
341
837
|
onConfirm: () => api.ui.dialog.clear(),
|
|
342
838
|
}));
|
|
@@ -351,11 +847,7 @@ function showQuotaDetails(api, options) {
|
|
|
351
847
|
const module = {
|
|
352
848
|
id: "oc-codex-multi-auth.status",
|
|
353
849
|
async tui(api) {
|
|
354
|
-
const
|
|
355
|
-
const promptOptions = {
|
|
356
|
-
maskEmail: getCodexTuiMaskEmail(pluginConfig),
|
|
357
|
-
maskEmailInQuotaDetails: getCodexTuiMaskEmailInQuotaDetails(pluginConfig),
|
|
358
|
-
};
|
|
850
|
+
const promptOptions = readPromptStatusOptions();
|
|
359
851
|
const [{ createElement, spread }, { createSignal, onCleanup }] = await Promise.all([import("@opentui/solid"), import("solid-js")]);
|
|
360
852
|
const solid = {
|
|
361
853
|
createElement,
|
|
@@ -365,7 +857,7 @@ const module = {
|
|
|
365
857
|
};
|
|
366
858
|
api.slots.register({
|
|
367
859
|
slots: {
|
|
368
|
-
session_prompt_right: () => createPromptStatus(api, solid, promptOptions),
|
|
860
|
+
session_prompt_right: (_ctx, props) => createPromptStatus(api, solid, promptOptions, props.session_id),
|
|
369
861
|
},
|
|
370
862
|
});
|
|
371
863
|
const disposeCommand = api.command.register(() => [
|
|
@@ -374,7 +866,7 @@ const module = {
|
|
|
374
866
|
value: "codex.quota.details",
|
|
375
867
|
description: "Show active account usage, reset times, source, and last refresh.",
|
|
376
868
|
category: "Codex",
|
|
377
|
-
onSelect: () => showQuotaDetails(api
|
|
869
|
+
onSelect: () => showQuotaDetails(api),
|
|
378
870
|
},
|
|
379
871
|
]);
|
|
380
872
|
api.lifecycle.onDispose(disposeCommand);
|