@cortexkit/opencode-openai-auth 1.1.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.
Files changed (66) hide show
  1. package/dist/bin.js +8 -8
  2. package/dist/{codex-request-BXDXToyt.d.ts → codex-request-B9D0e97Q.d.ts} +177 -28
  3. package/dist/{index-7c6sxtd6.js → index-1mbgsxsr.js} +2 -2
  4. package/dist/{index-4c8yw54k.js → index-2pgkrebq.js} +2 -2
  5. package/dist/index-4v2fs42x.js +65 -0
  6. package/dist/index-8x1aj7q3.js +2 -0
  7. package/dist/{index-f2fmb596.js → index-bc89akak.js} +1 -1
  8. package/dist/index-ex6nx3mq.js +2 -0
  9. package/dist/{index-zpm95mt5.js → index-ftkhhr6d.js} +1 -1
  10. package/dist/{index-msx2m78k.js → index-gn2b097f.js} +2 -2
  11. package/dist/{index-j8v2xwq6.js → index-m3ctrabv.js} +1 -1
  12. package/dist/index-psjyexj3.js +69 -0
  13. package/dist/{index-k6cydkgt.js → index-tddr876f.js} +2 -2
  14. package/dist/index-tn8x5tz8.js +90 -0
  15. package/dist/{index-csky2xpg.js → index-xwvze96g.js} +1 -1
  16. package/dist/index.d.ts +5 -3
  17. package/dist/index.js +1 -1
  18. package/dist/{model-YRg7Uowo.d.ts → model-BdITTmeb.d.ts} +1 -1
  19. package/dist/{port-file-CxEpgSHY.d.ts → port-file-DbHIVC9a.d.ts} +1 -1
  20. package/dist/{protocol-CDV1Pc6h.d.ts → protocol-MysYPAho.d.ts} +1 -1
  21. package/dist/rpc/port-file.d.ts +1 -1
  22. package/dist/rpc/port-file.js +1 -1
  23. package/dist/rpc/protocol.d.ts +1 -1
  24. package/dist/rpc/protocol.js +1 -1
  25. package/dist/rpc/rpc-client.d.ts +3 -3
  26. package/dist/rpc/rpc-client.js +1 -1
  27. package/dist/rpc/rpc-dir.js +1 -1
  28. package/dist/{sidebar-state-DjTWUbmf.d.ts → sidebar-state-CVSok7zv.d.ts} +114 -5
  29. package/dist/sidebar-state.d.ts +2 -2
  30. package/dist/sidebar-state.js +1 -1
  31. package/dist/token-util-cf0nyh7n.js +1 -0
  32. package/dist/{token-rz4t3ay6.js → token-z6qq7a1d.js} +1 -1
  33. package/dist/tui-preferences.js +1 -1
  34. package/dist/tui.d.ts +2 -2
  35. package/dist/v2/server.d.ts +109 -2
  36. package/dist/v2/server.js +3 -3
  37. package/package.json +4 -4
  38. package/src/tui-compiled/raw/shared/0332701351b3eeec-format.js +203 -18
  39. package/src/tui-compiled/raw/shared/0f77ebc3c914c9c6-custody.js +242 -16
  40. package/src/tui-compiled/raw/shared/169b2b4bf30909bc-consumer.js +168 -6
  41. package/src/tui-compiled/raw/shared/375d9cca2def037d-private-files.js +10 -2
  42. package/src/tui-compiled/raw/shared/94f53cfd572ba4bb-availability.js +106 -0
  43. package/src/tui-compiled/raw/shared/d0f0b6be200311af-insula.js +286 -21
  44. package/src/tui-compiled/raw/shared/d5a8f1061ee2f114-index.js +1 -1
  45. package/src/tui-compiled/raw/shared/dc9290c2a7552ba6-watch.js +253 -0
  46. package/src/tui-compiled/raw/shared/e8d3c8da776ef9cd-index.js +4 -1
  47. package/src/tui-compiled/raw/sidebar-state.js +63 -1
  48. package/src/tui-compiled/raw/tui.tsx +82 -30
  49. package/src/tui-compiled/runtime/shared/0332701351b3eeec-format.js +203 -18
  50. package/src/tui-compiled/runtime/shared/0f77ebc3c914c9c6-custody.js +242 -16
  51. package/src/tui-compiled/runtime/shared/169b2b4bf30909bc-consumer.js +168 -6
  52. package/src/tui-compiled/runtime/shared/375d9cca2def037d-private-files.js +10 -2
  53. package/src/tui-compiled/runtime/shared/94f53cfd572ba4bb-availability.js +106 -0
  54. package/src/tui-compiled/runtime/shared/d0f0b6be200311af-insula.js +286 -21
  55. package/src/tui-compiled/runtime/shared/d5a8f1061ee2f114-index.js +1 -1
  56. package/src/tui-compiled/runtime/shared/dc9290c2a7552ba6-watch.js +253 -0
  57. package/src/tui-compiled/runtime/shared/e8d3c8da776ef9cd-index.js +4 -1
  58. package/src/tui-compiled/runtime/sidebar-state.js +63 -1
  59. package/src/tui-compiled/runtime/tui.js +245 -208
  60. package/dist/index-0yh85bwb.js +0 -69
  61. package/dist/index-7eb3dw37.js +0 -90
  62. package/dist/index-8re8rprk.js +0 -2
  63. package/dist/index-edbdscy7.js +0 -65
  64. package/dist/index-m5qmvr6c.js +0 -2
  65. package/dist/map-Buq-GW7l.d.ts +0 -56
  66. package/dist/token-util-tpy2c7h6.js +0 -1
