@cortexkit/opencode-openai-auth 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.js +2 -2
- package/dist/{codex-request-CjXETpTA.d.ts → codex-request-B9D0e97Q.d.ts} +145 -28
- package/dist/{index-ehjj89rx.js → index-1mbgsxsr.js} +1 -1
- package/dist/{index-htvcmtcx.js → index-2pgkrebq.js} +1 -1
- package/dist/{index-rwbrhkse.js → index-4v2fs42x.js} +1 -1
- package/dist/index-8x1aj7q3.js +2 -0
- package/dist/{index-6n4pnfkg.js → index-ex6nx3mq.js} +1 -1
- package/dist/{index-qxbchh2d.js → index-gn2b097f.js} +1 -1
- package/dist/{index-qp5hdpw8.js → index-m3ctrabv.js} +1 -1
- package/dist/{index-03em8hv1.js → index-psjyexj3.js} +15 -15
- package/dist/{index-ffg0sgg5.js → index-tddr876f.js} +1 -1
- package/dist/index-tn8x5tz8.js +90 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/{model-CQcw10oW.d.ts → model-BdITTmeb.d.ts} +1 -1
- package/dist/{port-file-BDJnzgHH.d.ts → port-file-DbHIVC9a.d.ts} +1 -1
- package/dist/{protocol-C-kagE6O.d.ts → protocol-MysYPAho.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/{sidebar-state-BfQ4mpZO.d.ts → sidebar-state-CVSok7zv.d.ts} +20 -4
- package/dist/sidebar-state.d.ts +1 -1
- package/dist/sidebar-state.js +1 -1
- package/dist/tui-preferences.js +1 -1
- package/dist/tui.d.ts +2 -2
- package/dist/v2/server.d.ts +2 -2
- package/dist/v2/server.js +1 -1
- package/package.json +4 -4
- package/src/tui-compiled/raw/shared/0332701351b3eeec-format.js +69 -1
- package/src/tui-compiled/raw/shared/0f77ebc3c914c9c6-custody.js +225 -16
- package/src/tui-compiled/raw/shared/169b2b4bf30909bc-consumer.js +130 -5
- package/src/tui-compiled/raw/shared/375d9cca2def037d-private-files.js +10 -2
- package/src/tui-compiled/raw/shared/94f53cfd572ba4bb-availability.js +106 -0
- package/src/tui-compiled/raw/shared/d0f0b6be200311af-insula.js +286 -21
- package/src/tui-compiled/raw/shared/dc9290c2a7552ba6-watch.js +253 -0
- package/src/tui-compiled/raw/shared/e8d3c8da776ef9cd-index.js +4 -1
- package/src/tui-compiled/runtime/shared/0332701351b3eeec-format.js +69 -1
- package/src/tui-compiled/runtime/shared/0f77ebc3c914c9c6-custody.js +225 -16
- package/src/tui-compiled/runtime/shared/169b2b4bf30909bc-consumer.js +130 -5
- package/src/tui-compiled/runtime/shared/375d9cca2def037d-private-files.js +10 -2
- package/src/tui-compiled/runtime/shared/94f53cfd572ba4bb-availability.js +106 -0
- package/src/tui-compiled/runtime/shared/d0f0b6be200311af-insula.js +286 -21
- package/src/tui-compiled/runtime/shared/dc9290c2a7552ba6-watch.js +253 -0
- package/src/tui-compiled/runtime/shared/e8d3c8da776ef9cd-index.js +4 -1
- package/dist/index-f21addzm.js +0 -90
- package/dist/index-r2s51ac1.js +0 -2
|
@@ -0,0 +1,253 @@
|
|
|
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
|
+
if (force || this.#latestView !== this.#options.view())
|
|
113
|
+
await this.#options.discover();
|
|
114
|
+
}, (error) => {
|
|
115
|
+
if (!this.#stopped)
|
|
116
|
+
this.#options.onError?.(error);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
get mode() {
|
|
120
|
+
return this.#mode;
|
|
121
|
+
}
|
|
122
|
+
start() {
|
|
123
|
+
if (this.#stopped || this.#watch || this.#timer)
|
|
124
|
+
return;
|
|
125
|
+
if (typeof this.#options.client.watchScoped !== 'function') {
|
|
126
|
+
// A claustrum-client older than 0.12.0: poll exactly as before.
|
|
127
|
+
this.#setMode('fallback', 'client-without-watch');
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
void this.#open(false);
|
|
131
|
+
}
|
|
132
|
+
stop() {
|
|
133
|
+
if (this.#stopped)
|
|
134
|
+
return;
|
|
135
|
+
this.#stopped = true;
|
|
136
|
+
if (this.#timer)
|
|
137
|
+
(this.#options.clearTimeoutImpl ?? clearTimeout)(this.#timer);
|
|
138
|
+
this.#timer = undefined;
|
|
139
|
+
const watch = this.#watch;
|
|
140
|
+
this.#watch = undefined;
|
|
141
|
+
watch?.handle.stop();
|
|
142
|
+
}
|
|
143
|
+
#setMode(mode, reason) {
|
|
144
|
+
if (this.#mode === mode)
|
|
145
|
+
return;
|
|
146
|
+
this.#mode = mode;
|
|
147
|
+
this.#options.logger?.debug('claustrum vault watch', { mode, reason });
|
|
148
|
+
this.#options.onMode?.(mode);
|
|
149
|
+
}
|
|
150
|
+
#later(delay, run) {
|
|
151
|
+
this.#timer = (this.#options.setTimeoutImpl ?? setTimeout)(() => {
|
|
152
|
+
this.#timer = undefined;
|
|
153
|
+
if (!this.#stopped)
|
|
154
|
+
run();
|
|
155
|
+
}, delay);
|
|
156
|
+
this.#timer.unref?.();
|
|
157
|
+
}
|
|
158
|
+
async #open(resubscribed) {
|
|
159
|
+
let token;
|
|
160
|
+
try {
|
|
161
|
+
token = await this.#options.enrollmentToken();
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
if (this.#stopped)
|
|
165
|
+
return;
|
|
166
|
+
this.#options.onError?.(error);
|
|
167
|
+
this.#fallback('token-unreadable');
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const watchScoped = this.#options.client.watchScoped;
|
|
171
|
+
if (this.#stopped || this.#watch || !watchScoped)
|
|
172
|
+
return;
|
|
173
|
+
this.#reader.restart();
|
|
174
|
+
// A re-subscribed watch reads on its first notice: changes made while no
|
|
175
|
+
// watch was open were never announced.
|
|
176
|
+
this.#force = resubscribed;
|
|
177
|
+
let watch;
|
|
178
|
+
let handle;
|
|
179
|
+
try {
|
|
180
|
+
handle = watchScoped.call(this.#options.client, (notice) => {
|
|
181
|
+
if (this.#stopped || !watch || this.#watch !== watch)
|
|
182
|
+
return;
|
|
183
|
+
watch.notices++;
|
|
184
|
+
if (watch.notices === 1) {
|
|
185
|
+
this.#options.onReachable?.();
|
|
186
|
+
watch.acceptedAt = (this.#options.now ?? Date.now)();
|
|
187
|
+
this.#setMode('watching', 'accepted');
|
|
188
|
+
}
|
|
189
|
+
if (!this.#reader.continuous(notice))
|
|
190
|
+
this.#force = true;
|
|
191
|
+
this.#latestView = notice.view;
|
|
192
|
+
// The read itself decides whether to list, against the roster as
|
|
193
|
+
// it stands when the read runs.
|
|
194
|
+
void this.#reads.request();
|
|
195
|
+
}, { enrollmentToken: token });
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
this.#options.onError?.(error);
|
|
199
|
+
this.#fallback('watch-failed');
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
watch = { handle, notices: 0 };
|
|
203
|
+
this.#watch = watch;
|
|
204
|
+
this.#setMode('polling', resubscribed ? 'resubscribing' : 'opening');
|
|
205
|
+
const opened = watch;
|
|
206
|
+
handle.closed.then((end) => this.#closed(opened, end), (error) => this.#closed(opened, { kind: 'transport_lost', error }));
|
|
207
|
+
}
|
|
208
|
+
#closed(watch, end) {
|
|
209
|
+
if (this.#stopped || this.#watch !== watch)
|
|
210
|
+
return;
|
|
211
|
+
this.#watch = undefined;
|
|
212
|
+
if (end.kind === 'stopped')
|
|
213
|
+
return;
|
|
214
|
+
if (end.kind === 'transport_lost')
|
|
215
|
+
this.#options.onTransportLost?.();
|
|
216
|
+
if (end.kind === 'unsupported') {
|
|
217
|
+
// An older vault answers unknown_method: poll exactly as before.
|
|
218
|
+
this.#setMode('fallback', 'vault-without-watch');
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (end.kind === 'refused') {
|
|
222
|
+
this.#fallback('refused');
|
|
223
|
+
try {
|
|
224
|
+
this.#options.onRefused(end.error);
|
|
225
|
+
}
|
|
226
|
+
catch (error) {
|
|
227
|
+
this.#options.onError?.(error);
|
|
228
|
+
}
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (watch.notices > 0) {
|
|
232
|
+
// An accepted watch that closed (the vault restarted, or a revocation
|
|
233
|
+
// ended it) gets one new watch. A revocation then shows up as that
|
|
234
|
+
// watch's refusal.
|
|
235
|
+
const now = (this.#options.now ?? Date.now)();
|
|
236
|
+
if (watch.acceptedAt !== undefined &&
|
|
237
|
+
now - watch.acceptedAt >= SAFETY_POLL_MS)
|
|
238
|
+
this.#closes = 0;
|
|
239
|
+
const delay = Math.min(RESUBSCRIBE_BACKOFF_MS * 2 ** Math.min(this.#closes, 16), WATCH_RETRY_MS);
|
|
240
|
+
this.#closes++;
|
|
241
|
+
this.#setMode('polling', 'resubscribing');
|
|
242
|
+
this.#later(delay, () => void this.#open(true));
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
// Zero notices: the watch was never accepted (the vault is draining, or
|
|
246
|
+
// the connection failed). The client already made one read to learn why.
|
|
247
|
+
this.#fallback(end.kind);
|
|
248
|
+
}
|
|
249
|
+
#fallback(reason) {
|
|
250
|
+
this.#setMode('fallback', reason);
|
|
251
|
+
this.#later(WATCH_RETRY_MS, () => void this.#open(false));
|
|
252
|
+
}
|
|
253
|
+
}
|
|
@@ -6,13 +6,16 @@
|
|
|
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";
|
|
15
17
|
export { acceptVaultRoute, DEFAULT_ROUTE_PREFIX, declineVaultRoute, mutateVaultRoster, projectVaultRoster, readVaultRoster, recordVaultAccountQuota, recordVaultQuota, refreshVaultRoster, resolveVaultPrimary, vaultRoutingRows, } from "./30f5bc480f31f51c-roster.js";
|
|
18
|
+
export { ChangeNoticeReader, RESUBSCRIBE_BACKOFF_MS, SAFETY_POLL_MS, ScopedRosterWatch, WATCH_RETRY_MS, } from "./dc9290c2a7552ba6-watch.js";
|
|
16
19
|
/**
|
|
17
20
|
* Connect the client that lists and fetches this consumer's vault credentials
|
|
18
21
|
* on the request path. `connectionFile` is required: this library
|
|
@@ -122,6 +122,9 @@ function scopedWindowParts(quota, options) {
|
|
|
122
122
|
const remainingPercent = clampPercent(limit.remainingPercent ?? 0);
|
|
123
123
|
return {
|
|
124
124
|
scope: limit.scope,
|
|
125
|
+
...(limit.windowMinutes === undefined
|
|
126
|
+
? {}
|
|
127
|
+
: { windowMinutes: limit.windowMinutes }),
|
|
125
128
|
part: {
|
|
126
129
|
kind: 'window',
|
|
127
130
|
name: quotaWindowName(limit),
|
|
@@ -212,6 +215,66 @@ function scopeAbbreviations(scopes) {
|
|
|
212
215
|
}
|
|
213
216
|
return new Map(scopes.map((scope) => [scope, short(scopeDisplayName(scope))]));
|
|
214
217
|
}
|
|
218
|
+
/** The most groups a grouped narrow line shows. */
|
|
219
|
+
const NARROW_GROUP_LIMIT = 2;
|
|
220
|
+
/**
|
|
221
|
+
* The names the groups read under in the narrow line, one per listed group:
|
|
222
|
+
* each label's short name, derived exactly as a model window's is (`Gemini`
|
|
223
|
+
* → `Ge`, `Other models` → `Ot`, colliding names extended until they
|
|
224
|
+
* differ), computed over every listed group so a group keeps its name
|
|
225
|
+
* whichever groups show. A blank label falls back to the group's scope.
|
|
226
|
+
*/
|
|
227
|
+
function groupShortNames(groups) {
|
|
228
|
+
const labels = groups.map((group) => group.label.trim() || group.scope);
|
|
229
|
+
const short = scopeAbbreviations(labels);
|
|
230
|
+
return labels.map((label) => short.get(label) ?? label);
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* The narrow line of a reading whose windows belong to named groups and that
|
|
234
|
+
* has no account-wide window: for each group, its tightest window as
|
|
235
|
+
* `Ge 7d 62%`, with ` left` once at the end. The tightest window is the
|
|
236
|
+
* one with least left, compared before rounding; on a tie the first in
|
|
237
|
+
* display order (the shorter window) wins. At most two groups show: the two
|
|
238
|
+
* with least left, earlier-listed first on a tie, and always in the order
|
|
239
|
+
* the reading lists them, so the line does not reorder as the figures move.
|
|
240
|
+
* A group with no reported window is skipped. Windows outside the listed
|
|
241
|
+
* groups and the credit budget are left out. Undefined when the rule does
|
|
242
|
+
* not apply, so the caller uses the ordinary narrow rule.
|
|
243
|
+
*/
|
|
244
|
+
function groupedNarrowPieces(groups, windows) {
|
|
245
|
+
if (!groups || groups.length === 0)
|
|
246
|
+
return undefined;
|
|
247
|
+
const reported = windows.filter(({ part }) => part.kind === 'window');
|
|
248
|
+
if (reported.some(({ scope }) => scope === ALL_SCOPE))
|
|
249
|
+
return undefined;
|
|
250
|
+
const candidates = groups.flatMap((group, listed) => {
|
|
251
|
+
const inGroup = reported.filter(({ scope }) => scope === group.scope);
|
|
252
|
+
// `windows` is already in display order, and strictly lower wins, so on
|
|
253
|
+
// a tie the shorter window keeps the place.
|
|
254
|
+
const groupTightest = inGroup.reduce((least, window) => least === undefined ||
|
|
255
|
+
(window.part.remainingPercent ?? 0) < (least.part.remainingPercent ?? 0)
|
|
256
|
+
? window
|
|
257
|
+
: least, undefined);
|
|
258
|
+
return groupTightest ? [{ group, listed, window: groupTightest }] : [];
|
|
259
|
+
});
|
|
260
|
+
if (candidates.length === 0)
|
|
261
|
+
return undefined;
|
|
262
|
+
const names = groupShortNames(groups);
|
|
263
|
+
const left = (candidate) => candidate.window.part.remainingPercent ?? 0;
|
|
264
|
+
const shown = [...candidates]
|
|
265
|
+
.sort((a, b) => left(a) - left(b) || a.listed - b.listed)
|
|
266
|
+
.slice(0, NARROW_GROUP_LIMIT)
|
|
267
|
+
.sort((a, b) => a.listed - b.listed);
|
|
268
|
+
return shown.map((candidate, index) => {
|
|
269
|
+
const { windowMinutes } = candidate.window;
|
|
270
|
+
const piece = [
|
|
271
|
+
names[candidate.listed] ?? candidate.group.scope,
|
|
272
|
+
...(windowMinutes === undefined ? [] : [lengthName(windowMinutes)]),
|
|
273
|
+
`${wholePercent(left(candidate))}%`,
|
|
274
|
+
].join(' ');
|
|
275
|
+
return index === shown.length - 1 ? `${piece} left` : piece;
|
|
276
|
+
});
|
|
277
|
+
}
|
|
215
278
|
/**
|
|
216
279
|
* The narrow form's pieces: every account-wide window as `5h 92%`, then at
|
|
217
280
|
* most one more item, with ` left` added once to the last piece. That item is
|
|
@@ -224,10 +287,15 @@ function scopeAbbreviations(scopes) {
|
|
|
224
287
|
* be chosen; when nothing at all is shown it keeps its compact wording
|
|
225
288
|
* (`credits available`). No age is added; `isQuotaStale` says whether the
|
|
226
289
|
* reading is stale. A window whose reset arrived after the reading keeps its
|
|
227
|
-
* figure, as in the other forms.
|
|
290
|
+
* figure, as in the other forms. A reading that lists named groups and has no
|
|
291
|
+
* account-wide window reads one item per group instead (see
|
|
292
|
+
* `groupedNarrowPieces`).
|
|
228
293
|
*/
|
|
229
294
|
function narrowTextParts(quota, options) {
|
|
230
295
|
const { windows } = scopedWindowParts(quota, options);
|
|
296
|
+
const grouped = groupedNarrowPieces(quota?.groups, windows);
|
|
297
|
+
if (grouped)
|
|
298
|
+
return grouped;
|
|
231
299
|
const general = windows.filter(({ part, scope }) => part.kind === 'window' && scope === ALL_SCOPE);
|
|
232
300
|
const scoped = windows.filter(({ part, scope }) => part.kind === 'window' && scope !== ALL_SCOPE);
|
|
233
301
|
const abbreviations = scopeAbbreviations(scoped.map(({ scope }) => scope));
|