dsh-plugin-subscription-oauth 0.7.0 → 0.8.1

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.
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Cross-request memory of when each provider account's allowance resets.
3
+ *
4
+ * This is what lets one model id draw on the whole account pool without a wasted
5
+ * request: when the default account's allowance runs out, the moment it will refill is
6
+ * remembered, and until then the scheduler sends work straight to another account
7
+ * instead of trying the spent one, failing, and only then moving on. When the reset
8
+ * time passes the spent account is first choice again.
9
+ *
10
+ * The holder is shared by the route's reader (which learns reset times from usage
11
+ * reads) and its streamer (which consults them when picking an account), so the
12
+ * scheduler's view is exactly the last read's.
13
+ *
14
+ * @module dsh-plugin-subscription-oauth/auth/allowance-scheduler
15
+ */
16
+ /** One account's remembered allowance state. */
17
+ export interface AllowanceState {
18
+ /** Epoch milliseconds at which the account's allowance resets, when known. */
19
+ resetsAt?: number;
20
+ /** Epoch milliseconds at which this state was last learned. */
21
+ learnedAt: number;
22
+ }
23
+ /** In-memory, per-provider memory of account reset times. */
24
+ export declare class AllowanceScheduler {
25
+ private readonly byProvider;
26
+ /** One provider's account states, creating the map on first use. */
27
+ private provider;
28
+ /**
29
+ * Record that one account's allowance resets at a given moment.
30
+ *
31
+ * @param provider - the provider route id.
32
+ * @param account - the account key.
33
+ * @param resetsAt - epoch milliseconds of the allowance reset, when known.
34
+ */
35
+ noteReset(provider: string, account: string, resetsAt?: number): void;
36
+ /**
37
+ * Whether one account's allowance is known to be spent now.
38
+ *
39
+ * True when the account was recorded with a reset time that is still in the future:
40
+ * its allowance is spent and will not refill until then, so a request should go to a
41
+ * different account rather than fail against this one.
42
+ *
43
+ * @param provider - the provider route id.
44
+ * @param account - the account key.
45
+ * @returns whether the account's allowance is spent until its remembered reset.
46
+ */
47
+ isSpentUntilReset(provider: string, account: string): boolean;
48
+ /**
49
+ * The moment one account's allowance is known to reset, when it was recorded.
50
+ *
51
+ * @param provider - the provider route id.
52
+ * @param account - the account key.
53
+ * @returns the reset instant, or undefined when unknown or already passed.
54
+ */
55
+ resetAt(provider: string, account: string): number | undefined;
56
+ }
57
+ /** The one shared scheduler every route reads and writes. */
58
+ export declare const allowanceScheduler: AllowanceScheduler;
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Cross-request memory of when each provider account's allowance resets.
3
+ *
4
+ * This is what lets one model id draw on the whole account pool without a wasted
5
+ * request: when the default account's allowance runs out, the moment it will refill is
6
+ * remembered, and until then the scheduler sends work straight to another account
7
+ * instead of trying the spent one, failing, and only then moving on. When the reset
8
+ * time passes the spent account is first choice again.
9
+ *
10
+ * The holder is shared by the route's reader (which learns reset times from usage
11
+ * reads) and its streamer (which consults them when picking an account), so the
12
+ * scheduler's view is exactly the last read's.
13
+ *
14
+ * @module dsh-plugin-subscription-oauth/auth/allowance-scheduler
15
+ */
16
+ /** In-memory, per-provider memory of account reset times. */
17
+ export class AllowanceScheduler {
18
+ byProvider = new Map();
19
+ /** One provider's account states, creating the map on first use. */
20
+ provider(provider) {
21
+ let map = this.byProvider.get(provider);
22
+ if (map === undefined) {
23
+ map = new Map();
24
+ this.byProvider.set(provider, map);
25
+ }
26
+ return map;
27
+ }
28
+ /**
29
+ * Record that one account's allowance resets at a given moment.
30
+ *
31
+ * @param provider - the provider route id.
32
+ * @param account - the account key.
33
+ * @param resetsAt - epoch milliseconds of the allowance reset, when known.
34
+ */
35
+ noteReset(provider, account, resetsAt) {
36
+ this.provider(provider).set(account, { ...resetsAt === undefined ? {} : { resetsAt }, learnedAt: Date.now() });
37
+ }
38
+ /**
39
+ * Whether one account's allowance is known to be spent now.
40
+ *
41
+ * True when the account was recorded with a reset time that is still in the future:
42
+ * its allowance is spent and will not refill until then, so a request should go to a
43
+ * different account rather than fail against this one.
44
+ *
45
+ * @param provider - the provider route id.
46
+ * @param account - the account key.
47
+ * @returns whether the account's allowance is spent until its remembered reset.
48
+ */
49
+ isSpentUntilReset(provider, account) {
50
+ const state = this.byProvider.get(provider)?.get(account);
51
+ if (state === undefined)
52
+ return false;
53
+ if (state.resetsAt === undefined)
54
+ return false;
55
+ return Date.now() < state.resetsAt;
56
+ }
57
+ /**
58
+ * The moment one account's allowance is known to reset, when it was recorded.
59
+ *
60
+ * @param provider - the provider route id.
61
+ * @param account - the account key.
62
+ * @returns the reset instant, or undefined when unknown or already passed.
63
+ */
64
+ resetAt(provider, account) {
65
+ const state = this.byProvider.get(provider)?.get(account);
66
+ if (state === undefined || state.resetsAt === undefined)
67
+ return undefined;
68
+ return Date.now() < state.resetsAt ? state.resetsAt : undefined;
69
+ }
70
+ }
71
+ /** The one shared scheduler every route reads and writes. */
72
+ export const allowanceScheduler = new AllowanceScheduler();
@@ -117,10 +117,12 @@ export declare function balanceOf(value: {
117
117
  * Format one wallet for the chip's compact line.
118
118
  *
119
119
  * A balance is money, not a percentage, so it renders as `¥6.89` rather than a meter;
120
- * the currency prefix keeps two currencies from being mistaken for one sum.
120
+ * the currency prefix keeps two currencies from being mistaken for one sum. The amount
121
+ * is rounded to two decimal places (a balance reads to the cent, and a figure like
122
+ * `1.1` for `1.10` would mislead) and always carries both places.
121
123
  *
122
124
  * @param wallet - the wallet to show.
123
- * @returns the short form, e.g. `¥6.89` or `+¥2` for a bonus wallet.
125
+ * @returns the short form, e.g. `¥6.89` or `+$2.00` for a bonus wallet.
124
126
  */
125
127
  export declare function formatBalance(wallet: ChipBalance): string;
126
128
  /**
@@ -157,6 +159,60 @@ export declare function pickWindow(windows: readonly UsageWindow[]): UsageWindow
157
159
  * @param usage - the accounts the route answered for.
158
160
  * @returns the reading, or undefined when the route stated no figure.
159
161
  */
162
+ /** What one account's allowance reduces to for the chip. */
163
+ export interface AccountReading {
164
+ /** Which account these figures belong to. */
165
+ account: string;
166
+ /** Plan name, when the provider stated one. */
167
+ plan?: string;
168
+ /** Every metered window, in the order the route stated them. */
169
+ windows: readonly UsageWindow[];
170
+ }
171
+ /**
172
+ * Reduce a route's per-account usage to one reading per account.
173
+ *
174
+ * A provider can hold several accounts (two Antigravity sign-ins, say), and the chip
175
+ * shows each one's allowance on its own line, the way the settings cards do — a merged
176
+ * figure would hide that one account is exhausted while the other is free.
177
+ *
178
+ * @param usage - every account the route answered for.
179
+ * @returns one reading per account that states a figure, in the route's order.
180
+ */
181
+ /**
182
+ * Reduce a route's per-account usage to one reading per account.
183
+ *
184
+ * A provider can hold several accounts (two Antigravity sign-ins, say). Every account's
185
+ * windows are kept as the route stated them — a percentage-metered quota and a credit
186
+ * balance (`credits`) alike — so the per-account panel shows the real figures, and the
187
+ * compact line's merge decides which to integrate.
188
+ *
189
+ * @param usage - every account the route answered for.
190
+ * @returns one reading per account that states any window, in the route's order.
191
+ */
192
+ export declare function accountReadingsOf(usage: readonly ProviderUsage[]): readonly AccountReading[];
193
+ /**
194
+ * Merge every account's percentage-metered windows into one figure per label.
195
+ *
196
+ * The chip's compact line integrates the whole pool: two accounts at 50 percent read as
197
+ * one window at 50 percent (the same allowance spent twice as wide), not two lines. A
198
+ * credit balance (`credits`) is money, not a share of a shared allowance, so it is not
199
+ * merged — it stays on the per-account panel, where it reads correctly. Only windows
200
+ * that state a `usedPercent` (a share of an allowance) merge.
201
+ *
202
+ * @param readings - one per account, from {@link accountReadingsOf}.
203
+ * @returns one merged window per percentage-metered label, most-spent first.
204
+ */
205
+ export declare function mergeAccountWindows(readings: readonly AccountReading[]): readonly UsageWindow[];
206
+ /**
207
+ * Reduce one route's usage to the single line the compact chip renders.
208
+ *
209
+ * Kept for the balance-free providers that still show one merged line.
210
+ *
211
+ * @param provider - the route id.
212
+ * @param label - the route's display name.
213
+ * @param usage - the accounts the route answered for.
214
+ * @returns the first account's reading, or undefined when none states a figure.
215
+ */
160
216
  export declare function readingOf(provider: string, label: string, usage: readonly ProviderUsage[]): ChipReading | undefined;
161
217
  /**
162
218
  * Format a window's reset the way the settings cards do.
@@ -82,14 +82,16 @@ export function balanceOf(value) {
82
82
  * Format one wallet for the chip's compact line.
83
83
  *
84
84
  * A balance is money, not a percentage, so it renders as `¥6.89` rather than a meter;
85
- * the currency prefix keeps two currencies from being mistaken for one sum.
85
+ * the currency prefix keeps two currencies from being mistaken for one sum. The amount
86
+ * is rounded to two decimal places (a balance reads to the cent, and a figure like
87
+ * `1.1` for `1.10` would mislead) and always carries both places.
86
88
  *
87
89
  * @param wallet - the wallet to show.
88
- * @returns the short form, e.g. `¥6.89` or `+¥2` for a bonus wallet.
90
+ * @returns the short form, e.g. `¥6.89` or `+$2.00` for a bonus wallet.
89
91
  */
90
92
  export function formatBalance(wallet) {
91
93
  const amount = Number.parseFloat(wallet.balance);
92
- const trimmed = Number.isFinite(amount) ? String(Math.round(amount * 100) / 100) : wallet.balance;
94
+ const trimmed = Number.isFinite(amount) ? amount.toFixed(2) : wallet.balance;
93
95
  const symbol = wallet.currency === 'CNY' ? '¥' : wallet.currency === 'USD' ? '$' : `${wallet.currency} `;
94
96
  return `${wallet.bonus ? '+' : ''}${symbol}${trimmed}`;
95
97
  }
@@ -142,12 +144,102 @@ export function pickWindow(windows) {
142
144
  return best;
143
145
  }
144
146
  /**
145
- * Reduce one route's usage to what the chip renders.
147
+ * Reduce a route's per-account usage to one reading per account.
146
148
  *
147
- * @param provider - the route the figures came from.
149
+ * A provider can hold several accounts (two Antigravity sign-ins, say), and the chip
150
+ * shows each one's allowance on its own line, the way the settings cards do — a merged
151
+ * figure would hide that one account is exhausted while the other is free.
152
+ *
153
+ * @param usage - every account the route answered for.
154
+ * @returns one reading per account that states a figure, in the route's order.
155
+ */
156
+ /**
157
+ * Reduce a route's per-account usage to one reading per account.
158
+ *
159
+ * A provider can hold several accounts (two Antigravity sign-ins, say). Every account's
160
+ * windows are kept as the route stated them — a percentage-metered quota and a credit
161
+ * balance (`credits`) alike — so the per-account panel shows the real figures, and the
162
+ * compact line's merge decides which to integrate.
163
+ *
164
+ * @param usage - every account the route answered for.
165
+ * @returns one reading per account that states any window, in the route's order.
166
+ */
167
+ export function accountReadingsOf(usage) {
168
+ const out = [];
169
+ for (const entry of usage) {
170
+ if (entry.windows.length === 0)
171
+ continue;
172
+ out.push({ account: entry.account, windows: entry.windows, ...entry.plan === undefined ? {} : { plan: entry.plan } });
173
+ }
174
+ return out;
175
+ }
176
+ /**
177
+ * Merge every account's percentage-metered windows into one figure per label.
178
+ *
179
+ * The chip's compact line integrates the whole pool: two accounts at 50 percent read as
180
+ * one window at 50 percent (the same allowance spent twice as wide), not two lines. A
181
+ * credit balance (`credits`) is money, not a share of a shared allowance, so it is not
182
+ * merged — it stays on the per-account panel, where it reads correctly. Only windows
183
+ * that state a `usedPercent` (a share of an allowance) merge.
184
+ *
185
+ * @param readings - one per account, from {@link accountReadingsOf}.
186
+ * @returns one merged window per percentage-metered label, most-spent first.
187
+ */
188
+ export function mergeAccountWindows(readings) {
189
+ const byLabel = new Map();
190
+ for (const reading of readings) {
191
+ for (const window of reading.windows) {
192
+ // Only a percentage-metered window merges. A credit balance (a bare used/limit
193
+ // pair) is money, not a share of a shared allowance, and averaging it across
194
+ // accounts would misread money as a quota.
195
+ if (window.usedPercent === undefined)
196
+ continue;
197
+ const list = byLabel.get(window.label) ?? [];
198
+ list.push(window);
199
+ byLabel.set(window.label, list);
200
+ }
201
+ }
202
+ const merged = [];
203
+ for (const [label, list] of byLabel) {
204
+ // Percentages average across accounts: the pool's spent share is the mean of each
205
+ // account's spent share (accounts share an allowance, they do not add to it).
206
+ const withPercent = list.filter((window) => window.usedPercent !== undefined);
207
+ if (withPercent.length === 0)
208
+ continue;
209
+ const usedPercent = withPercent.reduce((sum, window) => sum + (window.usedPercent ?? 0), 0) / withPercent.length;
210
+ // The pool's reset is the soonest account reset: the window refills when the first
211
+ // account's does.
212
+ const resets = list.map((window) => window.resetsAt).filter((at) => typeof at === 'number');
213
+ merged.push({
214
+ label,
215
+ usedPercent: Math.max(0, Math.min(100, usedPercent)),
216
+ ...resets.length === 0 ? {} : { resetsAt: Math.min(...resets) },
217
+ });
218
+ }
219
+ // Order by window period, shortest first: the rolling `5h` window reads before the
220
+ // `weekly` one, the way the providers state them and the way a reader scans (what
221
+ // resets soonest is the nearer limit). Unrecognised labels keep their input order.
222
+ const order = (label) => {
223
+ const short = shortWindow(label);
224
+ if (short === '5h' || short === 's')
225
+ return 0;
226
+ if (short === '1d' || short === 'mo')
227
+ return 1;
228
+ if (short === '7d')
229
+ return 2;
230
+ return 3;
231
+ };
232
+ return merged.sort((left, right) => order(left.label) - order(right.label));
233
+ }
234
+ /**
235
+ * Reduce one route's usage to the single line the compact chip renders.
236
+ *
237
+ * Kept for the balance-free providers that still show one merged line.
238
+ *
239
+ * @param provider - the route id.
148
240
  * @param label - the route's display name.
149
241
  * @param usage - the accounts the route answered for.
150
- * @returns the reading, or undefined when the route stated no figure.
242
+ * @returns the first account's reading, or undefined when none states a figure.
151
243
  */
152
244
  export function readingOf(provider, label, usage) {
153
245
  for (const entry of usage) {
@@ -197,6 +289,7 @@ export function formatReset(at, now) {
197
289
  */
198
290
  export function UsageChip({ call, t, isOwnProvider, accountBalance, ccSwitchBalance, useProjection }) {
199
291
  const [reading, setReading] = useState(undefined);
292
+ const [accountReadings, setAccountReadings] = useState(undefined);
200
293
  const [balance, setBalance] = useState(undefined);
201
294
  const [ccBalance, setCcBalance] = useState(undefined);
202
295
  const [now, setNow] = useState(() => Date.now());
@@ -239,8 +332,7 @@ export function UsageChip({ call, t, isOwnProvider, accountBalance, ccSwitchBala
239
332
  const load = useCallback(async (target) => {
240
333
  try {
241
334
  const result = await call('usage', { provider: target });
242
- const label = result.status.providers.find((entry) => entry.provider === target)?.displayName ?? target;
243
- return readingOf(target, label, result.usage);
335
+ return accountReadingsOf(result.usage);
244
336
  }
245
337
  catch {
246
338
  // A failed read is not worth a chip of its own: the settings page is where a user
@@ -276,7 +368,7 @@ export function UsageChip({ call, t, isOwnProvider, accountBalance, ccSwitchBala
276
368
  // Not one of ours (or none selected): keep showing the last own reading.
277
369
  : lastOwn.current;
278
370
  if (target === undefined) {
279
- setReading(undefined);
371
+ setAccountReadings(undefined);
280
372
  return;
281
373
  }
282
374
  let live = true;
@@ -286,7 +378,7 @@ export function UsageChip({ call, t, isOwnProvider, accountBalance, ccSwitchBala
286
378
  if (!live || next === undefined)
287
379
  return;
288
380
  lastOwn.current = target;
289
- setReading(next);
381
+ setAccountReadings(next);
290
382
  });
291
383
  };
292
384
  refresh();
@@ -327,20 +419,22 @@ export function UsageChip({ call, t, isOwnProvider, accountBalance, ccSwitchBala
327
419
  setOpen((was) => !was);
328
420
  }, children: [_jsx("span", { className: "dsh-subscription-usage-chip-label", children: short }), balance.map((wallet) => (_jsx("span", { className: "dsh-subscription-usage-chip-cell", children: _jsx("span", { className: "dsh-subscription-usage-chip-value", children: formatBalance(wallet) }) }, `${wallet.currency}${wallet.bonus ? ':bonus' : ''}`)))] }), open && (_jsxs("span", { className: "dsh-subscription-usage-chip-panel", role: "status", children: [_jsx("span", { className: "dsh-subscription-usage-chip-full", children: t('usageChipFull', { provider: 'DeepSeek Account' }) }), balance.map((wallet) => (_jsx("span", { className: "dsh-subscription-usage-chip-meter", children: _jsxs("span", { className: "dsh-subscription-usage-chip-meterHead", children: [_jsx("span", { children: `${wallet.bonus ? 'Bonus · ' : ''}${wallet.currency}` }), _jsx("span", { className: "dsh-subscription-usage-chip-detail", children: formatBalance(wallet) })] }) }, `${wallet.currency}${wallet.bonus ? ':bonus' : ''}`)))] }))] }));
329
421
  }
330
- if (reading === undefined)
331
- return null;
332
- const percent = reading.window.usedPercent;
333
- if (percent === undefined)
422
+ if (accountReadings === undefined || accountReadings.length === 0)
334
423
  return null;
335
- const reset = formatReset(reading.window.resetsAt, now);
336
- const clamped = Math.max(0, Math.min(100, percent));
337
- const short = shortLabel(reading.provider, reading.label);
338
- return (_jsxs("span", { className: "dsh-subscription-usage-chip", children: [_jsx("style", { children: usageChipCss }), _jsxs("button", { type: "button", className: "dsh-subscription-usage-chip-trigger", "aria-expanded": open, title: t('usageChipTitle', { provider: reading.label }), onClick: (event) => {
424
+ const short = shortLabel(selected ?? '', selected ?? '');
425
+ // The compact line integrates the whole pool; the panel shows each account's own
426
+ // figures, so a merged glance hides nothing a per-account look cannot recover.
427
+ const merged = mergeAccountWindows(accountReadings);
428
+ return (_jsxs("span", { className: "dsh-subscription-usage-chip", children: [_jsx("style", { children: usageChipCss }), _jsxs("button", { type: "button", className: "dsh-subscription-usage-chip-trigger", "aria-expanded": open, title: t('usageChipTitle', { provider: selected ?? '' }), onClick: (event) => {
339
429
  // The document listener would otherwise close what this click just opened.
340
430
  event.stopPropagation();
341
431
  setOpen((was) => !was);
342
- }, children: [_jsx("span", { className: "dsh-subscription-usage-chip-label", children: short }), reading.windows.map((window) => (_jsxs("span", { className: "dsh-subscription-usage-chip-cell", children: [_jsx("span", { className: "dsh-subscription-usage-chip-window", children: shortWindow(window.label) }), _jsx("span", { className: "dsh-subscription-usage-chip-value", children: `${String(Math.round(window.usedPercent ?? 0))}%` })] }, window.label)))] }), open && (_jsxs("span", { className: "dsh-subscription-usage-chip-panel", role: "status", children: [_jsx("span", { className: "dsh-subscription-usage-chip-full", children: t('usageChipFull', { provider: reading.label }) }), reading.windows.map((window) => {
343
- const windowReset = formatReset(window.resetsAt, now);
344
- return (_jsxs("span", { className: "dsh-subscription-usage-chip-meter", children: [_jsxs("span", { className: "dsh-subscription-usage-chip-meterHead", children: [_jsx("span", { children: window.label }), _jsx("span", { className: "dsh-subscription-usage-chip-detail", children: `${String(Math.round(window.usedPercent ?? 0))}%${windowReset === undefined ? '' : ` · ${windowReset}`}` })] }), _jsx("span", { className: "dsh-subscription-usage-chip-track", "aria-hidden": "true", children: _jsx("span", { className: "dsh-subscription-usage-chip-fill", style: { width: `${String(Math.max(0, Math.min(100, window.usedPercent ?? 0)))}%` } }) })] }, window.label));
345
- }), reading.plan === undefined ? null : (_jsx("span", { className: "dsh-subscription-usage-chip-detail", children: reading.plan }))] }))] }));
432
+ }, children: [_jsx("span", { className: "dsh-subscription-usage-chip-label", children: short }), merged.map((window) => (_jsxs("span", { className: "dsh-subscription-usage-chip-cell", children: [_jsx("span", { className: "dsh-subscription-usage-chip-window", children: shortWindow(window.label) }), _jsx("span", { className: "dsh-subscription-usage-chip-value", children: `${String(Math.round(window.usedPercent ?? 0))}%` })] }, window.label)))] }), open && (_jsxs("span", { className: "dsh-subscription-usage-chip-panel", role: "status", children: [_jsx("span", { className: "dsh-subscription-usage-chip-full", children: t('usageChipFull', { provider: selected ?? '' }) }), accountReadings.map((account) => (_jsxs("span", { className: "dsh-subscription-usage-chip-account", children: [_jsx("span", { className: "dsh-subscription-usage-chip-meterHead", children: _jsx("span", { children: account.plan ?? account.account }) }), account.windows.map((window) => {
433
+ const windowReset = formatReset(window.resetsAt, now);
434
+ // A credit balance reads as money (`used`/`limit`), a quota as a percent.
435
+ const amount = window.usedPercent === undefined && window.used !== undefined && window.limit !== undefined
436
+ ? `${String(window.used)}/${String(window.limit)}`
437
+ : `${String(Math.round(window.usedPercent ?? 0))}%`;
438
+ return (_jsxs("span", { className: "dsh-subscription-usage-chip-meter", children: [_jsxs("span", { className: "dsh-subscription-usage-chip-meterHead", children: [_jsx("span", { children: window.label }), _jsx("span", { className: "dsh-subscription-usage-chip-detail", children: `${amount}${windowReset === undefined ? '' : ` · ${windowReset}`}` })] }), window.usedPercent !== undefined && (_jsx("span", { className: "dsh-subscription-usage-chip-track", "aria-hidden": "true", children: _jsx("span", { className: "dsh-subscription-usage-chip-fill", style: { width: `${String(Math.max(0, Math.min(100, window.usedPercent ?? 0)))}%` } }) }))] }, window.label));
439
+ })] }, account.account)))] }))] }));
346
440
  }
@@ -29,4 +29,4 @@ export declare const subscriptionsCss = "\n.dsh-subscriptions-settings,\n.dsh-su
29
29
  * otherwise read as a raised chip. Every token carries a fallback for hosts that do not
30
30
  * define it.
31
31
  */
32
- export declare const usageChipCss = "\n.dsh-subscription-usage-chip {\n position: relative;\n display: inline-flex;\n align-items: center;\n /* The bar is a centred flex row whose children are this slot's entries followed by the\n shell's context meter, and a list slot renders its entries as bare children (no\n wrapper). So the chip is itself a flex item: a higher order places it after the\n context meter, and no auto margin is set, so it sits immediately beside it rather\n than being pushed to the edge of the bar. */\n order: 1;\n min-width: 0;\n font-family: inherit;\n font-size: 12px;\n line-height: 1;\n}\n.dsh-subscription-usage-chip-trigger {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n margin: 0;\n padding: 2px 6px;\n border: 0;\n border-radius: var(--dsw-radius-sm, 6px);\n background: transparent;\n color: var(--dsw-alias-label-secondary, #8a8f98);\n font: inherit;\n cursor: pointer;\n}\n.dsh-subscription-usage-chip-trigger:hover {\n background: var(--dsw-alias-interactive-bg-hover, #ffffff14);\n}\n.dsh-subscription-usage-chip-trigger:focus-visible {\n outline: 2px solid var(--dsw-alias-border-focus, #4d6bfe);\n outline-offset: 1px;\n}\n.dsh-subscription-usage-chip-label {\n overflow: hidden;\n max-width: 6ch;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.dsh-subscription-usage-chip-cell {\n display: inline-flex;\n align-items: baseline;\n gap: 2px;\n}\n.dsh-subscription-usage-chip-window {\n opacity: 0.75;\n}\n.dsh-subscription-usage-chip-value {\n color: var(--dsw-alias-label-primary, #e8e8e8);\n font-variant-numeric: tabular-nums;\n}\n.dsh-subscription-usage-chip-panel {\n position: absolute;\n bottom: calc(100% + 6px);\n right: 0;\n z-index: 20;\n display: flex;\n flex-direction: column;\n gap: 8px;\n min-width: 168px;\n padding: 10px 12px;\n border: 1px solid var(--dsw-alias-border-secondary, #ffffff1f);\n border-radius: var(--dsw-radius-md, 10px);\n background: var(--dsw-alias-bg-layer-2, #26262a);\n box-shadow: var(--dsw-elevation-prominent, 0 12px 32px #0007);\n color: var(--dsw-alias-label-primary, #e8e8e8);\n white-space: nowrap;\n}\n.dsh-subscription-usage-chip-full {\n font-size: 11px;\n color: var(--dsw-alias-label-secondary, #8a8f98);\n}\n.dsh-subscription-usage-chip-meter {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n.dsh-subscription-usage-chip-meterHead {\n display: flex;\n align-items: baseline;\n justify-content: space-between;\n gap: 12px;\n font-size: 12px;\n}\n.dsh-subscription-usage-chip-detail {\n color: var(--dsw-alias-label-secondary, #8a8f98);\n font-variant-numeric: tabular-nums;\n}\n.dsh-subscription-usage-chip-track {\n display: block;\n width: 100%;\n height: 5px;\n border-radius: 999px;\n /* As on the settings meter: an unspent window has to show its track, so the neutral and\n its ring are explicit rather than a layer token that may match the panel. */\n background: #8a8f9826;\n box-shadow: inset 0 0 0 1px #8a8f9833;\n overflow: hidden;\n}\n.dsh-subscription-usage-chip-fill {\n display: block;\n height: 100%;\n background: var(--dsw-alias-state-success-primary, #067647);\n}\n";
32
+ export declare const usageChipCss = "\n.dsh-subscription-usage-chip {\n position: relative;\n display: inline-flex;\n align-items: center;\n /* The bar is a centred flex row whose children are this slot's entries followed by the\n shell's context meter, and a list slot renders its entries as bare children (no\n wrapper). So the chip is itself a flex item: a higher order places it after the\n context meter, and no auto margin is set, so it sits immediately beside it rather\n than being pushed to the edge of the bar. */\n order: 1;\n min-width: 0;\n font-family: inherit;\n font-size: 12px;\n line-height: 1;\n}\n.dsh-subscription-usage-chip-trigger {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n margin: 0;\n padding: 2px 6px;\n border: 0;\n border-radius: var(--dsw-radius-sm, 6px);\n background: transparent;\n color: var(--dsw-alias-label-secondary, #8a8f98);\n font: inherit;\n cursor: pointer;\n}\n.dsh-subscription-usage-chip-trigger:hover {\n background: var(--dsw-alias-interactive-bg-hover, #ffffff14);\n}\n.dsh-subscription-usage-chip-trigger:focus-visible {\n outline: 2px solid var(--dsw-alias-border-focus, #4d6bfe);\n outline-offset: 1px;\n}\n.dsh-subscription-usage-chip-label {\n overflow: hidden;\n max-width: 6ch;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.dsh-subscription-usage-chip-cell {\n display: inline-flex;\n align-items: baseline;\n gap: 2px;\n}\n.dsh-subscription-usage-chip-window {\n opacity: 0.75;\n}\n.dsh-subscription-usage-chip-value {\n color: var(--dsw-alias-label-primary, #e8e8e8);\n font-variant-numeric: tabular-nums;\n}\n.dsh-subscription-usage-chip-account {\n display: flex;\n flex-direction: column;\n gap: 6px;\n /* A hairline between accounts, so two sign-ins read as two groups not one run. */\n padding-top: 6px;\n border-top: 1px solid var(--dsw-alias-border-secondary, #ffffff14);\n}\n.dsh-subscription-usage-chip-account:first-of-type {\n padding-top: 0;\n border-top: 0;\n}\n.dsh-subscription-usage-chip-panel {\n position: absolute;\n bottom: calc(100% + 6px);\n right: 0;\n z-index: 20;\n display: flex;\n flex-direction: column;\n gap: 10px;\n min-width: 180px;\n padding: 12px 14px;\n border: 1px solid var(--dsw-alias-border-secondary, #ffffff1f);\n border-radius: var(--dsw-radius-md, 10px);\n background: var(--dsw-alias-bg-layer-2, #26262a);\n box-shadow: var(--dsw-elevation-prominent, 0 12px 32px #0007);\n color: var(--dsw-alias-label-primary, #e8e8e8);\n white-space: nowrap;\n}\n.dsh-subscription-usage-chip-full {\n font-size: 11px;\n color: var(--dsw-alias-label-secondary, #8a8f98);\n}\n.dsh-subscription-usage-chip-meter {\n display: flex;\n flex-direction: column;\n gap: 5px;\n}\n.dsh-subscription-usage-chip-meterHead {\n display: flex;\n align-items: baseline;\n justify-content: space-between;\n gap: 12px;\n font-size: 12px;\n}\n.dsh-subscription-usage-chip-detail {\n color: var(--dsw-alias-label-secondary, #8a8f98);\n font-variant-numeric: tabular-nums;\n}\n.dsh-subscription-usage-chip-track {\n display: block;\n width: 100%;\n height: 5px;\n border-radius: 999px;\n /* As on the settings meter: an unspent window has to show its track, so the neutral and\n its ring are explicit rather than a layer token that may match the panel. */\n background: #8a8f9826;\n box-shadow: inset 0 0 0 1px #8a8f9833;\n overflow: hidden;\n}\n.dsh-subscription-usage-chip-fill {\n display: block;\n height: 100%;\n background: var(--dsw-alias-state-success-primary, #067647);\n}\n";
@@ -328,6 +328,18 @@ export const usageChipCss = `
328
328
  color: var(--dsw-alias-label-primary, #e8e8e8);
329
329
  font-variant-numeric: tabular-nums;
330
330
  }
331
+ .dsh-subscription-usage-chip-account {
332
+ display: flex;
333
+ flex-direction: column;
334
+ gap: 6px;
335
+ /* A hairline between accounts, so two sign-ins read as two groups not one run. */
336
+ padding-top: 6px;
337
+ border-top: 1px solid var(--dsw-alias-border-secondary, #ffffff14);
338
+ }
339
+ .dsh-subscription-usage-chip-account:first-of-type {
340
+ padding-top: 0;
341
+ border-top: 0;
342
+ }
331
343
  .dsh-subscription-usage-chip-panel {
332
344
  position: absolute;
333
345
  bottom: calc(100% + 6px);
@@ -335,9 +347,9 @@ export const usageChipCss = `
335
347
  z-index: 20;
336
348
  display: flex;
337
349
  flex-direction: column;
338
- gap: 8px;
339
- min-width: 168px;
340
- padding: 10px 12px;
350
+ gap: 10px;
351
+ min-width: 180px;
352
+ padding: 12px 14px;
341
353
  border: 1px solid var(--dsw-alias-border-secondary, #ffffff1f);
342
354
  border-radius: var(--dsw-radius-md, 10px);
343
355
  background: var(--dsw-alias-bg-layer-2, #26262a);
@@ -352,7 +364,7 @@ export const usageChipCss = `
352
364
  .dsh-subscription-usage-chip-meter {
353
365
  display: flex;
354
366
  flex-direction: column;
355
- gap: 4px;
367
+ gap: 5px;
356
368
  }
357
369
  .dsh-subscription-usage-chip-meterHead {
358
370
  display: flex;