@@ -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
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  formatQuota,
3
+ isQuotaStale,
3
4
  projectQuota,
4
5
  quotaWindowName
5
6
  } from "./shared/d5a8f1061ee2f114-index.js";
@@ -266,7 +267,7 @@ export function resolveSessionSidebarRouting(state, sessionId, now = Date.now())
266
267
  }
267
268
  function vaultAccountsInPlacementOrder(state) {
268
269
  const accounts = state.vaultAccounts ?? [], byId = new Map(accounts.map((account) => [account.id, account]));
269
- return orderForPlacement(accounts.map((account) => account.id), state.route === "sticky-balanced" ? "roster" : orderedPlacement(state.route), vaultMainRouteId(accounts) ?? "").flatMap((id) => {
270
+ return orderForPlacement(accounts.map((account) => account.id), placementOf(state), vaultMainRouteId(accounts) ?? "").flatMap((id) => {
270
271
  const account = byId.get(id);
271
272
  return account ? [account] : [];
272
273
  });
@@ -962,6 +963,67 @@ export function getCollapsedQuotaSummary(quota, now = Date.now()) {
962
963
  text: !projection || projection.limits.length === 0 && !projection.budget ? null : formatQuota(projection, { now, form: "compact" })
963
964
  };
964
965
  }
966
+ function placementOf(state) {
967
+ return state.route === "sticky-balanced" ? "roster" : orderedPlacement(state.route);
968
+ }
969
+ export function getCollapsedAccountLines(state, options = {}) {
970
+ const now = options.now ?? Date.now(), activeId = resolveActiveAccount(state).id;
971
+ return (state.vaultAccounts !== void 0 ? vaultAccountsInPlacementOrder(state).map((account) => ({
972
+ id: account.id,
973
+ name: account.label ?? account.id,
974
+ quota: account.quota,
975
+ killed: account.killed
976
+ })) : localAccountsInPlacementOrder(state, activeId, options.showFallbacks ?? !0)).map((account) => {
977
+ const projection = sidebarQuotaProjection(account.quota, now), reading = projection && (projection.limits.length > 0 || projection.budget) ? projection : void 0;
978
+ return {
979
+ id: account.id,
980
+ name: account.name,
981
+ killed: account.killed,
982
+ active: account.id === activeId,
983
+ stale: reading ? isQuotaStale(reading, { now }) : !1,
984
+ text: reading ? formatQuota(reading, { now, form: "narrow" }) : null
985
+ };
986
+ });
987
+ }
988
+ function localAccountsInPlacementOrder(state, activeId, showFallbacks) {
989
+ const accounts = [
990
+ {
991
+ id: "main",
992
+ name: "main",
993
+ quota: state.main.quota,
994
+ killed: state.main.killed
995
+ },
996
+ ...state.fallbacks.filter((account) => account.enabled && (showFallbacks || account.id === activeId)).map((account) => ({
997
+ id: account.id,
998
+ name: account.label ?? account.id,
999
+ quota: account.quota,
1000
+ killed: account.killed
1001
+ }))
1002
+ ], byId = new Map(accounts.map((account) => [account.id, account]));
1003
+ return orderForPlacement(accounts.map((account) => account.id), placementOf(state), "main").flatMap((id) => {
1004
+ const account = byId.get(id);
1005
+ return account ? [account] : [];
1006
+ });
1007
+ }
1008
+ export function displayWidth(text) {
1009
+ const bun = globalThis.Bun;
1010
+ return bun?.stringWidth ? bun.stringWidth(text) : Array.from(text).length;
1011
+ }
1012
+ export function fitCollapsedLabel(label, columns) {
1013
+ if (displayWidth(label) <= columns)
1014
+ return label;
1015
+ if (columns < 1)
1016
+ return "";
1017
+ let kept = "", used = 0;
1018
+ for (const char of label) {
1019
+ const width = displayWidth(char);
1020
+ if (used + width > columns - 1)
1021
+ break;
1022
+ kept += char;
1023
+ used += width;
1024
+ }
1025
+ return `${kept}\u2026`;
1026
+ }
965
1027
  const PACING_MIN_ELAPSED_MS = 300000, PACING_MIN_ELAPSED_FRACTION = 0.01, ON_PACE_DELTA = 1;
966
1028
  export function computeQuotaPacing(window, windowMs, now) {
967
1029
  if (!window.resetsAt)
@@ -18,7 +18,7 @@ import {
18
18
  createEffect,
19
19
  createSignal,
20
20
  For,
21
- type JSX,
21
+ Index,
22
22
  onCleanup,
23
23
  Show,
24
24
  } from "solid-js"
@@ -28,8 +28,12 @@ import { createRpcClient } from "./rpc/rpc-client.js"
28
28
  import { resolveRpcDir } from "./rpc/rpc-dir.js"
29
29
  import {
30
30
  type AccountQuota,
31
+ type CollapsedAccountLine,
31
32
  computeQuotaPacing,
32
33
  DEFAULT_SIDEBAR_STATE,
34
+ displayWidth,
35
+ fitCollapsedLabel,
36
+ getCollapsedAccountLines,
33
37
  getCollapsedQuotaSummary,
34
38
  getPresentQuotaWindows,
35
39
  getSidebarState,
@@ -250,17 +254,72 @@ function StatRow(props: {
250
254
  )
251
255
  }
252
256
 
253
- // Compact row for the collapsed view: muted label left, caller-provided value
254
- // right. Mirrors StatRow's layout so columns line up.
255
- function CollapsedRow(props: {
257
+ // One account in the collapsed view: label left; narrow quota text and the
258
+ // status dot right (red ⊘ when killed). The session's account keeps the
259
+ // usage tone; other accounts, and any account whose reading is stale, are
260
+ // drawn muted. The row measures its own width so only the label is cut (with
261
+ // …) when the line does not fit; the quota text is never cut.
262
+ function CollapsedAccountRow(props: {
256
263
  theme: ThemeCurrent
257
- label: string
258
- children: JSX.Element
264
+ line: CollapsedAccountLine
265
+ tone: Tone
259
266
  }) {
267
+ const [width, setWidth] = createSignal(0)
268
+ let row: { width: number } | undefined
269
+ const dim = () => props.line.stale || !props.line.active
270
+ const label = () => {
271
+ const columns = width()
272
+ if (columns <= 0) return props.line.name
273
+ // The value side is the text plus ` ●`, or a lone `—`; one column
274
+ // separates it from the label.
275
+ const value =
276
+ props.line.text === null ? 1 : displayWidth(props.line.text) + 2
277
+ return fitCollapsedLabel(props.line.name, columns - value - 1)
278
+ }
260
279
  return (
261
- <box width='100%' flexDirection='row' justifyContent='space-between'>
262
- <text fg={props.theme.textMuted}>{props.label}</text>
263
- {props.children}
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
290
+ fg={toneColor(props.theme, dim() ? 'muted' : 'text')}
291
+ wrapMode='none'
292
+ flexShrink={1}
293
+ >
294
+ {label()}
295
+ </text>
296
+ <Show
297
+ when={props.line.text !== null}
298
+ fallback={<text fg={props.theme.textMuted}>{'\u2014'}</text>}
299
+ >
300
+ <box flexDirection='row' flexShrink={0}>
301
+ <Show
302
+ when={!dim()}
303
+ fallback={
304
+ <text fg={toneColor(props.theme, 'muted')}>
305
+ {props.line.text}
306
+ </text>
307
+ }
308
+ >
309
+ <text fg={toneColor(props.theme, props.tone)}>
310
+ <b>{props.line.text}</b>
311
+ </text>
312
+ </Show>
313
+ <text
314
+ fg={toneColor(
315
+ props.theme,
316
+ props.line.killed ? 'err' : dim() ? 'muted' : props.tone,
317
+ )}
318
+ >
319
+ {props.line.killed ? ' \u2298' : ' \u25cf'}
320
+ </text>
321
+ </box>
322
+ </Show>
264
323
  </box>
265
324
  )
266
325
  }
@@ -823,6 +882,10 @@ function QuotaSidebar(props: {
823
882
  route: sessionRouting().route,
824
883
  })
825
884
  const activeAccount = () => resolveActiveAccount(sessionState())
885
+ const collapsedLines = () =>
886
+ getCollapsedAccountLines(sessionState(), {
887
+ showFallbacks: prefs().sections.fallbackAccounts,
888
+ })
826
889
  const quotaLabelWidth = () => computeQuotaLabelWidth(renderedQuotas(state()))
827
890
  const activeQuotaSummary = () =>
828
891
  getCollapsedQuotaSummary(activeAccount().quota)
@@ -910,28 +973,17 @@ function QuotaSidebar(props: {
910
973
  </Show>
911
974
  </box>
912
975
 
913
- {/* Collapsed: active account's present quota windows + dot (red ⊘ when killed) */}
976
+ {/* Collapsed: one line per account, narrow quota text + dot (red ⊘ when killed) */}
914
977
  <Show when={collapsed() && hasData()}>
915
- <CollapsedRow theme={theme()} label={activeAccount().name}>
916
- <Show
917
- when={activeQuotaSummary().text != null}
918
- fallback={<text fg={theme().textMuted}>{'\u2014'}</text>}
919
- >
920
- <box flexDirection='row'>
921
- <text fg={toneColor(theme(), activeQuotaTone())}>
922
- <b>{activeQuotaSummary().text}</b>
923
- </text>
924
- <text
925
- fg={toneColor(
926
- theme(),
927
- activeAccount().killed ? 'err' : activeQuotaTone(),
928
- )}
929
- >
930
- {activeAccount().killed ? ' \u2298' : ' \u25cf'}
931
- </text>
932
- </box>
933
- </Show>
934
- </CollapsedRow>
978
+ <Index each={collapsedLines()}>
979
+ {(line) => (
980
+ <CollapsedAccountRow
981
+ theme={theme()}
982
+ line={line()}
983
+ tone={activeQuotaTone()}
984
+ />
985
+ )}
986
+ </Index>
935
987
  </Show>
936
988
 
937
989
  {/* Expanded: full sections. Also render when there's no data so the