@cortexkit/opencode-openai-auth 1.2.0 → 1.4.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/dist/bin.js +8 -8
- package/dist/{codex-request-CjXETpTA.d.ts → codex-request-hw0XcldE.d.ts} +146 -29
- package/dist/index-1eq2s1cp.js +2 -0
- package/dist/{index-qp5hdpw8.js → index-1tdnrt68.js} +1 -1
- package/dist/index-5230x6s8.js +2 -0
- package/dist/{index-qxbchh2d.js → index-ag04nyv7.js} +7 -7
- package/dist/index-brbqqer5.js +90 -0
- package/dist/{index-xwvze96g.js → index-d98f1xdp.js} +1 -1
- package/dist/index-jcyrm58f.js +65 -0
- package/dist/{index-htvcmtcx.js → index-rsvnabbe.js} +2 -2
- package/dist/{index-ftkhhr6d.js → index-s88n2bjn.js} +1 -1
- package/dist/{index-bc89akak.js → index-t5m0dhmq.js} +1 -1
- package/dist/index-vy729a0b.js +69 -0
- package/dist/index-wgwze391.js +2 -0
- package/dist/{index-ffg0sgg5.js → index-yhw3hnsh.js} +2 -2
- package/dist/index.d.ts +11 -3
- package/dist/index.js +1 -1
- package/dist/{model-CQcw10oW.d.ts → model-DphbMQce.d.ts} +1 -1
- package/dist/{port-file-BDJnzgHH.d.ts → port-file-Cx-wwe5Q.d.ts} +1 -1
- package/dist/{protocol-C-kagE6O.d.ts → protocol-CDqdjh74.d.ts} +1 -1
- package/dist/rpc/port-file.d.ts +1 -1
- package/dist/rpc/port-file.js +1 -1
- package/dist/rpc/protocol.d.ts +1 -1
- package/dist/rpc/protocol.js +1 -1
- package/dist/rpc/rpc-client.d.ts +3 -3
- package/dist/rpc/rpc-client.js +1 -1
- package/dist/rpc/rpc-dir.js +1 -1
- package/dist/{sidebar-state-BfQ4mpZO.d.ts → sidebar-state-pTd9gg9W.d.ts} +71 -7
- package/dist/sidebar-state.d.ts +2 -2
- package/dist/sidebar-state.js +1 -1
- package/dist/{token-z6qq7a1d.js → token-0z5g59p8.js} +1 -1
- package/dist/token-util-jf9g6stk.js +1 -0
- package/dist/tui-preferences.js +1 -1
- package/dist/tui.d.ts +30 -3
- package/dist/v2/server.d.ts +2 -2
- package/dist/v2/server.js +2 -2
- package/package.json +6 -6
- package/src/tui-compiled/raw/shared/0332701351b3eeec-format.js +119 -6
- package/src/tui-compiled/raw/shared/0f77ebc3c914c9c6-custody.js +294 -16
- package/src/tui-compiled/raw/shared/169b2b4bf30909bc-consumer.js +178 -26
- package/src/tui-compiled/raw/shared/2577ec69f9973ae9-quota-normalize.js +45 -1
- package/src/tui-compiled/raw/shared/30f5bc480f31f51c-roster.js +100 -43
- package/src/tui-compiled/raw/shared/375d9cca2def037d-private-files.js +10 -2
- package/src/tui-compiled/raw/shared/39c3cb05738fcf6d-projection.js +3 -1
- package/src/tui-compiled/raw/shared/42ae93ab554fcec2-open-url.js +7 -9
- package/src/tui-compiled/raw/shared/4825e03209d6ae3e-with-lock.js +2 -0
- package/src/tui-compiled/raw/shared/4f36d333d081d040-merge.js +17 -3
- package/src/tui-compiled/raw/shared/81dddd203f1b92a3-quota-writer.js +253 -0
- package/src/tui-compiled/raw/shared/905bffc0220f43d0-map.js +31 -6
- package/src/tui-compiled/raw/shared/94f53cfd572ba4bb-availability.js +106 -0
- package/src/tui-compiled/raw/shared/abd062d633e29fd2-pool-routing.js +116 -12
- package/src/tui-compiled/raw/shared/bfbdd0f9aae56b47-pool-quota.js +15 -3
- package/src/tui-compiled/raw/shared/d0f0b6be200311af-insula.js +348 -21
- package/src/tui-compiled/raw/shared/d5a8f1061ee2f114-index.js +2 -2
- package/src/tui-compiled/raw/shared/dbc61cd405941828-accounts.js +5 -4
- package/src/tui-compiled/raw/shared/dc9290c2a7552ba6-watch.js +254 -0
- package/src/tui-compiled/raw/shared/e8d3c8da776ef9cd-index.js +5 -1
- package/src/tui-compiled/raw/sidebar-state.js +32 -3
- package/src/tui-compiled/raw/tui/v2-dialog.js +2 -1
- package/src/tui-compiled/raw/tui.tsx +170 -26
- package/src/tui-compiled/runtime/shared/0332701351b3eeec-format.js +119 -6
- package/src/tui-compiled/runtime/shared/0f77ebc3c914c9c6-custody.js +294 -16
- package/src/tui-compiled/runtime/shared/169b2b4bf30909bc-consumer.js +178 -26
- package/src/tui-compiled/runtime/shared/2577ec69f9973ae9-quota-normalize.js +45 -1
- package/src/tui-compiled/runtime/shared/30f5bc480f31f51c-roster.js +100 -43
- package/src/tui-compiled/runtime/shared/375d9cca2def037d-private-files.js +10 -2
- package/src/tui-compiled/runtime/shared/39c3cb05738fcf6d-projection.js +3 -1
- package/src/tui-compiled/runtime/shared/42ae93ab554fcec2-open-url.js +7 -9
- package/src/tui-compiled/runtime/shared/4825e03209d6ae3e-with-lock.js +2 -0
- package/src/tui-compiled/runtime/shared/4f36d333d081d040-merge.js +17 -3
- package/src/tui-compiled/runtime/shared/81dddd203f1b92a3-quota-writer.js +253 -0
- package/src/tui-compiled/runtime/shared/905bffc0220f43d0-map.js +31 -6
- package/src/tui-compiled/runtime/shared/94f53cfd572ba4bb-availability.js +106 -0
- package/src/tui-compiled/runtime/shared/abd062d633e29fd2-pool-routing.js +116 -12
- package/src/tui-compiled/runtime/shared/bfbdd0f9aae56b47-pool-quota.js +15 -3
- package/src/tui-compiled/runtime/shared/d0f0b6be200311af-insula.js +348 -21
- package/src/tui-compiled/runtime/shared/d5a8f1061ee2f114-index.js +2 -2
- package/src/tui-compiled/runtime/shared/dbc61cd405941828-accounts.js +5 -4
- package/src/tui-compiled/runtime/shared/dc9290c2a7552ba6-watch.js +254 -0
- package/src/tui-compiled/runtime/shared/e8d3c8da776ef9cd-index.js +5 -1
- package/src/tui-compiled/runtime/sidebar-state.js +32 -3
- package/src/tui-compiled/runtime/tui/v2-dialog.js +2 -1
- package/src/tui-compiled/runtime/tui.js +350 -219
- package/dist/index-03em8hv1.js +0 -69
- package/dist/index-6n4pnfkg.js +0 -2
- package/dist/index-ehjj89rx.js +0 -2
- package/dist/index-f21addzm.js +0 -90
- package/dist/index-r2s51ac1.js +0 -2
- package/dist/index-rwbrhkse.js +0 -65
- package/dist/token-util-cf0nyh7n.js +0 -1
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* While a vault watch is healthy, the roster is still read this often: the
|
|
3
|
+
* watch carries no history, so a lost notice or a silently dead stream is
|
|
4
|
+
* caught within this period.
|
|
5
|
+
*/
|
|
6
|
+
export const SAFETY_POLL_MS = 5 * 60_000;
|
|
7
|
+
/**
|
|
8
|
+
* After a watch fails without being accepted, the host polls and tries one
|
|
9
|
+
* new watch this much later, so a refusing or draining module is never asked
|
|
10
|
+
* in a tight loop.
|
|
11
|
+
*/
|
|
12
|
+
export const WATCH_RETRY_MS = 5 * 60_000;
|
|
13
|
+
/**
|
|
14
|
+
* First wait before re-subscribing a watch that closed after it had been
|
|
15
|
+
* accepted. It doubles for each further close in a row, up to WATCH_RETRY_MS.
|
|
16
|
+
*/
|
|
17
|
+
export const RESUBSCRIBE_BACKOFF_MS = 1_000;
|
|
18
|
+
/**
|
|
19
|
+
* Tells whether a notice follows the previous one with nothing lost between
|
|
20
|
+
* them. `seq` restarts at 1 for every watch and `epoch` names the sending
|
|
21
|
+
* module's process lifetime. A sequence gap (notices are sheddable), a
|
|
22
|
+
* different epoch (the module restarted) or a notice of an unknown kind all
|
|
23
|
+
* mean changes may have been missed, so the caller must re-read everything.
|
|
24
|
+
*/
|
|
25
|
+
export class ChangeNoticeReader {
|
|
26
|
+
#epoch;
|
|
27
|
+
#seq = 0;
|
|
28
|
+
/**
|
|
29
|
+
* Begin a new watch, whose first notice is seq 1 again. The epoch is kept,
|
|
30
|
+
* so a module restart between two watches still reads as a break.
|
|
31
|
+
*/
|
|
32
|
+
restart() {
|
|
33
|
+
this.#seq = 0;
|
|
34
|
+
}
|
|
35
|
+
continuous(notice) {
|
|
36
|
+
const continuous = notice.kind === 'changed' &&
|
|
37
|
+
notice.seq === this.#seq + 1 &&
|
|
38
|
+
(this.#epoch === undefined || notice.epoch === this.#epoch);
|
|
39
|
+
this.#epoch = notice.epoch;
|
|
40
|
+
this.#seq = notice.seq;
|
|
41
|
+
return continuous;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Runs `read` with at most one call in flight and at most one more queued
|
|
46
|
+
* behind it, however many requests arrive meanwhile. A request made while a
|
|
47
|
+
* read is running may have arrived after that read took its snapshot, so it
|
|
48
|
+
* earns exactly one more read; any further requests share that one.
|
|
49
|
+
*/
|
|
50
|
+
export class CoalescedRead {
|
|
51
|
+
#read;
|
|
52
|
+
#onError;
|
|
53
|
+
#running;
|
|
54
|
+
#queued = false;
|
|
55
|
+
constructor(read, onError) {
|
|
56
|
+
this.#read = read;
|
|
57
|
+
this.#onError = onError;
|
|
58
|
+
}
|
|
59
|
+
/** The read in flight (including its queued follow-up), if any. */
|
|
60
|
+
get running() {
|
|
61
|
+
return this.#running;
|
|
62
|
+
}
|
|
63
|
+
request() {
|
|
64
|
+
if (this.#running) {
|
|
65
|
+
this.#queued = true;
|
|
66
|
+
return this.#running;
|
|
67
|
+
}
|
|
68
|
+
this.#running = (async () => {
|
|
69
|
+
do {
|
|
70
|
+
this.#queued = false;
|
|
71
|
+
try {
|
|
72
|
+
await this.#read();
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
this.#onError(error);
|
|
76
|
+
}
|
|
77
|
+
} while (this.#queued);
|
|
78
|
+
})().finally(() => {
|
|
79
|
+
this.#running = undefined;
|
|
80
|
+
});
|
|
81
|
+
return this.#running;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Keeps one vault `watchScoped` open for a host and runs the host's discovery
|
|
86
|
+
* when a notice shows the visible account list changed. Discovery runs only
|
|
87
|
+
* when the notice's view differs from the last discovered roster's view, when
|
|
88
|
+
* a notice shows a gap or a new epoch, or on the first notice of a watch that
|
|
89
|
+
* was re-subscribed after a close (the time between two watches was not
|
|
90
|
+
* observed). It never opens a second watch while one is open.
|
|
91
|
+
*/
|
|
92
|
+
export class ScopedRosterWatch {
|
|
93
|
+
#options;
|
|
94
|
+
#reader = new ChangeNoticeReader();
|
|
95
|
+
#reads;
|
|
96
|
+
#mode = 'polling';
|
|
97
|
+
#watch;
|
|
98
|
+
#timer;
|
|
99
|
+
#stopped = false;
|
|
100
|
+
#latestView;
|
|
101
|
+
#force = false;
|
|
102
|
+
/**
|
|
103
|
+
* How many accepted watches in a row have closed; each one doubles the wait
|
|
104
|
+
* before the next re-subscribe. Reset once a watch stays up five minutes.
|
|
105
|
+
*/
|
|
106
|
+
#closes = 0;
|
|
107
|
+
constructor(options) {
|
|
108
|
+
this.#options = options;
|
|
109
|
+
this.#reads = new CoalescedRead(async () => {
|
|
110
|
+
const force = this.#force;
|
|
111
|
+
this.#force = false;
|
|
112
|
+
// A read is only ever requested by a notice, which set #latestView.
|
|
113
|
+
if (force || this.#latestView !== this.#options.view())
|
|
114
|
+
await this.#options.discover(this.#latestView ?? '');
|
|
115
|
+
}, (error) => {
|
|
116
|
+
if (!this.#stopped)
|
|
117
|
+
this.#options.onError?.(error);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
get mode() {
|
|
121
|
+
return this.#mode;
|
|
122
|
+
}
|
|
123
|
+
start() {
|
|
124
|
+
if (this.#stopped || this.#watch || this.#timer)
|
|
125
|
+
return;
|
|
126
|
+
if (typeof this.#options.client.watchScoped !== 'function') {
|
|
127
|
+
// A claustrum-client older than 0.12.0: poll exactly as before.
|
|
128
|
+
this.#setMode('fallback', 'client-without-watch');
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
void this.#open(false);
|
|
132
|
+
}
|
|
133
|
+
stop() {
|
|
134
|
+
if (this.#stopped)
|
|
135
|
+
return;
|
|
136
|
+
this.#stopped = true;
|
|
137
|
+
if (this.#timer)
|
|
138
|
+
(this.#options.clearTimeoutImpl ?? clearTimeout)(this.#timer);
|
|
139
|
+
this.#timer = undefined;
|
|
140
|
+
const watch = this.#watch;
|
|
141
|
+
this.#watch = undefined;
|
|
142
|
+
watch?.handle.stop();
|
|
143
|
+
}
|
|
144
|
+
#setMode(mode, reason) {
|
|
145
|
+
if (this.#mode === mode)
|
|
146
|
+
return;
|
|
147
|
+
this.#mode = mode;
|
|
148
|
+
this.#options.logger?.debug('claustrum vault watch', { mode, reason });
|
|
149
|
+
this.#options.onMode?.(mode);
|
|
150
|
+
}
|
|
151
|
+
#later(delay, run) {
|
|
152
|
+
this.#timer = (this.#options.setTimeoutImpl ?? setTimeout)(() => {
|
|
153
|
+
this.#timer = undefined;
|
|
154
|
+
if (!this.#stopped)
|
|
155
|
+
run();
|
|
156
|
+
}, delay);
|
|
157
|
+
this.#timer.unref?.();
|
|
158
|
+
}
|
|
159
|
+
async #open(resubscribed) {
|
|
160
|
+
let token;
|
|
161
|
+
try {
|
|
162
|
+
token = await this.#options.enrollmentToken();
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
if (this.#stopped)
|
|
166
|
+
return;
|
|
167
|
+
this.#options.onError?.(error);
|
|
168
|
+
this.#fallback('token-unreadable');
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const watchScoped = this.#options.client.watchScoped;
|
|
172
|
+
if (this.#stopped || this.#watch || !watchScoped)
|
|
173
|
+
return;
|
|
174
|
+
this.#reader.restart();
|
|
175
|
+
// A re-subscribed watch reads on its first notice: changes made while no
|
|
176
|
+
// watch was open were never announced.
|
|
177
|
+
this.#force = resubscribed;
|
|
178
|
+
let watch;
|
|
179
|
+
let handle;
|
|
180
|
+
try {
|
|
181
|
+
handle = watchScoped.call(this.#options.client, (notice) => {
|
|
182
|
+
if (this.#stopped || !watch || this.#watch !== watch)
|
|
183
|
+
return;
|
|
184
|
+
watch.notices++;
|
|
185
|
+
if (watch.notices === 1) {
|
|
186
|
+
this.#options.onReachable?.();
|
|
187
|
+
watch.acceptedAt = (this.#options.now ?? Date.now)();
|
|
188
|
+
this.#setMode('watching', 'accepted');
|
|
189
|
+
}
|
|
190
|
+
if (!this.#reader.continuous(notice))
|
|
191
|
+
this.#force = true;
|
|
192
|
+
this.#latestView = notice.view;
|
|
193
|
+
// The read itself decides whether to list, against the roster as
|
|
194
|
+
// it stands when the read runs.
|
|
195
|
+
void this.#reads.request();
|
|
196
|
+
}, { enrollmentToken: token });
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
this.#options.onError?.(error);
|
|
200
|
+
this.#fallback('watch-failed');
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
watch = { handle, notices: 0 };
|
|
204
|
+
this.#watch = watch;
|
|
205
|
+
this.#setMode('polling', resubscribed ? 'resubscribing' : 'opening');
|
|
206
|
+
const opened = watch;
|
|
207
|
+
handle.closed.then((end) => this.#closed(opened, end), (error) => this.#closed(opened, { kind: 'transport_lost', error }));
|
|
208
|
+
}
|
|
209
|
+
#closed(watch, end) {
|
|
210
|
+
if (this.#stopped || this.#watch !== watch)
|
|
211
|
+
return;
|
|
212
|
+
this.#watch = undefined;
|
|
213
|
+
if (end.kind === 'stopped')
|
|
214
|
+
return;
|
|
215
|
+
if (end.kind === 'transport_lost')
|
|
216
|
+
this.#options.onTransportLost?.();
|
|
217
|
+
if (end.kind === 'unsupported') {
|
|
218
|
+
// An older vault answers unknown_method: poll exactly as before.
|
|
219
|
+
this.#setMode('fallback', 'vault-without-watch');
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (end.kind === 'refused') {
|
|
223
|
+
this.#fallback('refused');
|
|
224
|
+
try {
|
|
225
|
+
this.#options.onRefused(end.error);
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
this.#options.onError?.(error);
|
|
229
|
+
}
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (watch.notices > 0) {
|
|
233
|
+
// An accepted watch that closed (the vault restarted, or a revocation
|
|
234
|
+
// ended it) gets one new watch. A revocation then shows up as that
|
|
235
|
+
// watch's refusal.
|
|
236
|
+
const now = (this.#options.now ?? Date.now)();
|
|
237
|
+
if (watch.acceptedAt !== undefined &&
|
|
238
|
+
now - watch.acceptedAt >= SAFETY_POLL_MS)
|
|
239
|
+
this.#closes = 0;
|
|
240
|
+
const delay = Math.min(RESUBSCRIBE_BACKOFF_MS * 2 ** Math.min(this.#closes, 16), WATCH_RETRY_MS);
|
|
241
|
+
this.#closes++;
|
|
242
|
+
this.#setMode('polling', 'resubscribing');
|
|
243
|
+
this.#later(delay, () => void this.#open(true));
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
// Zero notices: the watch was never accepted (the vault is draining, or
|
|
247
|
+
// the connection failed). The client already made one read to learn why.
|
|
248
|
+
this.#fallback(end.kind);
|
|
249
|
+
}
|
|
250
|
+
#fallback(reason) {
|
|
251
|
+
this.#setMode('fallback', reason);
|
|
252
|
+
this.#later(WATCH_RETRY_MS, () => void this.#open(false));
|
|
253
|
+
}
|
|
254
|
+
}
|
|
@@ -6,13 +6,17 @@
|
|
|
6
6
|
import { ClaustrumClient, } from "@cortexkit/claustrum-client";
|
|
7
7
|
import { insulaUsageTransport } from "./d0f0b6be200311af-insula.js";
|
|
8
8
|
export { ClaustrumCredentialError, } from "@cortexkit/claustrum-client";
|
|
9
|
+
export { ClaustrumUnreachableError, classifyVaultFailure, VAULT_FAILURE_CLASSES, } from "./94f53cfd572ba4bb-availability.js";
|
|
9
10
|
export { ClaustrumConsumer, } from "./169b2b4bf30909bc-consumer.js";
|
|
10
|
-
export { ClaustrumScopedCustody, decideScopedRetryAfter401, isScopedCredentialRotation, SERVING_MARGIN_MS, } from "./0f77ebc3c914c9c6-custody.js";
|
|
11
|
+
export { ClaustrumScopedCustody, decideScopedRetryAfter401, isScopedCredentialRotation, KEPT_RECEIPT_MARGIN_MS, SERVING_MARGIN_MS, } from "./0f77ebc3c914c9c6-custody.js";
|
|
11
12
|
export { ClaustrumEnrollmentManager, classifyEnrollmentError, connectClaustrumEnrollmentClient, disconnectClaustrumEnrollment, enrollmentAuthority, enrollmentName, getClaustrumEnrollmentPaths, hostEnrollmentPaths, RETRYABLE_ENROLLMENT_CODES, readClaustrumEnrollmentStatus, readClaustrumEnrollmentToken, resetClaustrumEnrollmentState, TERMINAL_ENROLLMENT_CODES, } from "./b9588ad4725c1d10-enrollment.js";
|
|
12
13
|
export { ClaustrumConsumerError, } from "./ca0fccaba1d87af4-errors.js";
|
|
13
14
|
export { assertHostSlotMatchesMode, assertNotCustodyPlaceholder, CUSTODY_PLACEHOLDER_PREFIX, classifyHostSlot, custodyPlaceholder, custodyPlaceholderKey, isCustodyPlaceholder, isCustodyPlaceholderValue, } from "./d9d2892a4c371301-host-slot.js";
|
|
15
|
+
export { acquireInsulaQuotaSource, } from "./d0f0b6be200311af-insula.js";
|
|
14
16
|
export { acceptAccount, declineAccount, isDeclined, } from "./7c140680fc81be92-interlock.js";
|
|
17
|
+
export { drainVaultQuota } from "./81dddd203f1b92a3-quota-writer.js";
|
|
15
18
|
export { acceptVaultRoute, DEFAULT_ROUTE_PREFIX, declineVaultRoute, mutateVaultRoster, projectVaultRoster, readVaultRoster, recordVaultAccountQuota, recordVaultQuota, refreshVaultRoster, resolveVaultPrimary, vaultRoutingRows, } from "./30f5bc480f31f51c-roster.js";
|
|
19
|
+
export { ChangeNoticeReader, RESUBSCRIBE_BACKOFF_MS, SAFETY_POLL_MS, ScopedRosterWatch, WATCH_RETRY_MS, } from "./dc9290c2a7552ba6-watch.js";
|
|
16
20
|
/**
|
|
17
21
|
* Connect the client that lists and fetches this consumer's vault credentials
|
|
18
22
|
* on the request path. `connectionFile` is required: this library
|
|
@@ -2,13 +2,35 @@ import {
|
|
|
2
2
|
formatQuota,
|
|
3
3
|
isQuotaStale,
|
|
4
4
|
projectQuota,
|
|
5
|
+
quotaCreditsText,
|
|
5
6
|
quotaWindowName
|
|
6
7
|
} from "./shared/d5a8f1061ee2f114-index.js";
|
|
7
8
|
import { orderForPlacement } from "./shared/3e7d45a77c6ad4d5-index.js";
|
|
8
9
|
import {
|
|
10
|
+
creditsUsable,
|
|
9
11
|
orderedPlacement,
|
|
10
12
|
vaultMainRouteId
|
|
11
13
|
} from "./shared/abd062d633e29fd2-pool-routing.js";
|
|
14
|
+
export function accountCreditsText(quota) {
|
|
15
|
+
const credits = quota?.credits;
|
|
16
|
+
if (credits === null || typeof credits !== "object")
|
|
17
|
+
return;
|
|
18
|
+
const { hasCredits, unlimited, balance } = credits;
|
|
19
|
+
if (typeof hasCredits !== "boolean" || typeof unlimited !== "boolean")
|
|
20
|
+
return;
|
|
21
|
+
return quotaCreditsText({
|
|
22
|
+
hasCredits,
|
|
23
|
+
unlimited,
|
|
24
|
+
...typeof balance === "number" ? { balance } : {}
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
export function accountServesOnCredits(quota, now = Date.now()) {
|
|
28
|
+
if (!exhaustedQuotaResetAt(quota, now))
|
|
29
|
+
return !1;
|
|
30
|
+
if (spendControlExhaustedResetAt(quota, now))
|
|
31
|
+
return !1;
|
|
32
|
+
return creditsUsable(quota?.credits, now);
|
|
33
|
+
}
|
|
12
34
|
const QUOTA_WINDOW_KEYS = ["primary", "secondary"], LEGACY_WINDOW_MINUTES = {
|
|
13
35
|
primary: 300,
|
|
14
36
|
secondary: 10080
|
|
@@ -153,7 +175,7 @@ export function normalizeSidebarState(raw) {
|
|
|
153
175
|
};
|
|
154
176
|
} else
|
|
155
177
|
main = { quota: null, killed: !1 };
|
|
156
|
-
const fallbacks = normalizeAccountList(r.fallbacks), vaultAccounts = Array.isArray(r.vaultAccounts) ? normalizeAccountList(r.vaultAccounts) : void 0, activeId = typeof r.activeId === "string" ? r.activeId : void 0, route = typeof r.route === "string" ? r.route : DEFAULT_SIDEBAR_STATE.route, lastUpdated = typeof r.lastUpdated === "number" ? r.lastUpdated : 0, planType = typeof r.planType === "string" ? r.planType : void 0,
|
|
178
|
+
const fallbacks = normalizeAccountList(r.fallbacks), vaultAccounts = Array.isArray(r.vaultAccounts) ? normalizeAccountList(r.vaultAccounts) : void 0, activeId = typeof r.activeId === "string" ? r.activeId : void 0, route = typeof r.route === "string" ? r.route : DEFAULT_SIDEBAR_STATE.route, lastUpdated = typeof r.lastUpdated === "number" ? r.lastUpdated : 0, planType = typeof r.planType === "string" ? r.planType : void 0, activeRouting = normalizeActiveRouting(r.activeRouting), stickyAssignments = normalizeStickyAssignments(r.stickyAssignments);
|
|
157
179
|
return {
|
|
158
180
|
main,
|
|
159
181
|
fallbacks,
|
|
@@ -163,7 +185,6 @@ export function normalizeSidebarState(raw) {
|
|
|
163
185
|
...activeRouting !== void 0 ? { activeRouting } : {},
|
|
164
186
|
...stickyAssignments !== void 0 ? { stickyAssignments } : {},
|
|
165
187
|
...planType !== void 0 ? { planType } : {},
|
|
166
|
-
...credits !== void 0 ? { credits } : {},
|
|
167
188
|
...r.accountPool === !0 ? { accountPool: !0 } : {},
|
|
168
189
|
...vaultAccounts !== void 0 ? { vaultAccounts } : {}
|
|
169
190
|
};
|
|
@@ -491,12 +512,18 @@ function mergeSpendControl(fresher, older) {
|
|
|
491
512
|
return;
|
|
492
513
|
return older.spendControl;
|
|
493
514
|
}
|
|
515
|
+
function fresherCredits(incoming, existing) {
|
|
516
|
+
if (!incoming || !existing)
|
|
517
|
+
return incoming ?? existing;
|
|
518
|
+
const at = (credits) => typeof credits.checkedAt === "number" && Number.isFinite(credits.checkedAt) ? credits.checkedAt : Number.NEGATIVE_INFINITY;
|
|
519
|
+
return at(existing) > at(incoming) ? existing : incoming;
|
|
520
|
+
}
|
|
494
521
|
function mergeQuotaByWindow(incoming, existing) {
|
|
495
522
|
if (!incoming)
|
|
496
523
|
return existing;
|
|
497
524
|
if (!existing)
|
|
498
525
|
return incoming;
|
|
499
|
-
const incomingAt = latestQuotaCheckedAt(incoming), existingAt = latestQuotaCheckedAt(existing), existingSnapshotIsFresher = existingAt !== void 0 && (incomingAt === void 0 || existingAt > incomingAt), primary = freshestWindow(incoming.primary, existing.primary, existingSnapshotIsFresher, incoming.checkedAt, existing.checkedAt), secondary = freshestWindow(incoming.secondary, existing.secondary, existingSnapshotIsFresher, incoming.checkedAt, existing.checkedAt), spendControl = existingSnapshotIsFresher ? mergeSpendControl(existing, incoming) : mergeSpendControl(incoming, existing), spendControlCleared = spendControl === void 0 && (incoming.spendControlCleared === !0 || existing.spendControlCleared === !0);
|
|
526
|
+
const incomingAt = latestQuotaCheckedAt(incoming), existingAt = latestQuotaCheckedAt(existing), existingSnapshotIsFresher = existingAt !== void 0 && (incomingAt === void 0 || existingAt > incomingAt), primary = freshestWindow(incoming.primary, existing.primary, existingSnapshotIsFresher, incoming.checkedAt, existing.checkedAt), secondary = freshestWindow(incoming.secondary, existing.secondary, existingSnapshotIsFresher, incoming.checkedAt, existing.checkedAt), spendControl = existingSnapshotIsFresher ? mergeSpendControl(existing, incoming) : mergeSpendControl(incoming, existing), spendControlCleared = spendControl === void 0 && (incoming.spendControlCleared === !0 || existing.spendControlCleared === !0), credits = fresherCredits(incoming.credits, existing.credits);
|
|
500
527
|
let checkedAt;
|
|
501
528
|
for (const stamp of [
|
|
502
529
|
finiteWindowCheckedAt(primary),
|
|
@@ -507,6 +534,7 @@ function mergeQuotaByWindow(incoming, existing) {
|
|
|
507
534
|
const {
|
|
508
535
|
spendControl: _incomingSpendControl,
|
|
509
536
|
spendControlCleared: _incomingCleared,
|
|
537
|
+
credits: _incomingCredits,
|
|
510
538
|
...incomingRest
|
|
511
539
|
} = incoming;
|
|
512
540
|
return {
|
|
@@ -515,6 +543,7 @@ function mergeQuotaByWindow(incoming, existing) {
|
|
|
515
543
|
secondary,
|
|
516
544
|
...spendControl !== void 0 ? { spendControl } : {},
|
|
517
545
|
...spendControlCleared ? { spendControlCleared: !0 } : {},
|
|
546
|
+
...credits !== void 0 ? { credits } : {},
|
|
518
547
|
checkedAt: checkedAt ?? incoming.checkedAt
|
|
519
548
|
};
|
|
520
549
|
}
|
|
@@ -22,7 +22,8 @@ export async function openControlDialog(dialog, initial, apply, active = () => !
|
|
|
22
22
|
const choices = [
|
|
23
23
|
...section.items.filter((item) => item.actions.length > 0).map((item) => ({
|
|
24
24
|
title: item.label,
|
|
25
|
-
description: inlineDescription(item.
|
|
25
|
+
description: inlineDescription(item.detail),
|
|
26
|
+
...item.status ? { footer: item.status } : {},
|
|
26
27
|
value: { itemId: item.id, actions: item.actions }
|
|
27
28
|
})),
|
|
28
29
|
...section.actions.map((action) => ({
|
|
@@ -16,6 +16,7 @@ import type {
|
|
|
16
16
|
} from "@opencode-ai/plugin/tui"
|
|
17
17
|
import {
|
|
18
18
|
createEffect,
|
|
19
|
+
createMemo,
|
|
19
20
|
createSignal,
|
|
20
21
|
For,
|
|
21
22
|
Index,
|
|
@@ -28,6 +29,8 @@ import { createRpcClient } from "./rpc/rpc-client.js"
|
|
|
28
29
|
import { resolveRpcDir } from "./rpc/rpc-dir.js"
|
|
29
30
|
import {
|
|
30
31
|
type AccountQuota,
|
|
32
|
+
accountCreditsText,
|
|
33
|
+
accountServesOnCredits,
|
|
31
34
|
type CollapsedAccountLine,
|
|
32
35
|
computeQuotaPacing,
|
|
33
36
|
DEFAULT_SIDEBAR_STATE,
|
|
@@ -254,6 +257,45 @@ function StatRow(props: {
|
|
|
254
257
|
)
|
|
255
258
|
}
|
|
256
259
|
|
|
260
|
+
// One of an account's own rows under its quota bars (budget amounts, prepaid
|
|
261
|
+
// credits, reset credits): muted label left, value right. Like the quota rows
|
|
262
|
+
// it stays on one line: it measures its own width and, when both do not fit,
|
|
263
|
+
// cuts only the label (with …), keeping one blank cell before the value.
|
|
264
|
+
function AccountMetaRow(props: {
|
|
265
|
+
theme: ThemeCurrent
|
|
266
|
+
label: string
|
|
267
|
+
value: string
|
|
268
|
+
}) {
|
|
269
|
+
const [width, setWidth] = createSignal(0)
|
|
270
|
+
let row: { width: number } | undefined
|
|
271
|
+
const label = () => {
|
|
272
|
+
const columns = width()
|
|
273
|
+
if (columns <= 0) return props.label
|
|
274
|
+
return fitCollapsedLabel(
|
|
275
|
+
props.label,
|
|
276
|
+
columns - displayWidth(props.value) - 1,
|
|
277
|
+
)
|
|
278
|
+
}
|
|
279
|
+
return (
|
|
280
|
+
<box
|
|
281
|
+
ref={(element: { width: number }) => {
|
|
282
|
+
row = element
|
|
283
|
+
}}
|
|
284
|
+
onSizeChange={() => setWidth(row?.width ?? 0)}
|
|
285
|
+
width='100%'
|
|
286
|
+
flexDirection='row'
|
|
287
|
+
justifyContent='space-between'
|
|
288
|
+
>
|
|
289
|
+
<text fg={props.theme.textMuted} wrapMode='none' flexShrink={1}>
|
|
290
|
+
{label()}
|
|
291
|
+
</text>
|
|
292
|
+
<text fg={props.theme.text} wrapMode='none' flexShrink={0}>
|
|
293
|
+
<b>{props.value}</b>
|
|
294
|
+
</text>
|
|
295
|
+
</box>
|
|
296
|
+
)
|
|
297
|
+
}
|
|
298
|
+
|
|
257
299
|
// One account in the collapsed view: label left; narrow quota text and the
|
|
258
300
|
// status dot right (red ⊘ when killed). The session's account keeps the
|
|
259
301
|
// usage tone; other accounts, and any account whose reading is stale, are
|
|
@@ -475,9 +517,6 @@ export function getQuotaMetadataRows(
|
|
|
475
517
|
): Array<{ label: string; value: string }> {
|
|
476
518
|
const rows: Array<{ label: string; value: string }> = []
|
|
477
519
|
if (state.planType) rows.push({ label: 'Plan', value: state.planType })
|
|
478
|
-
if (state.credits !== undefined) {
|
|
479
|
-
rows.push({ label: 'Credits', value: String(state.credits) })
|
|
480
|
-
}
|
|
481
520
|
return rows
|
|
482
521
|
}
|
|
483
522
|
|
|
@@ -499,9 +538,18 @@ export function formatSpendControlAmounts(
|
|
|
499
538
|
return `${formatGroupedAmount(spendControl.used)} / ${formatGroupedAmount(spendControl.limit)} ${unit}`
|
|
500
539
|
}
|
|
501
540
|
|
|
541
|
+
/**
|
|
542
|
+
* The account's own rows under its quota bars: the workspace budget's amounts
|
|
543
|
+
* (`credits`), the prepaid credits (`prepaid`, worded by `accountCreditsText`)
|
|
544
|
+
* and the reset-credit count (`resets`), each only when known. While the
|
|
545
|
+
* account is serving the session on its prepaid credits (its free quota is
|
|
546
|
+
* used up), the prepaid row says so in place of its `prepaid` label.
|
|
547
|
+
*/
|
|
502
548
|
export function getAccountMetadataRows(
|
|
503
549
|
resetCredits: number | undefined,
|
|
504
550
|
spendControl?: SpendControlReading,
|
|
551
|
+
prepaidCredits?: string,
|
|
552
|
+
servingOnCredits = false,
|
|
505
553
|
): Array<{ label: string; value: string }> {
|
|
506
554
|
const rows: Array<{ label: string; value: string }> = []
|
|
507
555
|
if (spendControl) {
|
|
@@ -510,16 +558,64 @@ export function getAccountMetadataRows(
|
|
|
510
558
|
value: formatSpendControlAmounts(spendControl),
|
|
511
559
|
})
|
|
512
560
|
}
|
|
561
|
+
if (prepaidCredits !== undefined) {
|
|
562
|
+
rows.push({
|
|
563
|
+
label: servingOnCredits ? 'serving on credits' : 'prepaid',
|
|
564
|
+
value: prepaidCredits,
|
|
565
|
+
})
|
|
566
|
+
}
|
|
513
567
|
if (resetCredits !== undefined) {
|
|
514
568
|
rows.push({ label: 'resets', value: String(resetCredits) })
|
|
515
569
|
}
|
|
516
570
|
return rows
|
|
517
571
|
}
|
|
518
572
|
|
|
573
|
+
/** The fewest cells a shortened quota bar keeps; below this it is dropped. */
|
|
574
|
+
const MIN_QUOTA_BAR_CELLS = 3
|
|
575
|
+
|
|
576
|
+
export interface QuotaRowFit {
|
|
577
|
+
/** Cells of usage bar to draw; 0 draws no bar. */
|
|
578
|
+
barCells: number
|
|
579
|
+
showReset: boolean
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* How an expanded quota row fits on one line `columns` cells wide. The row is
|
|
584
|
+
* the padded label, the bar, the " N% left" figure and, at the right edge, the
|
|
585
|
+
* reset time. When it does not fit, the reset time goes first, then the bar
|
|
586
|
+
* shrinks (to no fewer than MIN_QUOTA_BAR_CELLS cells, else it is dropped).
|
|
587
|
+
* The label and the figure are never cut. `columns` of 0 means the row has not
|
|
588
|
+
* been measured yet, and everything is drawn.
|
|
589
|
+
*/
|
|
590
|
+
export function fitQuotaRow(input: {
|
|
591
|
+
columns: number
|
|
592
|
+
label: string
|
|
593
|
+
barWidth: number
|
|
594
|
+
figure: string
|
|
595
|
+
resetText?: string
|
|
596
|
+
}): QuotaRowFit {
|
|
597
|
+
const { columns, barWidth } = input
|
|
598
|
+
const reset = input.resetText ? displayWidth(input.resetText) : 0
|
|
599
|
+
if (columns <= 0) return { barCells: barWidth, showReset: reset > 0 }
|
|
600
|
+
const fixed = displayWidth(input.label) + displayWidth(input.figure)
|
|
601
|
+
// The reset sits at the right edge with at least one blank cell before it,
|
|
602
|
+
// so it never runs into the figure.
|
|
603
|
+
if (reset > 0 && fixed + barWidth + 1 + reset <= columns)
|
|
604
|
+
return { barCells: barWidth, showReset: true }
|
|
605
|
+
const room = columns - fixed
|
|
606
|
+
if (room >= barWidth) return { barCells: barWidth, showReset: false }
|
|
607
|
+
return {
|
|
608
|
+
barCells: room >= MIN_QUOTA_BAR_CELLS ? room : 0,
|
|
609
|
+
showReset: false,
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
519
613
|
// Quota window row: muted label left, tone-colored usage bar and shared quota text.
|
|
520
614
|
// When pacing data is present, the bar
|
|
521
615
|
// gains a pace segment and an off-pace window adds a muted subline with the
|
|
522
616
|
// reserve/deficit delta and the projected runout.
|
|
617
|
+
// The row measures its own width and never wraps: fitQuotaRow decides what is
|
|
618
|
+
// left out when it is too narrow, and the pacing subline is cut with ….
|
|
523
619
|
function QuotaRow(props: {
|
|
524
620
|
theme: ThemeCurrent
|
|
525
621
|
appearance: AppearancePrefs
|
|
@@ -531,6 +627,29 @@ function QuotaRow(props: {
|
|
|
531
627
|
pacing: QuotaPacing | null
|
|
532
628
|
}) {
|
|
533
629
|
const used = () => props.window?.usedPercent ?? 0
|
|
630
|
+
const [columns, setColumns] = createSignal(0)
|
|
631
|
+
let row: { width: number } | undefined
|
|
632
|
+
const label = () => props.label.padEnd(props.labelWidth)
|
|
633
|
+
const figure = () => ` ${props.text.slice(props.label.length + 1)}`
|
|
634
|
+
const fit = () =>
|
|
635
|
+
fitQuotaRow({
|
|
636
|
+
columns: columns(),
|
|
637
|
+
label: label(),
|
|
638
|
+
barWidth: props.appearance.barWidth,
|
|
639
|
+
figure: figure(),
|
|
640
|
+
resetText: props.resetText,
|
|
641
|
+
})
|
|
642
|
+
// Memos, so a new measured width that leaves the fit unchanged (as the
|
|
643
|
+
// first measurement usually does) does not rebuild the bar's text nodes:
|
|
644
|
+
// rebuilt nodes are blank until the next frame is drawn.
|
|
645
|
+
const barCells = createMemo(() => fit().barCells)
|
|
646
|
+
const showReset = createMemo(() => fit().showReset)
|
|
647
|
+
const fittedPaceLine = () => {
|
|
648
|
+
const line = paceLine()
|
|
649
|
+
if (line === null || columns() <= 0) return line
|
|
650
|
+
// The subline is indented by the label column; cut it to the rest.
|
|
651
|
+
return fitCollapsedLabel(line, columns() - props.labelWidth)
|
|
652
|
+
}
|
|
534
653
|
const paceLine = () => {
|
|
535
654
|
const pacing = props.pacing
|
|
536
655
|
if (!pacing || pacing.state === 'on-pace') return null
|
|
@@ -553,40 +672,64 @@ function QuotaRow(props: {
|
|
|
553
672
|
}
|
|
554
673
|
>
|
|
555
674
|
{/* Bars show usage; the adjacent figure shows quota left, as in the dialog. */}
|
|
556
|
-
<box
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
675
|
+
<box
|
|
676
|
+
ref={(element: { width: number }) => {
|
|
677
|
+
row = element
|
|
678
|
+
}}
|
|
679
|
+
onSizeChange={() => setColumns(row?.width ?? 0)}
|
|
680
|
+
width='100%'
|
|
681
|
+
flexDirection='row'
|
|
682
|
+
justifyContent='space-between'
|
|
683
|
+
>
|
|
684
|
+
<box flexDirection='row' flexShrink={0}>
|
|
685
|
+
<text fg={props.theme.textMuted} wrapMode='none'>
|
|
686
|
+
{label()}
|
|
560
687
|
</text>
|
|
561
|
-
<For
|
|
688
|
+
<For
|
|
689
|
+
each={quotaBarSegments(
|
|
690
|
+
used(),
|
|
691
|
+
{ ...props.appearance, barWidth: barCells() },
|
|
692
|
+
props.pacing,
|
|
693
|
+
)}
|
|
694
|
+
>
|
|
562
695
|
{(segment) => (
|
|
563
|
-
<text fg={toneColor(props.theme, segment.tone)}>
|
|
696
|
+
<text fg={toneColor(props.theme, segment.tone)} wrapMode='none'>
|
|
564
697
|
{segment.text}
|
|
565
698
|
</text>
|
|
566
699
|
)}
|
|
567
700
|
</For>
|
|
568
701
|
<text
|
|
569
702
|
fg={toneColor(props.theme, usageTone(used(), props.appearance))}
|
|
703
|
+
wrapMode='none'
|
|
570
704
|
>
|
|
571
|
-
{
|
|
705
|
+
{figure()}
|
|
572
706
|
</text>
|
|
573
707
|
</box>
|
|
574
|
-
<Show when={
|
|
575
|
-
<text fg={props.theme.textMuted}
|
|
708
|
+
<Show when={showReset()}>
|
|
709
|
+
<text fg={props.theme.textMuted} wrapMode='none' flexShrink={0}>
|
|
710
|
+
{props.resetText}
|
|
711
|
+
</text>
|
|
576
712
|
</Show>
|
|
577
713
|
</box>
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
props.
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
714
|
+
{/* Keyed, so a cut to a new width draws a new text node: on Bun 1.3
|
|
715
|
+
an existing node kept drawing the text it was created with. */}
|
|
716
|
+
<Show when={fittedPaceLine()} keyed>
|
|
717
|
+
{(line) => (
|
|
718
|
+
<box width='100%' flexDirection='row'>
|
|
719
|
+
<text fg={props.theme.textMuted}>
|
|
720
|
+
{''.padEnd(props.labelWidth)}
|
|
721
|
+
</text>
|
|
722
|
+
<text
|
|
723
|
+
fg={toneColor(
|
|
724
|
+
props.theme,
|
|
725
|
+
props.pacing?.state === 'deficit' ? 'warn' : 'muted',
|
|
726
|
+
)}
|
|
727
|
+
wrapMode='none'
|
|
728
|
+
>
|
|
729
|
+
{line}
|
|
730
|
+
</text>
|
|
731
|
+
</box>
|
|
732
|
+
)}
|
|
590
733
|
</Show>
|
|
591
734
|
</Show>
|
|
592
735
|
)
|
|
@@ -672,14 +815,15 @@ function AccountBlock(props: {
|
|
|
672
815
|
each={getAccountMetadataRows(
|
|
673
816
|
props.resetCredits,
|
|
674
817
|
props.quota?.spendControl,
|
|
818
|
+
accountCreditsText(props.quota),
|
|
819
|
+
props.active && accountServesOnCredits(props.quota, Date.now()),
|
|
675
820
|
)}
|
|
676
821
|
>
|
|
677
822
|
{(row) => (
|
|
678
|
-
<
|
|
823
|
+
<AccountMetaRow
|
|
679
824
|
theme={props.theme}
|
|
680
825
|
label={row.label}
|
|
681
826
|
value={row.value}
|
|
682
|
-
tone='text'
|
|
683
827
|
/>
|
|
684
828
|
)}
|
|
685
829
|
</For>
|