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.
- package/lib/auth/allowance-scheduler.d.ts +58 -0
- package/lib/auth/allowance-scheduler.js +72 -0
- package/lib/client/UsageChip.d.ts +58 -2
- package/lib/client/UsageChip.js +116 -22
- package/lib/client/styles.d.ts +1 -1
- package/lib/client/styles.js +16 -4
- package/lib/client.js +106 -58
- package/lib/client.js.map +1 -1
- package/lib/providers/antigravity.d.ts +6 -0
- package/lib/providers/antigravity.js +65 -14
- package/lib/providers/ark-provider.js +8 -0
- package/lib/providers/ark.d.ts +3 -0
- package/lib/providers/ark.js +4 -1
- package/lib/providers/glm-provider.js +5 -0
- package/lib/providers/glm.d.ts +2 -0
- package/lib/providers/glm.js +4 -1
- package/lib/providers/grok.d.ts +3 -0
- package/lib/providers/grok.js +9 -1
- package/lib/providers/kimi-provider.js +5 -0
- package/lib/providers/kimi.d.ts +9 -0
- package/lib/providers/kimi.js +8 -1
- package/lib/providers/responses-stream.d.ts +44 -0
- package/lib/providers/responses-stream.js +125 -30
- package/package.json +1 -1
|
@@ -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
|
|
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.
|
package/lib/client/UsageChip.js
CHANGED
|
@@ -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
|
|
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) ?
|
|
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
|
|
147
|
+
* Reduce a route's per-account usage to one reading per account.
|
|
146
148
|
*
|
|
147
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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 }),
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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
|
}
|
package/lib/client/styles.d.ts
CHANGED
|
@@ -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:
|
|
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";
|
package/lib/client/styles.js
CHANGED
|
@@ -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:
|
|
339
|
-
min-width:
|
|
340
|
-
padding:
|
|
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:
|
|
367
|
+
gap: 5px;
|
|
356
368
|
}
|
|
357
369
|
.dsh-subscription-usage-chip-meterHead {
|
|
358
370
|
display: flex;
|