dsh-subscriptions 0.1.1 → 0.1.3
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/README.md +115 -4
- package/icon.svg +28 -0
- package/lib/client/CursorCard.js +15 -11
- package/lib/client/ExternalUsageCards.d.ts +11 -1
- package/lib/client/ExternalUsageCards.js +28 -13
- package/lib/client/SubscriptionUsageBadge.d.ts +25 -4
- package/lib/client/SubscriptionUsageBadge.js +115 -28
- package/lib/client/SubscriptionsSection.d.ts +7 -2
- package/lib/client/SubscriptionsSection.js +9 -13
- package/lib/client/UsageBadgeDisplaySetting.js +19 -2
- package/lib/client/UsageMeter.d.ts +8 -0
- package/lib/client/UsageMeter.js +56 -0
- package/lib/client/host-contract.d.ts +1 -1
- package/lib/client/host-contract.js +1 -0
- package/lib/client/locales.d.ts +50 -0
- package/lib/client/locales.js +52 -2
- package/lib/client/subscription-card-styles.js +0 -5
- package/lib/client/usage-badge-preferences.d.ts +7 -2
- package/lib/client/usage-badge-preferences.js +3 -2
- package/lib/client/usage-color-preferences.d.ts +6 -0
- package/lib/client/usage-color-preferences.js +38 -0
- package/lib/client/usage-pace.d.ts +42 -0
- package/lib/client/usage-pace.js +71 -0
- package/lib/client.js +489 -132
- package/lib/client.js.map +1 -1
- package/lib/index.js +252 -35
- package/lib/providers/claude.js +7 -0
- package/lib/providers/codex.js +5 -0
- package/lib/providers/common.d.ts +10 -0
- package/lib/providers/cursor-usage.js +6 -2
- package/lib/providers/external-usage-controller.d.ts +1 -1
- package/lib/providers/external-usage-controller.js +8 -3
- package/lib/providers/external-usage.d.ts +1 -1
- package/lib/providers/external-usage.js +62 -2
- package/lib/providers/grok.js +3 -1
- package/lib/providers/minimax-usage.d.ts +24 -0
- package/lib/providers/minimax-usage.js +85 -0
- package/lib/providers/pool-usage.js +6 -4
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +7 -3
|
@@ -18,15 +18,18 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
18
18
|
* The collapsed pill reads only the default account — the same account
|
|
19
19
|
* direct (non-pool) routes serve — so it stays one short segment even for a
|
|
20
20
|
* provider with several accounts connected; the dialog shows them all.
|
|
21
|
-
* Every color resolves through a `--dsw-*` design token
|
|
22
|
-
*
|
|
21
|
+
* Every color resolves through a `--dsw-*` design token, the dialog wears the
|
|
22
|
+
* host's menu material (its translucent fill plus its backdrop blur), and
|
|
23
|
+
* every user-visible string goes through the locale `t` of the
|
|
23
24
|
* 'settings.subscriptions' namespace.
|
|
24
25
|
*/
|
|
25
26
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
26
27
|
import { createPortal } from 'react-dom';
|
|
27
28
|
import * as primitives from '@deepseek-ai/dsh-client-ui-primitives';
|
|
28
29
|
import { useAnchoredPosition, useDismissOnOutsidePointer } from '@deepseek-ai/dsh-client-ui-primitives';
|
|
29
|
-
import { callSubscriptionsAuth
|
|
30
|
+
import { callSubscriptionsAuth } from './SubscriptionsSection.js';
|
|
31
|
+
import { UsageMeter } from './UsageMeter.js';
|
|
32
|
+
import { displayUsedPercent } from './usage-pace.js';
|
|
30
33
|
import { en } from './locales.js';
|
|
31
34
|
import { hostIcon } from './host-icons.js';
|
|
32
35
|
import { USAGE_BADGE_REFRESH_EVENT, useUsageBadgeMode } from './usage-badge-preferences.js';
|
|
@@ -39,6 +42,8 @@ const UsageBadgeIcon = usageBadgeIcon(primitives);
|
|
|
39
42
|
const USAGE_POLL_INTERVAL_MS = 15 * 60_000;
|
|
40
43
|
/** How often the badge re-reads the session's current model (model switches arrive only by asking). */
|
|
41
44
|
const MODEL_POLL_INTERVAL_MS = 3000;
|
|
45
|
+
/** How long the rotation holds one provider in `always` mode (a display cadence, not a data poll). */
|
|
46
|
+
const BADGE_ROTATION_INTERVAL_MS = 10_000;
|
|
42
47
|
/** Distance between the trigger's top edge and the dialog's bottom (host stat dialogs use the same). */
|
|
43
48
|
const PANEL_GAP = 8;
|
|
44
49
|
/** Distance kept between the dialog and each viewport edge. */
|
|
@@ -56,6 +61,8 @@ const PROVIDER_NAMES = {
|
|
|
56
61
|
'cursor-subscription': 'Cursor',
|
|
57
62
|
'opencode-go': 'OpenCode Go',
|
|
58
63
|
'kimi-coding': 'Kimi Code',
|
|
64
|
+
'minimax': 'MiniMax',
|
|
65
|
+
'minimax-cn': 'MiniMax CN',
|
|
59
66
|
};
|
|
60
67
|
/** Key of one account's cached windows: `provider:accountKey`. */
|
|
61
68
|
function usageKeyOf(provider, account) {
|
|
@@ -119,7 +126,7 @@ export async function loadBadgeRoster(rpc) {
|
|
|
119
126
|
}
|
|
120
127
|
if (external.status === 'fulfilled') {
|
|
121
128
|
for (const [source, provider] of [
|
|
122
|
-
['opencode-go', 'opencode-go'], ['kimi-code', 'kimi-coding'],
|
|
129
|
+
['opencode-go', 'opencode-go'], ['kimi-code', 'kimi-coding'], ['minimax', 'minimax'], ['minimax-cn', 'minimax-cn'],
|
|
123
130
|
]) {
|
|
124
131
|
refreshed.add(provider);
|
|
125
132
|
if (external.value[source]?.configured)
|
|
@@ -133,7 +140,7 @@ export async function loadBadgeRoster(rpc) {
|
|
|
133
140
|
export async function usageOf(rpc, { provider, account }) {
|
|
134
141
|
if (provider === 'cursor-subscription')
|
|
135
142
|
return callSubscriptionsAuth(rpc, 'cursorUsage', {});
|
|
136
|
-
if (provider === 'opencode-go' || provider === 'kimi-coding') {
|
|
143
|
+
if (provider === 'opencode-go' || provider === 'kimi-coding' || provider === 'minimax' || provider === 'minimax-cn') {
|
|
137
144
|
return callSubscriptionsAuth(rpc, 'externalUsage', { source: provider === 'kimi-coding' ? 'kimi-code' : provider });
|
|
138
145
|
}
|
|
139
146
|
return callSubscriptionsAuth(rpc, 'usage', { provider, account: account.key });
|
|
@@ -160,7 +167,7 @@ export function createCurrentModelReader(models, sessionId) {
|
|
|
160
167
|
* Falls back to the scope/kind abbreviation when no reset time is known.
|
|
161
168
|
*/
|
|
162
169
|
export function windowLabel(w) {
|
|
163
|
-
if (w.resetsAt === undefined) {
|
|
170
|
+
if (w.resetsAt === undefined || !Number.isFinite(w.resetsAt)) {
|
|
164
171
|
if (w.scope !== undefined && w.scope !== '')
|
|
165
172
|
return w.scope;
|
|
166
173
|
switch (w.kind) {
|
|
@@ -179,9 +186,13 @@ export function windowLabel(w) {
|
|
|
179
186
|
return `${hours}h${minutes % 60}m`;
|
|
180
187
|
return `${Math.max(1, minutes)}m`;
|
|
181
188
|
}
|
|
182
|
-
/**
|
|
183
|
-
|
|
184
|
-
|
|
189
|
+
/**
|
|
190
|
+
* One compact segment piece (`6d1h 25%`); an unusable provider percentage
|
|
191
|
+
* renders as the no-data dash, consistent with the meter's unavailable state.
|
|
192
|
+
*/
|
|
193
|
+
function windowSegment(label, w) {
|
|
194
|
+
const percent = displayUsedPercent(w.usedPercent);
|
|
195
|
+
return percent === undefined ? `${label} —` : `${label} ${percent}%`;
|
|
185
196
|
}
|
|
186
197
|
/** Keep model quotas separate: matching percentages do not imply a shared pool. */
|
|
187
198
|
export function prioritizeWindows(windows, model) {
|
|
@@ -210,10 +221,10 @@ export function compactSegment(d, model, t = fallbackTranslate) {
|
|
|
210
221
|
count: new Set(windows.map(w => w.scope).filter(Boolean)).size,
|
|
211
222
|
})}`;
|
|
212
223
|
}
|
|
213
|
-
const parts = matching.slice(0, 2).map(w =>
|
|
224
|
+
const parts = matching.slice(0, 2).map(w => windowSegment(w.kind === 'weekly' ? t('usageWeekly') : t('usageWindow'), w));
|
|
214
225
|
return `${d.name} ${parts.join(' · ')}`;
|
|
215
226
|
}
|
|
216
|
-
const parts = windows.slice(0, 2).map(w =>
|
|
227
|
+
const parts = windows.slice(0, 2).map(w => windowSegment(windowLabel(w), w));
|
|
217
228
|
if (windows.length > 2)
|
|
218
229
|
parts.push(`+${windows.length - 2}`);
|
|
219
230
|
return `${d.name} ${parts.join(' · ')}`;
|
|
@@ -227,6 +238,27 @@ export function collapsedDisplays(displays, current) {
|
|
|
227
238
|
const match = displays.find(d => d.provider === current);
|
|
228
239
|
return match === undefined ? [] : [match];
|
|
229
240
|
}
|
|
241
|
+
/**
|
|
242
|
+
* `always` mode: the current model's provider while it reports usage,
|
|
243
|
+
* otherwise the rotation steps through the providers that do, in display
|
|
244
|
+
* order, wrapping around. The index is taken modulo the current provider
|
|
245
|
+
* count, so providers appearing or dropping out stay inside range; the
|
|
246
|
+
* sequence itself may shift when the roster changes. Unlike `recent`, the
|
|
247
|
+
* retained subscription does not pin the pill — that retention is exactly
|
|
248
|
+
* what this mode exists to escape.
|
|
249
|
+
* @param displays - providers with usage, in display order.
|
|
250
|
+
* @param current - the provider of the session's current model.
|
|
251
|
+
* @param step - rotation tick; consecutive ticks advance one provider.
|
|
252
|
+
*/
|
|
253
|
+
export function rotatingDisplay(displays, current, step) {
|
|
254
|
+
if (displays.length === 0)
|
|
255
|
+
return [];
|
|
256
|
+
const match = displays.find(d => d.provider === current);
|
|
257
|
+
if (match !== undefined)
|
|
258
|
+
return [match];
|
|
259
|
+
const index = Number.isFinite(step) ? Math.floor(step) : 0;
|
|
260
|
+
return [displays[((index % displays.length) + displays.length) % displays.length]];
|
|
261
|
+
}
|
|
230
262
|
/** Order for the expanded dialog: the current provider first, the rest in poll order. */
|
|
231
263
|
export function expandedDisplays(displays, current) {
|
|
232
264
|
const match = displays.find(d => d.provider === current);
|
|
@@ -262,7 +294,7 @@ function usageWindowLabel(t, window) {
|
|
|
262
294
|
* every provider's accounts and their windows. Returns null when no data is
|
|
263
295
|
* available.
|
|
264
296
|
*/
|
|
265
|
-
export function SubscriptionUsageBadge({ rpc, currentModel, t }) {
|
|
297
|
+
export function SubscriptionUsageBadge({ rpc, currentModel, t, rotationMs }) {
|
|
266
298
|
const translate = t ?? fallbackTranslate;
|
|
267
299
|
const [displays, setDisplays] = useState([]);
|
|
268
300
|
const [selection, setSelection] = useState(undefined);
|
|
@@ -272,6 +304,11 @@ export function SubscriptionUsageBadge({ rpc, currentModel, t }) {
|
|
|
272
304
|
const badgeSelection = retainSubscriptionSelection(lastSubscription, selection);
|
|
273
305
|
const [open, setOpen] = useState(false);
|
|
274
306
|
const [hover, setHover] = useState(false);
|
|
307
|
+
// Rotation tick for `always` mode; the interval runs only in that mode.
|
|
308
|
+
const [rotation, setRotation] = useState(0);
|
|
309
|
+
// The provider the pill showed when the dialog opened; an open dialog
|
|
310
|
+
// keeps leading with it while the rotation keeps ticking underneath.
|
|
311
|
+
const [dialogLead, setDialogLead] = useState(undefined);
|
|
275
312
|
const inflightRef = useRef(false);
|
|
276
313
|
const mountedRef = useRef(true);
|
|
277
314
|
const rootRef = useRef(null);
|
|
@@ -287,8 +324,10 @@ export function SubscriptionUsageBadge({ rpc, currentModel, t }) {
|
|
|
287
324
|
// across a failed poll (e.g. a 429 during the server's own negative-cache
|
|
288
325
|
// cooldown) so a row doesn't flicker away — it only disappears once the
|
|
289
326
|
// account actually logs out or a fetch succeeds but reports the window as
|
|
290
|
-
// unsupported.
|
|
327
|
+
// unsupported. Observation metadata (freshness) shares this lifecycle: an
|
|
328
|
+
// entry is dropped wherever its windows are dropped.
|
|
291
329
|
const lastKnownRef = useRef(new Map());
|
|
330
|
+
const observationsRef = useRef(new Map());
|
|
292
331
|
const refresh = useCallback(async () => {
|
|
293
332
|
if (rpc === undefined || inflightRef.current)
|
|
294
333
|
return;
|
|
@@ -302,8 +341,10 @@ export function SubscriptionUsageBadge({ rpc, currentModel, t }) {
|
|
|
302
341
|
// real signal, unlike a fetch failure.
|
|
303
342
|
const live = new Set(roster.map(({ provider, account }) => usageKeyOf(provider, account)));
|
|
304
343
|
for (const key of lastKnown.keys()) {
|
|
305
|
-
if (refreshed.has(key.split(':', 1)[0]) && !live.has(key))
|
|
344
|
+
if (refreshed.has(key.split(':', 1)[0]) && !live.has(key)) {
|
|
306
345
|
lastKnown.delete(key);
|
|
346
|
+
observationsRef.current.delete(key);
|
|
347
|
+
}
|
|
307
348
|
}
|
|
308
349
|
const results = await Promise.allSettled(roster.map(async ({ provider, account }) => {
|
|
309
350
|
const usage = await usageOf(rpc, { provider, account });
|
|
@@ -311,6 +352,10 @@ export function SubscriptionUsageBadge({ rpc, currentModel, t }) {
|
|
|
311
352
|
}));
|
|
312
353
|
if (!mountedRef.current)
|
|
313
354
|
return;
|
|
355
|
+
for (const { provider, account } of roster) {
|
|
356
|
+
const key = usageKeyOf(provider, account);
|
|
357
|
+
observationsRef.current.set(key, { ...observationsRef.current.get(key), stale: true });
|
|
358
|
+
}
|
|
314
359
|
const plans = new Map();
|
|
315
360
|
for (const r of results) {
|
|
316
361
|
if (r.status !== 'fulfilled')
|
|
@@ -321,17 +366,29 @@ export function SubscriptionUsageBadge({ rpc, currentModel, t }) {
|
|
|
321
366
|
plans.set(key, usage.plan);
|
|
322
367
|
if (!usage.supported || !usage.windows || usage.windows.length === 0) {
|
|
323
368
|
lastKnown.delete(key);
|
|
369
|
+
observationsRef.current.delete(key);
|
|
324
370
|
continue;
|
|
325
371
|
}
|
|
326
372
|
lastKnown.set(key, usage.windows);
|
|
373
|
+
observationsRef.current.set(key, { observedAt: usage.observedAt ?? Date.now(), stale: usage.stale === true });
|
|
327
374
|
}
|
|
328
375
|
setDisplays(previous => [
|
|
329
|
-
...groupUsageDisplays(roster, lastKnown, plans),
|
|
330
|
-
|
|
376
|
+
...groupUsageDisplays(roster, lastKnown, plans).map(display => ({ ...display,
|
|
377
|
+
accounts: display.accounts.map(account => ({ ...account,
|
|
378
|
+
...observationsRef.current.get(`${display.provider}:${account.key}`),
|
|
379
|
+
})),
|
|
380
|
+
})),
|
|
381
|
+
...previous.filter(display => !refreshed.has(display.provider)).map(display => ({ ...display,
|
|
382
|
+
accounts: display.accounts.map(account => ({ ...account, stale: true })),
|
|
383
|
+
})),
|
|
331
384
|
]);
|
|
332
385
|
}
|
|
333
386
|
catch {
|
|
334
|
-
//
|
|
387
|
+
// Keep cached numbers, but never present failed observations as current.
|
|
388
|
+
if (mountedRef.current)
|
|
389
|
+
setDisplays(previous => previous.map(display => ({ ...display,
|
|
390
|
+
accounts: display.accounts.map(account => ({ ...account, stale: true })),
|
|
391
|
+
})));
|
|
335
392
|
}
|
|
336
393
|
finally {
|
|
337
394
|
inflightRef.current = false;
|
|
@@ -414,22 +471,49 @@ export function SubscriptionUsageBadge({ rpc, currentModel, t }) {
|
|
|
414
471
|
if (displayMode === 'hidden')
|
|
415
472
|
setOpen(false);
|
|
416
473
|
}, [displayMode]);
|
|
474
|
+
// `always` mode advances the rotation on its own cadence; other modes
|
|
475
|
+
// never advance, so the pill never changes on its own.
|
|
476
|
+
useEffect(() => {
|
|
477
|
+
if (displayMode !== 'always')
|
|
478
|
+
return;
|
|
479
|
+
const interval = rotationMs !== undefined && Number.isFinite(rotationMs) && rotationMs > 0
|
|
480
|
+
? rotationMs
|
|
481
|
+
: BADGE_ROTATION_INTERVAL_MS;
|
|
482
|
+
const timer = setInterval(() => { setRotation(step => step + 1); }, interval);
|
|
483
|
+
return () => { clearInterval(timer); };
|
|
484
|
+
}, [displayMode, rotationMs]);
|
|
417
485
|
const seat = _jsx("span", { ref: seatRef, style: styles.seat, "aria-hidden": true });
|
|
418
|
-
|
|
486
|
+
// `always` follows the live model, not the retained subscription: the
|
|
487
|
+
// retention pins the pill in `recent`, which is what this mode escapes.
|
|
488
|
+
const collapsed = displayMode === 'always'
|
|
489
|
+
? rotatingDisplay(displays, current, rotation)
|
|
490
|
+
: collapsedDisplays(displays, badgeSelection?.provider);
|
|
419
491
|
if (displayMode === 'hidden' || collapsed.length === 0)
|
|
420
492
|
return seat;
|
|
421
|
-
|
|
493
|
+
// Model scope only while the pill shows the current model's provider;
|
|
494
|
+
// a rotated provider has no live model, so it reads at account level.
|
|
495
|
+
const pillModel = displayMode === 'always'
|
|
496
|
+
? (current === collapsed[0]?.provider ? selection?.model : undefined)
|
|
497
|
+
: badgeSelection?.model;
|
|
498
|
+
const label = compactSegment(collapsed[0], pillModel, translate);
|
|
422
499
|
// The dialog opens from the badge, so it follows the provider and model the
|
|
423
500
|
// badge shows (a retained subscription when the current model is not one).
|
|
424
|
-
|
|
501
|
+
// In `always` mode the pill moves under the cursor, so an open dialog keeps
|
|
502
|
+
// the provider that was showing when it opened instead of reordering live.
|
|
503
|
+
const dialogProvider = open && displayMode === 'always' ? dialogLead : badgeSelection?.provider;
|
|
504
|
+
const dialogModel = open && displayMode === 'always'
|
|
505
|
+
? (dialogLead === current ? selection?.model : undefined)
|
|
506
|
+
: badgeSelection?.model;
|
|
507
|
+
const expanded = expandedDisplays(displays, dialogProvider);
|
|
425
508
|
const title = translate('usageBadgeTitle');
|
|
426
509
|
const toggle = () => {
|
|
427
510
|
const next = !open;
|
|
511
|
+
setDialogLead(collapsed[0]?.provider);
|
|
428
512
|
setOpen(next);
|
|
429
513
|
if (next)
|
|
430
514
|
void refresh();
|
|
431
515
|
};
|
|
432
|
-
const pill = (_jsxs("span", { ref: rootRef, style: styles.anchor, children: [_jsxs("button", { type: "button", style: { ...styles.pill, ...(hover || open ? styles.pillActive : {}) }, "aria-haspopup": "dialog", "aria-expanded": open, "aria-label": `${title} · ${label}`, title: title, onMouseEnter: () => { setHover(true); }, onMouseLeave: () => { setHover(false); }, onClick: toggle, children: [_jsx(UsageBadgeIcon, {}), _jsx("span", { style: styles.label, children: label })] }), open && createPortal(_jsxs("div", { ref: panelRef, role: "dialog", "aria-label": title, style: { ...styles.panel, ...(pos ?? MEASURE_STYLE) }, children: [_jsx("div", { style: styles.title, children: _jsxs("span", { style: styles.titleLabel, children: [_jsx(UsageBadgeIcon, {}), title] }) }), _jsx("div", { style: styles.titleRule, "aria-hidden": true }), expanded.map((d, index) => (_jsxs("section", { style: index === 0 ? undefined : styles.section, children: [_jsxs("div", { style: styles.providerRow, children: [_jsxs("span", { style: styles.providerName, children: [d.name, d.provider === current && _jsx("span", { style: styles.currentTag, children: translate('usageBadgeCurrent') })] }), d.accounts.length === 1 && _jsx(AccountMeta, { account: d.accounts[0], translate: translate })] }), d.accounts.map((account, accountIndex) => (_jsxs("div", { style: accountIndex === 0 ? undefined : styles.accountBlock, children: [d.accounts.length > 1 && (_jsx("div", { style: styles.accountRow, children: _jsx(AccountMeta, { account: account, translate: translate }) })), _jsx(AccountWindows, { windows: account.windows, model: d.provider ===
|
|
516
|
+
const pill = (_jsxs("span", { ref: rootRef, style: styles.anchor, children: [_jsxs("button", { type: "button", style: { ...styles.pill, ...(hover || open ? styles.pillActive : {}) }, "aria-haspopup": "dialog", "aria-expanded": open, "aria-label": `${title} · ${label}`, title: title, onMouseEnter: () => { setHover(true); }, onMouseLeave: () => { setHover(false); }, onClick: toggle, children: [_jsx(UsageBadgeIcon, {}), _jsx("span", { style: styles.label, children: label })] }), open && createPortal(_jsxs("div", { ref: panelRef, role: "dialog", "aria-label": title, style: { ...styles.panel, ...(pos ?? MEASURE_STYLE) }, children: [_jsx("div", { style: styles.title, children: _jsxs("span", { style: styles.titleLabel, children: [_jsx(UsageBadgeIcon, {}), title] }) }), _jsx("div", { style: styles.titleRule, "aria-hidden": true }), expanded.map((d, index) => (_jsxs("section", { style: index === 0 ? undefined : styles.section, children: [_jsxs("div", { style: styles.providerRow, children: [_jsxs("span", { style: styles.providerName, children: [d.name, d.provider === current && _jsx("span", { style: styles.currentTag, children: translate('usageBadgeCurrent') })] }), d.accounts.length === 1 && _jsx(AccountMeta, { account: d.accounts[0], translate: translate })] }), d.accounts.map((account, accountIndex) => (_jsxs("div", { style: accountIndex === 0 ? undefined : styles.accountBlock, children: [d.accounts.length > 1 && (_jsx("div", { style: styles.accountRow, children: _jsx(AccountMeta, { account: account, translate: translate }) })), _jsx(AccountWindows, { windows: account.windows, observedAt: account.observedAt, stale: account.stale, model: d.provider === dialogProvider ? dialogModel : undefined, provider: d.provider, translate: translate }, `${d.provider}:${dialogModel ?? ''}`)] }, account.key)))] }, d.provider)))] }), document.body)] }));
|
|
433
517
|
return (_jsxs(_Fragment, { children: [seat, statsRow !== null && statsRow.isConnected ? createPortal(pill, statsRow) : pill] }));
|
|
434
518
|
}
|
|
435
519
|
/**
|
|
@@ -457,15 +541,15 @@ function AccountMeta({ account, translate }) {
|
|
|
457
541
|
return (_jsxs("span", { style: styles.providerMeta, title: account.account, children: [account.isDefault && _jsx("span", { style: styles.defaultStar, "aria-label": "default", children: "\u2605 " }), parts.join(' · ')] }));
|
|
458
542
|
}
|
|
459
543
|
/** Preview each account independently; all remaining quotas stay accessible. */
|
|
460
|
-
export function AccountWindows({ windows, model, provider, translate }) {
|
|
544
|
+
export function AccountWindows({ windows, model, provider, translate, observedAt, stale }) {
|
|
461
545
|
const { shown, hidden } = previewWindows(windows, model, provider);
|
|
462
|
-
const rows = (items) => (_jsx("dl", { style: styles.details, children: items.map((w, i) => (_jsx(WindowRow, { label: `${usageWindowLabel(translate, w)}${model !== undefined && w.scope === model ? ` · ${translate('usageBadgeCurrent')}` : ''}`, window: w }, i))) }));
|
|
546
|
+
const rows = (items) => (_jsx("dl", { style: styles.details, children: items.map((w, i) => (_jsx(WindowRow, { label: `${usageWindowLabel(translate, w)}${model !== undefined && w.scope === model ? ` · ${translate('usageBadgeCurrent')}` : ''}`, window: w, t: translate, observedAt: observedAt, stale: stale }, i))) }));
|
|
463
547
|
return _jsxs(_Fragment, { children: [rows(shown), hidden.length > 0 && _jsxs("details", { style: styles.moreWindows, children: [_jsx("summary", { style: styles.moreSummary, children: translate('usageBadgeMoreWindows', { count: hidden.length }) }), rows(hidden)] })] });
|
|
464
548
|
}
|
|
465
549
|
/** One `dt`/`dd` pair: window name → `25% · 6d1h`, with the bar underneath. */
|
|
466
|
-
function WindowRow({ label, window: w }) {
|
|
467
|
-
const percent =
|
|
468
|
-
return (_jsxs(_Fragment, { children: [_jsx("dt", { style: styles.dt, children: label }), _jsxs("dd", { style: styles.dd, children: [percent
|
|
550
|
+
function WindowRow({ label, window: w, t, observedAt, stale }) {
|
|
551
|
+
const percent = displayUsedPercent(w.usedPercent);
|
|
552
|
+
return (_jsxs(_Fragment, { children: [_jsx("dt", { style: styles.dt, children: label }), _jsxs("dd", { style: styles.dd, children: [percent === undefined ? t('usageMeterInvalid') : `${percent}%`, w.resetsAt !== undefined && Number.isFinite(w.resetsAt) && _jsxs("span", { style: styles.reset, children: [" \u00B7 ", windowLabel(w)] })] }), _jsx(UsageMeter, { window: w, t: t, style: styles.bar, observedAt: observedAt, stale: stale })] }));
|
|
469
553
|
}
|
|
470
554
|
/**
|
|
471
555
|
* Unplaced portal panel: hidden but laid out so the clamp measures real
|
|
@@ -490,10 +574,14 @@ const styles = {
|
|
|
490
574
|
color: 'var(--dsw-alias-label-secondary)',
|
|
491
575
|
},
|
|
492
576
|
label: { textOverflow: 'ellipsis', minWidth: 0, overflow: 'hidden' },
|
|
493
|
-
// Mirrors the host stat-dialog panel.
|
|
577
|
+
// Mirrors the host stat-dialog panel. The menu fill is translucent by
|
|
578
|
+
// design and only the host's backdrop blur keeps it readable as a surface:
|
|
579
|
+
// without the blur the transcript behind the dialog stays sharp and shows
|
|
580
|
+
// through the fill.
|
|
494
581
|
panel: {
|
|
495
582
|
position: 'fixed', zIndex: 1100, boxSizing: 'border-box',
|
|
496
583
|
background: 'var(--dsw-specific-menu)',
|
|
584
|
+
backdropFilter: 'var(--dsw-menu-backdrop-filter)',
|
|
497
585
|
width: 'max-content', minWidth: 'min(300px, 100vw - 24px)', maxWidth: 'min(440px, 100vw - 24px)',
|
|
498
586
|
maxHeight: 'min(560px, 100dvh - 24px)', overflowY: 'auto', overscrollBehavior: 'contain',
|
|
499
587
|
boxShadow: 'var(--dsw-elevation-prominent)',
|
|
@@ -539,5 +627,4 @@ const styles = {
|
|
|
539
627
|
gridColumn: '1 / -1', height: 4, borderRadius: 2, overflow: 'hidden',
|
|
540
628
|
background: 'var(--dsw-alias-border-l2)', marginBottom: 2,
|
|
541
629
|
},
|
|
542
|
-
barFill: { height: '100%', borderRadius: 2 },
|
|
543
630
|
};
|
|
@@ -30,9 +30,14 @@ export interface UsageWindow {
|
|
|
30
30
|
scope?: string;
|
|
31
31
|
usedPercent: number;
|
|
32
32
|
resetsAt?: number;
|
|
33
|
+
startsAt?: number;
|
|
34
|
+
windowDurationMs?: number;
|
|
35
|
+
fixedWindow?: boolean;
|
|
33
36
|
}
|
|
34
37
|
/** `usage` endpoint value: the node half owns this shape. */
|
|
35
38
|
export interface ProviderUsage {
|
|
39
|
+
observedAt?: number;
|
|
40
|
+
stale?: boolean;
|
|
36
41
|
supported: boolean;
|
|
37
42
|
windows?: UsageWindow[];
|
|
38
43
|
plan?: string;
|
|
@@ -73,8 +78,8 @@ export type SubscriptionsSectionProps = Partial<SubscriptionsSectionInjected>;
|
|
|
73
78
|
* fallback, never as a plan limit.
|
|
74
79
|
*/
|
|
75
80
|
export declare function clientVersionText(t: SubscriptionsSectionInjected['t'], clientVersion: ProviderStatus['clientVersion']): string;
|
|
76
|
-
/**
|
|
77
|
-
export
|
|
81
|
+
/** Meter fill color lives with the pace logic: fresh 90%+ is red, pace presets add a yellow lead threshold. */
|
|
82
|
+
export { usageBarColor } from './usage-pace.js';
|
|
78
83
|
/** What one provider's collapsible default-effort section renders. */
|
|
79
84
|
export interface ModelDefaultsView {
|
|
80
85
|
/** Models with reasoning levels, after the name filter — one row each. */
|
|
@@ -18,6 +18,8 @@ import { en } from './locales.js';
|
|
|
18
18
|
import { ProviderAccountManager } from './ProviderAccountManager.js';
|
|
19
19
|
import { ExternalUsageCards } from './ExternalUsageCards.js';
|
|
20
20
|
import { CursorCard } from './CursorCard.js';
|
|
21
|
+
import { UsageMeter } from './UsageMeter.js';
|
|
22
|
+
import { displayUsedPercent } from './usage-pace.js';
|
|
21
23
|
import { UsageBadgeDisplaySetting } from './UsageBadgeDisplaySetting.js';
|
|
22
24
|
import { USAGE_BADGE_REFRESH_EVENT } from './usage-badge-preferences.js';
|
|
23
25
|
import { subscriptionCardStyles as cardStyles } from './subscription-card-styles.js';
|
|
@@ -102,8 +104,6 @@ const styles = {
|
|
|
102
104
|
font: 'inherit', fontSize: 14, lineHeight: '20px', cursor: 'pointer',
|
|
103
105
|
color: 'var(--dsw-alias-state-warn-label)',
|
|
104
106
|
},
|
|
105
|
-
usageTrack: cardStyles.usageTrack,
|
|
106
|
-
usageFill: cardStyles.usageFill,
|
|
107
107
|
deviceCode: {
|
|
108
108
|
marginTop: 4, display: 'flex', flexDirection: 'column', gap: 6,
|
|
109
109
|
border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 8,
|
|
@@ -183,14 +183,8 @@ function usageWindowLabel(t, window) {
|
|
|
183
183
|
: window.kind === 'weekly' ? t('usageWeekly') : t('usageWindow');
|
|
184
184
|
return window.scope !== undefined && window.scope !== '' ? `${base} · ${window.scope}` : base;
|
|
185
185
|
}
|
|
186
|
-
/**
|
|
187
|
-
export
|
|
188
|
-
if (usedPercent >= 95)
|
|
189
|
-
return 'var(--dsw-alias-state-error-primary)';
|
|
190
|
-
if (usedPercent >= 80)
|
|
191
|
-
return 'var(--dsw-alias-state-warn-label)';
|
|
192
|
-
return 'var(--dsw-alias-state-success-primary)';
|
|
193
|
-
}
|
|
186
|
+
/** Meter fill color lives with the pace logic: fresh 90%+ is red, pace presets add a yellow lead threshold. */
|
|
187
|
+
export { usageBarColor } from './usage-pace.js';
|
|
194
188
|
/**
|
|
195
189
|
* Derive one provider's default-effort section from its catalog and filter.
|
|
196
190
|
* Pure so the collapsed-header counts and the filter stay testable without a
|
|
@@ -268,6 +262,7 @@ export function SubscriptionsSection(props) {
|
|
|
268
262
|
const [copiedCode, setCopiedCode] = useState(undefined);
|
|
269
263
|
/** Usage snapshots keyed `${provider}:${accountKey}` — every account tracks its own windows. */
|
|
270
264
|
const [usages, setUsages] = useState({});
|
|
265
|
+
const [observedAt, setObservedAt] = useState({});
|
|
271
266
|
const [usageErrors, setUsageErrors] = useState({});
|
|
272
267
|
const [usageLoading, setUsageLoading] = useState({});
|
|
273
268
|
const mountedRef = useRef(true);
|
|
@@ -377,6 +372,7 @@ export function SubscriptionsSection(props) {
|
|
|
377
372
|
if (!mountedRef.current)
|
|
378
373
|
return;
|
|
379
374
|
setUsages(prev => ({ ...prev, [key]: usage }));
|
|
375
|
+
setObservedAt(prev => ({ ...prev, [key]: usage.observedAt ?? Date.now() }));
|
|
380
376
|
window.dispatchEvent(new Event(USAGE_BADGE_REFRESH_EVENT));
|
|
381
377
|
setUsageErrors((prev) => {
|
|
382
378
|
const next = { ...prev };
|
|
@@ -535,9 +531,9 @@ export function SubscriptionsSection(props) {
|
|
|
535
531
|
if (!account.isDefault)
|
|
536
532
|
void setDefault(id, account.key);
|
|
537
533
|
}, children: account.isDefault ? '★' : '☆' }), _jsx("span", { style: styles.accountName, children: display }), account.plan !== undefined && (_jsx("span", { style: styles.usagePlan, children: account.plan })), account.expiresAt !== undefined && (_jsx("span", { style: styles.statusLine, children: t('accountExpires', { date: new Date(account.expiresAt).toLocaleString() }) })), _jsx("button", { type: "button", style: { ...styles.button, marginLeft: 'auto', flexShrink: 0 }, onClick: () => { void logout(id, account.key, display, name); }, children: t('logout') })] }), showUsage && (_jsxs("div", { style: styles.usage, children: [_jsxs("div", { style: styles.usageHeader, children: [_jsx("span", { style: styles.usageTitle, children: t('usageTitle') }), usage?.plan !== undefined && (_jsx("span", { style: styles.usagePlan, children: t('usagePlan', { plan: usage.plan }) })), _jsx("button", { type: "button", style: { ...styles.usageRefresh, ...usageLoading[usageKey] === true ? { opacity: 0.5, cursor: 'default' } : {} }, disabled: usageLoading[usageKey] === true, onClick: () => { void loadUsage(id, account.key, true); }, children: t('usageRefresh') })] }), usage === undefined && usageError === undefined && (_jsx("p", { style: styles.statusLine, children: t('usageLoading') })), usageError !== undefined && (_jsx("p", { style: styles.errorLine, children: t('usageError', { message: usageError }) })), usage?.windows !== undefined && usage.windows.length === 0 && (_jsx("p", { style: styles.statusLine, children: t('usageEmpty') })), (usage?.windows ?? []).map((window, index) => {
|
|
538
|
-
const percent =
|
|
539
|
-
return (_jsxs("div", { style: styles.usageRow, children: [_jsxs("div", { style: styles.usageMeta, children: [_jsx("span", { children: usageWindowLabel(t, window) }), _jsxs("span", { children: [`${
|
|
540
|
-
&& ` · ${t('usageResets', { date: new Date(window.resetsAt).toLocaleString() })}`] })] }), _jsx(
|
|
534
|
+
const percent = displayUsedPercent(window.usedPercent);
|
|
535
|
+
return (_jsxs("div", { style: styles.usageRow, children: [_jsxs("div", { style: styles.usageMeta, children: [_jsx("span", { children: usageWindowLabel(t, window) }), _jsxs("span", { children: [percent === undefined ? t('usageMeterInvalid') : `${percent}%`, window.resetsAt !== undefined
|
|
536
|
+
&& ` · ${t('usageResets', { date: new Date(window.resetsAt).toLocaleString() })}`] })] }), _jsx(UsageMeter, { window: window, t: t, observedAt: observedAt[usageKey], stale: usageError !== undefined || usage.stale === true })] }, index));
|
|
541
537
|
})] }))] }, account.key));
|
|
542
538
|
}), _jsxs("div", { style: styles.actions, children: [!busy && accounts.length === 0 && (_jsx("button", { type: "button", style: styles.button, onClick: () => { void login(id); }, children: t('login') })), !busy && accounts.length > 0 && id === 'claude' && (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", style: styles.button, onClick: () => { void login(id, 'oauth'); }, children: t('addAccountOAuth') }), _jsx("button", { type: "button", style: styles.button, onClick: () => { void login(id, 'keychain'); }, children: t('addAccountKeychain') })] })), !busy && accounts.length > 0 && id !== 'claude' && (_jsx("button", { type: "button", style: styles.button, onClick: () => { void login(id); }, children: t('addAccount') })), _jsx("button", { type: "button", style: styles.button, "aria-haspopup": "dialog", onClick: () => setManagedProvider({ id, name }), children: t('accountsManage') }), busy && (_jsx("button", { type: "button", style: styles.button, onClick: () => { void cancel(id); }, children: t('cancel') }))] }), !busy && accounts.length > 0 && (_jsx("p", { style: styles.statusLine, children: t('addAccountHint') })), busy && deviceCode !== undefined && (_jsxs("div", { style: styles.deviceCode, children: [_jsx("span", { style: styles.statusLine, children: t('deviceCodePrompt') }), _jsx("span", { style: styles.deviceCodeText, children: deviceCode.userCode }), _jsxs("div", { style: styles.actions, children: [_jsx("button", { type: "button", style: styles.button, onClick: () => { copyDeviceCode(id, deviceCode.userCode); }, children: copiedCode === id ? t('deviceCodeCopied') : t('deviceCodeCopy') }), _jsx("button", { type: "button", style: styles.button, onClick: () => { window.open(deviceCode.verificationUrl, '_blank', 'noopener'); }, children: t('deviceCodeOpenPage') })] })] })), busy && deviceCode === undefined && (_jsxs("details", { style: styles.manual, children: [_jsx("summary", { children: t('manualSummary') }), _jsxs("div", { style: styles.manualRow, children: [_jsx("input", { style: styles.manualInput, value: manualDrafts[id], placeholder: t('manualPlaceholder'), onChange: event => setManualDrafts(prev => ({ ...prev, [id]: event.target.value })) }), _jsx("button", { type: "button", style: styles.button, onClick: () => { void submitManual(id); }, children: t('submit') })] })] }))] }, id));
|
|
543
539
|
}), _jsx(CursorCard, { rpc: rpc, t: t }), _jsx(ExternalUsageCards, { rpc: rpc, t: t }), managedProvider && _jsx(ProviderAccountManager, { provider: managedProvider.id, name: managedProvider.name, rpc: rpc, t: t, onClose: () => setManagedProvider(undefined) })] }));
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useState } from 'react';
|
|
3
3
|
import { setUsageBadgeMode, useUsageBadgeMode } from './usage-badge-preferences.js';
|
|
4
|
+
import { isUsageColorPreset, setUsageColorPreset, useUsageColorPreset } from './usage-color-preferences.js';
|
|
4
5
|
/** Browser-local display preference; never changes provider or account settings. */
|
|
5
6
|
export function UsageBadgeDisplaySetting({ t }) {
|
|
6
7
|
const mode = useUsageBadgeMode();
|
|
8
|
+
const preset = useUsageColorPreset();
|
|
7
9
|
const [failed, setFailed] = useState(false);
|
|
8
10
|
return (_jsxs("div", { style: styles.card, children: [_jsxs("label", { style: styles.field, children: [_jsx("span", { children: t('usageBadgeDisplay') }), _jsxs("select", { style: styles.select, "aria-label": t('usageBadgeDisplay'), value: mode, onChange: event => {
|
|
9
11
|
const value = event.currentTarget.value;
|
|
10
|
-
if (value !== 'recent' && value !== 'hidden')
|
|
12
|
+
if (value !== 'recent' && value !== 'always' && value !== 'hidden')
|
|
11
13
|
return;
|
|
12
14
|
try {
|
|
13
15
|
setUsageBadgeMode(value);
|
|
@@ -18,7 +20,22 @@ export function UsageBadgeDisplaySetting({ t }) {
|
|
|
18
20
|
throw error;
|
|
19
21
|
setFailed(true);
|
|
20
22
|
}
|
|
21
|
-
}, children: [_jsx("option", { value: "recent", children: t('usageBadgeDisplayRecent') }), _jsx("option", { value: "hidden", children: t('usageBadgeDisplayHidden') })] })] }), _jsx("p", { style: styles.hint, children: t('usageBadgeDisplayHint') }),
|
|
23
|
+
}, children: [_jsx("option", { value: "recent", children: t('usageBadgeDisplayRecent') }), _jsx("option", { value: "always", children: t('usageBadgeDisplayAlways') }), _jsx("option", { value: "hidden", children: t('usageBadgeDisplayHidden') })] })] }), _jsx("p", { style: styles.hint, children: t('usageBadgeDisplayHint') }), _jsxs("label", { style: styles.field, children: [_jsx("span", { children: t('usageColorLabel') }), _jsxs("select", { style: styles.select, "aria-label": t('usageColorLabel'), value: preset, onChange: event => {
|
|
24
|
+
const value = event.currentTarget.value;
|
|
25
|
+
if (!isUsageColorPreset(value))
|
|
26
|
+
return;
|
|
27
|
+
try {
|
|
28
|
+
setUsageColorPreset(value);
|
|
29
|
+
setFailed(false);
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
if (!(error instanceof DOMException) || !['SecurityError', 'QuotaExceededError'].includes(error.name))
|
|
33
|
+
throw error;
|
|
34
|
+
setFailed(true);
|
|
35
|
+
}
|
|
36
|
+
}, children: [_jsx("option", { value: "standard", children: t('usageColorStandard') }), _jsx("option", { value: "relaxed", children: t('usageColorRelaxed') }), _jsx("option", { value: "remaining", children: t('usageColorRemaining') })] })] }), _jsx("p", { style: styles.hint, children: t(preset === 'remaining' ? 'usageColorRemainingHint' : 'usageColorPaceHint', {
|
|
37
|
+
allowance: preset === 'relaxed' ? 15 : 10,
|
|
38
|
+
}) }), failed && _jsx("p", { role: "alert", style: { ...styles.hint, color: 'var(--dsw-alias-state-error-primary)' }, children: t('usageBadgeDisplaySaveFailed') })] }));
|
|
22
39
|
}
|
|
23
40
|
const styles = {
|
|
24
41
|
card: {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { CSSProperties } from 'react';
|
|
2
|
+
import type { UsageWindow, SubscriptionsSectionInjected } from './SubscriptionsSection.js';
|
|
3
|
+
import type { UsageObservation } from './usage-pace.js';
|
|
4
|
+
export declare function UsageMeter({ window: quota, t, style, observedAt, stale }: {
|
|
5
|
+
window: UsageWindow;
|
|
6
|
+
t: SubscriptionsSectionInjected['t'];
|
|
7
|
+
style?: CSSProperties;
|
|
8
|
+
} & UsageObservation): import("react").JSX.Element;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import { elapsedPercent, isUsageFresh, resetCountdownParts, usageBarColor, usageColorState, validUsage } from './usage-pace.js';
|
|
4
|
+
import { useUsageColorPreset } from './usage-color-preferences.js';
|
|
5
|
+
/** Singular/plural locale keys per countdown unit (zh maps both to one string). */
|
|
6
|
+
const UNIT_KEYS = {
|
|
7
|
+
day: ['usageUnitDay', 'usageUnitDays'],
|
|
8
|
+
hour: ['usageUnitHour', 'usageUnitHours'],
|
|
9
|
+
minute: ['usageUnitMinute', 'usageUnitMinutes'],
|
|
10
|
+
};
|
|
11
|
+
export function UsageMeter({ window: quota, t, style, observedAt, stale }) {
|
|
12
|
+
const preset = useUsageColorPreset();
|
|
13
|
+
const [now, setNow] = useState(Date.now);
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
const timer = setInterval(() => setNow(Date.now()), 60_000);
|
|
16
|
+
return () => clearInterval(timer);
|
|
17
|
+
}, []);
|
|
18
|
+
// Use render time for new readings, without trusting a future observation timestamp.
|
|
19
|
+
const current = Math.max(now, Date.now());
|
|
20
|
+
const valid = validUsage(quota.usedPercent);
|
|
21
|
+
const used = valid ? quota.usedPercent : 0;
|
|
22
|
+
const fresh = isUsageFresh(quota, { observedAt, stale }, current);
|
|
23
|
+
// The cursor is where the window stands in time. It needs only the window's
|
|
24
|
+
// own timing, so it stays for an unused, stale, aged or unobserved reading.
|
|
25
|
+
const elapsed = elapsedPercent(quota, current);
|
|
26
|
+
// Comparing usage with elapsed time is meaningful only while the percentage
|
|
27
|
+
// is current; an old one is not set against today's clock.
|
|
28
|
+
const pace = fresh ? elapsed : undefined;
|
|
29
|
+
const state = usageColorState(quota.usedPercent, pace, preset, fresh);
|
|
30
|
+
const lead = pace === undefined ? 0 : Math.round(used - pace);
|
|
31
|
+
const label = [
|
|
32
|
+
valid ? t('usageMeterUsed', { used: Math.round(used), remaining: Math.round(100 - used) }) : t('usageMeterInvalid'),
|
|
33
|
+
// An invalid reading already states unavailability; the stale line would repeat it.
|
|
34
|
+
!valid ? '' : !fresh ? t('usageMeterStale') : state === 'red' ? t('usageMeterNearLimit')
|
|
35
|
+
: pace === undefined && preset !== 'remaining' ? t('usageMeterUnknownTime')
|
|
36
|
+
: state === 'yellow' ? t('usageMeterAhead') : t('usageMeterNoWarning'),
|
|
37
|
+
elapsed === undefined ? '' : t('usageMeterPace', { elapsed: Math.round(elapsed) }),
|
|
38
|
+
pace === undefined ? '' : lead > 0 ? t('usageMeterPaceAhead', { points: lead })
|
|
39
|
+
: lead < 0 ? t('usageMeterPaceBehind', { points: -lead }) : t('usageMeterPaceEven'),
|
|
40
|
+
quota.resetsAt !== undefined && quota.resetsAt > current ? t('usageMeterReset', {
|
|
41
|
+
duration: resetCountdownParts(quota.resetsAt - current)
|
|
42
|
+
.map(part => t(UNIT_KEYS[part.unit][part.count === 1 ? 0 : 1], { count: part.count }))
|
|
43
|
+
.join(' '),
|
|
44
|
+
}) : '',
|
|
45
|
+
].filter(Boolean).join(' · ');
|
|
46
|
+
return _jsxs("div", { role: "img", "aria-label": label, title: label, "data-usage-color": state, style: {
|
|
47
|
+
height: 6, borderRadius: 3, overflow: 'hidden',
|
|
48
|
+
background: 'var(--dsw-alias-bg-layer-1)', border: '1px solid var(--dsw-alias-border-l2)',
|
|
49
|
+
...style, position: 'relative',
|
|
50
|
+
}, children: [_jsx("div", { style: { height: '100%', borderRadius: 'inherit', width: `${used}%`,
|
|
51
|
+
background: usageBarColor(quota.usedPercent, pace, preset, fresh) } }), elapsed !== undefined && _jsx("span", { "aria-hidden": "true", "data-usage-time-marker": true, style: {
|
|
52
|
+
position: 'absolute', left: `clamp(0px, calc(${elapsed}% - 1px), calc(100% - 2px))`,
|
|
53
|
+
top: 0, bottom: 0, width: 2, background: 'var(--dsw-alias-label-primary)',
|
|
54
|
+
boxShadow: '0 0 0 1px var(--dsw-alias-bg-layer-1)',
|
|
55
|
+
} })] });
|
|
56
|
+
}
|
|
@@ -83,7 +83,7 @@ export declare const HOST_CONTRACT: {
|
|
|
83
83
|
/** Design tokens the client styles with; the host theme must define each one. */
|
|
84
84
|
readonly tokens: {
|
|
85
85
|
readonly package: "@deepseek-ai/dsh-client-ui-theme";
|
|
86
|
-
readonly names: readonly ["--dsw-alias-bg-layer-1", "--dsw-alias-bg-skeleton", "--dsw-alias-border-l2", "--dsw-alias-border-l2-darkmode-thin", "--dsw-alias-interactive-bg-hover", "--dsw-alias-interactive-bg-hover-solid", "--dsw-alias-label-dimmed", "--dsw-alias-label-primary", "--dsw-alias-label-secondary", "--dsw-alias-label-tertiary", "--dsw-alias-state-error-primary", "--dsw-alias-state-success-primary", "--dsw-alias-state-warn-label", "--dsw-elevation-prominent", "--dsw-specific-menu"];
|
|
86
|
+
readonly names: readonly ["--dsw-alias-bg-layer-1", "--dsw-alias-bg-skeleton", "--dsw-alias-border-l2", "--dsw-alias-border-l2-darkmode-thin", "--dsw-alias-interactive-bg-hover", "--dsw-alias-interactive-bg-hover-solid", "--dsw-alias-label-dimmed", "--dsw-alias-label-primary", "--dsw-alias-label-secondary", "--dsw-alias-label-tertiary", "--dsw-alias-state-error-primary", "--dsw-alias-state-success-primary", "--dsw-alias-state-warn-label", "--dsw-elevation-prominent", "--dsw-menu-backdrop-filter", "--dsw-specific-menu"];
|
|
87
87
|
};
|
|
88
88
|
};
|
|
89
89
|
/** A glyph name listed in {@link HOST_CONTRACT}; hostIcon() accepts no other. */
|
package/lib/client/locales.d.ts
CHANGED
|
@@ -79,6 +79,30 @@ export declare const en: {
|
|
|
79
79
|
usageMonthly: string;
|
|
80
80
|
usageWindow: string;
|
|
81
81
|
usageResets: string;
|
|
82
|
+
usageMeterUsed: string;
|
|
83
|
+
usageMeterPace: string;
|
|
84
|
+
usageMeterPaceAhead: string;
|
|
85
|
+
usageMeterPaceBehind: string;
|
|
86
|
+
usageMeterPaceEven: string;
|
|
87
|
+
usageMeterInvalid: string;
|
|
88
|
+
usageMeterStale: string;
|
|
89
|
+
usageMeterNearLimit: string;
|
|
90
|
+
usageMeterAhead: string;
|
|
91
|
+
usageMeterNoWarning: string;
|
|
92
|
+
usageMeterUnknownTime: string;
|
|
93
|
+
usageMeterReset: string;
|
|
94
|
+
usageUnitDay: string;
|
|
95
|
+
usageUnitDays: string;
|
|
96
|
+
usageUnitHour: string;
|
|
97
|
+
usageUnitHours: string;
|
|
98
|
+
usageUnitMinute: string;
|
|
99
|
+
usageUnitMinutes: string;
|
|
100
|
+
usageColorLabel: string;
|
|
101
|
+
usageColorStandard: string;
|
|
102
|
+
usageColorRelaxed: string;
|
|
103
|
+
usageColorRemaining: string;
|
|
104
|
+
usageColorPaceHint: string;
|
|
105
|
+
usageColorRemainingHint: string;
|
|
82
106
|
usagePlan: string;
|
|
83
107
|
externalUsageTitle: string;
|
|
84
108
|
externalUsageConnected: string;
|
|
@@ -131,6 +155,7 @@ export declare const en: {
|
|
|
131
155
|
usageBadgeTitle: string;
|
|
132
156
|
usageBadgeDisplay: string;
|
|
133
157
|
usageBadgeDisplayRecent: string;
|
|
158
|
+
usageBadgeDisplayAlways: string;
|
|
134
159
|
usageBadgeDisplayHidden: string;
|
|
135
160
|
usageBadgeDisplayHint: string;
|
|
136
161
|
usageBadgeDisplaySaveFailed: string;
|
|
@@ -226,6 +251,30 @@ export declare const zh: {
|
|
|
226
251
|
usageMonthly: string;
|
|
227
252
|
usageWindow: string;
|
|
228
253
|
usageResets: string;
|
|
254
|
+
usageMeterUsed: string;
|
|
255
|
+
usageMeterPace: string;
|
|
256
|
+
usageMeterPaceAhead: string;
|
|
257
|
+
usageMeterPaceBehind: string;
|
|
258
|
+
usageMeterPaceEven: string;
|
|
259
|
+
usageMeterInvalid: string;
|
|
260
|
+
usageMeterStale: string;
|
|
261
|
+
usageMeterNearLimit: string;
|
|
262
|
+
usageMeterAhead: string;
|
|
263
|
+
usageMeterNoWarning: string;
|
|
264
|
+
usageMeterUnknownTime: string;
|
|
265
|
+
usageMeterReset: string;
|
|
266
|
+
usageUnitDay: string;
|
|
267
|
+
usageUnitDays: string;
|
|
268
|
+
usageUnitHour: string;
|
|
269
|
+
usageUnitHours: string;
|
|
270
|
+
usageUnitMinute: string;
|
|
271
|
+
usageUnitMinutes: string;
|
|
272
|
+
usageColorLabel: string;
|
|
273
|
+
usageColorStandard: string;
|
|
274
|
+
usageColorRelaxed: string;
|
|
275
|
+
usageColorRemaining: string;
|
|
276
|
+
usageColorPaceHint: string;
|
|
277
|
+
usageColorRemainingHint: string;
|
|
229
278
|
usagePlan: string;
|
|
230
279
|
externalUsageTitle: string;
|
|
231
280
|
externalUsageConnected: string;
|
|
@@ -278,6 +327,7 @@ export declare const zh: {
|
|
|
278
327
|
usageBadgeTitle: string;
|
|
279
328
|
usageBadgeDisplay: string;
|
|
280
329
|
usageBadgeDisplayRecent: string;
|
|
330
|
+
usageBadgeDisplayAlways: string;
|
|
281
331
|
usageBadgeDisplayHidden: string;
|
|
282
332
|
usageBadgeDisplayHint: string;
|
|
283
333
|
usageBadgeDisplaySaveFailed: string;
|