dsh-subscriptions 0.1.2 → 0.1.4
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 +92 -10
- package/lib/auth/rpc.d.ts +3 -1
- package/lib/auth/rpc.js +10 -1
- package/lib/client/ExternalUsageCards.d.ts +11 -1
- package/lib/client/ExternalUsageCards.js +18 -3
- package/lib/client/ProviderAccountManager.js +34 -41
- package/lib/client/ProviderModelEditor.js +5 -9
- package/lib/client/SpeedSelect.js +8 -7
- package/lib/client/SubscriptionUsageBadge.d.ts +19 -2
- package/lib/client/SubscriptionUsageBadge.js +65 -9
- package/lib/client/SubscriptionsSection.d.ts +22 -0
- package/lib/client/SubscriptionsSection.js +128 -38
- package/lib/client/UsageBadgeDisplaySetting.js +6 -10
- package/lib/client/UsageMeter.js +11 -6
- package/lib/client/host-contract.d.ts +1 -1
- package/lib/client/host-contract.js +22 -0
- package/lib/client/locales.d.ts +60 -0
- package/lib/client/locales.js +62 -2
- package/lib/client/provider-settings-styles.d.ts +3 -0
- package/lib/client/provider-settings-styles.js +206 -0
- package/lib/client/reset-action.d.ts +14 -0
- package/lib/client/reset-action.js +15 -0
- package/lib/client/reset-credits-view.d.ts +53 -0
- package/lib/client/reset-credits-view.js +103 -0
- package/lib/client/usage-badge-preferences.d.ts +7 -2
- package/lib/client/usage-badge-preferences.js +3 -2
- package/lib/client/usage-pace.d.ts +14 -1
- package/lib/client/usage-pace.js +14 -1
- package/lib/client.js +1036 -334
- package/lib/client.js.map +1 -1
- package/lib/index.d.ts +9 -0
- package/lib/index.js +1120 -602
- package/lib/providers/account-preferences.js +3 -2
- package/lib/providers/catalog-store.d.ts +3 -2
- package/lib/providers/catalog-store.js +57 -26
- package/lib/providers/claude.d.ts +22 -3
- package/lib/providers/claude.js +113 -15
- package/lib/providers/codex.d.ts +15 -2
- package/lib/providers/codex.js +72 -4
- package/lib/providers/common.d.ts +25 -0
- package/lib/providers/common.js +22 -18
- package/lib/providers/copilot.d.ts +2 -1
- package/lib/providers/copilot.js +10 -3
- package/lib/providers/grok.d.ts +2 -2
- package/lib/providers/grok.js +2 -2
- package/lib/providers/minimax-usage.d.ts +22 -1
- package/lib/providers/minimax-usage.js +37 -7
- package/lib/providers/pool-health.d.ts +2 -0
- package/lib/providers/pool-health.js +9 -0
- package/lib/providers/pool-usage.d.ts +13 -1
- package/lib/providers/pool-usage.js +39 -7
- package/lib/providers/pool.d.ts +7 -5
- package/lib/providers/pool.js +89 -12
- package/lib/providers/rate-limit.d.ts +0 -14
- package/lib/providers/rate-limit.js +0 -26
- package/lib/providers/replay.d.ts +4 -0
- package/lib/providers/replay.js +50 -0
- package/lib/providers/reset-redemption.d.ts +51 -0
- package/lib/providers/reset-redemption.js +102 -0
- package/lib/translate/anthropic.d.ts +21 -2
- package/lib/translate/anthropic.js +35 -11
- package/lib/translate/antigravity.js +13 -6
- package/lib/translate/chat-completions.js +1 -1
- package/lib/translate/resolved.d.ts +21 -0
- package/lib/translate/resolved.js +63 -3
- package/lib/translate/responses.js +8 -6
- package/lib/translate/sse.js +8 -0
- package/package.json +1 -1
|
@@ -24,6 +24,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
24
24
|
* 'settings.subscriptions' namespace.
|
|
25
25
|
*/
|
|
26
26
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
27
|
+
import { subscriptionChromeCss } from './provider-settings-styles.js';
|
|
27
28
|
import { createPortal } from 'react-dom';
|
|
28
29
|
import * as primitives from '@deepseek-ai/dsh-client-ui-primitives';
|
|
29
30
|
import { useAnchoredPosition, useDismissOnOutsidePointer } from '@deepseek-ai/dsh-client-ui-primitives';
|
|
@@ -42,6 +43,8 @@ const UsageBadgeIcon = usageBadgeIcon(primitives);
|
|
|
42
43
|
const USAGE_POLL_INTERVAL_MS = 15 * 60_000;
|
|
43
44
|
/** How often the badge re-reads the session's current model (model switches arrive only by asking). */
|
|
44
45
|
const MODEL_POLL_INTERVAL_MS = 3000;
|
|
46
|
+
/** How long the rotation holds one provider in `always` mode (a display cadence, not a data poll). */
|
|
47
|
+
const BADGE_ROTATION_INTERVAL_MS = 10_000;
|
|
45
48
|
/** Distance between the trigger's top edge and the dialog's bottom (host stat dialogs use the same). */
|
|
46
49
|
const PANEL_GAP = 8;
|
|
47
50
|
/** Distance kept between the dialog and each viewport edge. */
|
|
@@ -236,6 +239,27 @@ export function collapsedDisplays(displays, current) {
|
|
|
236
239
|
const match = displays.find(d => d.provider === current);
|
|
237
240
|
return match === undefined ? [] : [match];
|
|
238
241
|
}
|
|
242
|
+
/**
|
|
243
|
+
* `always` mode: the current model's provider while it reports usage,
|
|
244
|
+
* otherwise the rotation steps through the providers that do, in display
|
|
245
|
+
* order, wrapping around. The index is taken modulo the current provider
|
|
246
|
+
* count, so providers appearing or dropping out stay inside range; the
|
|
247
|
+
* sequence itself may shift when the roster changes. Unlike `recent`, the
|
|
248
|
+
* retained subscription does not pin the pill — that retention is exactly
|
|
249
|
+
* what this mode exists to escape.
|
|
250
|
+
* @param displays - providers with usage, in display order.
|
|
251
|
+
* @param current - the provider of the session's current model.
|
|
252
|
+
* @param step - rotation tick; consecutive ticks advance one provider.
|
|
253
|
+
*/
|
|
254
|
+
export function rotatingDisplay(displays, current, step) {
|
|
255
|
+
if (displays.length === 0)
|
|
256
|
+
return [];
|
|
257
|
+
const match = displays.find(d => d.provider === current);
|
|
258
|
+
if (match !== undefined)
|
|
259
|
+
return [match];
|
|
260
|
+
const index = Number.isFinite(step) ? Math.floor(step) : 0;
|
|
261
|
+
return [displays[((index % displays.length) + displays.length) % displays.length]];
|
|
262
|
+
}
|
|
239
263
|
/** Order for the expanded dialog: the current provider first, the rest in poll order. */
|
|
240
264
|
export function expandedDisplays(displays, current) {
|
|
241
265
|
const match = displays.find(d => d.provider === current);
|
|
@@ -271,7 +295,7 @@ function usageWindowLabel(t, window) {
|
|
|
271
295
|
* every provider's accounts and their windows. Returns null when no data is
|
|
272
296
|
* available.
|
|
273
297
|
*/
|
|
274
|
-
export function SubscriptionUsageBadge({ rpc, currentModel, t }) {
|
|
298
|
+
export function SubscriptionUsageBadge({ rpc, currentModel, t, rotationMs }) {
|
|
275
299
|
const translate = t ?? fallbackTranslate;
|
|
276
300
|
const [displays, setDisplays] = useState([]);
|
|
277
301
|
const [selection, setSelection] = useState(undefined);
|
|
@@ -281,6 +305,11 @@ export function SubscriptionUsageBadge({ rpc, currentModel, t }) {
|
|
|
281
305
|
const badgeSelection = retainSubscriptionSelection(lastSubscription, selection);
|
|
282
306
|
const [open, setOpen] = useState(false);
|
|
283
307
|
const [hover, setHover] = useState(false);
|
|
308
|
+
// Rotation tick for `always` mode; the interval runs only in that mode.
|
|
309
|
+
const [rotation, setRotation] = useState(0);
|
|
310
|
+
// The provider the pill showed when the dialog opened; an open dialog
|
|
311
|
+
// keeps leading with it while the rotation keeps ticking underneath.
|
|
312
|
+
const [dialogLead, setDialogLead] = useState(undefined);
|
|
284
313
|
const inflightRef = useRef(false);
|
|
285
314
|
const mountedRef = useRef(true);
|
|
286
315
|
const rootRef = useRef(null);
|
|
@@ -443,22 +472,49 @@ export function SubscriptionUsageBadge({ rpc, currentModel, t }) {
|
|
|
443
472
|
if (displayMode === 'hidden')
|
|
444
473
|
setOpen(false);
|
|
445
474
|
}, [displayMode]);
|
|
475
|
+
// `always` mode advances the rotation on its own cadence; other modes
|
|
476
|
+
// never advance, so the pill never changes on its own.
|
|
477
|
+
useEffect(() => {
|
|
478
|
+
if (displayMode !== 'always')
|
|
479
|
+
return;
|
|
480
|
+
const interval = rotationMs !== undefined && Number.isFinite(rotationMs) && rotationMs > 0
|
|
481
|
+
? rotationMs
|
|
482
|
+
: BADGE_ROTATION_INTERVAL_MS;
|
|
483
|
+
const timer = setInterval(() => { setRotation(step => step + 1); }, interval);
|
|
484
|
+
return () => { clearInterval(timer); };
|
|
485
|
+
}, [displayMode, rotationMs]);
|
|
446
486
|
const seat = _jsx("span", { ref: seatRef, style: styles.seat, "aria-hidden": true });
|
|
447
|
-
|
|
487
|
+
// `always` follows the live model, not the retained subscription: the
|
|
488
|
+
// retention pins the pill in `recent`, which is what this mode escapes.
|
|
489
|
+
const collapsed = displayMode === 'always'
|
|
490
|
+
? rotatingDisplay(displays, current, rotation)
|
|
491
|
+
: collapsedDisplays(displays, badgeSelection?.provider);
|
|
448
492
|
if (displayMode === 'hidden' || collapsed.length === 0)
|
|
449
493
|
return seat;
|
|
450
|
-
|
|
494
|
+
// Model scope only while the pill shows the current model's provider;
|
|
495
|
+
// a rotated provider has no live model, so it reads at account level.
|
|
496
|
+
const pillModel = displayMode === 'always'
|
|
497
|
+
? (current === collapsed[0]?.provider ? selection?.model : undefined)
|
|
498
|
+
: badgeSelection?.model;
|
|
499
|
+
const label = compactSegment(collapsed[0], pillModel, translate);
|
|
451
500
|
// The dialog opens from the badge, so it follows the provider and model the
|
|
452
501
|
// badge shows (a retained subscription when the current model is not one).
|
|
453
|
-
|
|
502
|
+
// In `always` mode the pill moves under the cursor, so an open dialog keeps
|
|
503
|
+
// the provider that was showing when it opened instead of reordering live.
|
|
504
|
+
const dialogProvider = open && displayMode === 'always' ? dialogLead : badgeSelection?.provider;
|
|
505
|
+
const dialogModel = open && displayMode === 'always'
|
|
506
|
+
? (dialogLead === current ? selection?.model : undefined)
|
|
507
|
+
: badgeSelection?.model;
|
|
508
|
+
const expanded = expandedDisplays(displays, dialogProvider);
|
|
454
509
|
const title = translate('usageBadgeTitle');
|
|
455
510
|
const toggle = () => {
|
|
456
511
|
const next = !open;
|
|
512
|
+
setDialogLead(collapsed[0]?.provider);
|
|
457
513
|
setOpen(next);
|
|
458
514
|
if (next)
|
|
459
515
|
void refresh();
|
|
460
516
|
};
|
|
461
|
-
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 ===
|
|
517
|
+
const pill = (_jsxs("span", { ref: rootRef, style: styles.anchor, children: [_jsx("style", { children: subscriptionChromeCss }), _jsxs("button", { type: "button", className: "dsh-subscription-usage-pill", 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, className: "dsh-subscription-usage-panel", 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)] }));
|
|
462
518
|
return (_jsxs(_Fragment, { children: [seat, statsRow !== null && statsRow.isConnected ? createPortal(pill, statsRow) : pill] }));
|
|
463
519
|
}
|
|
464
520
|
/**
|
|
@@ -509,9 +565,9 @@ const styles = {
|
|
|
509
565
|
pill: {
|
|
510
566
|
boxSizing: 'border-box', maxWidth: '100%',
|
|
511
567
|
color: 'var(--dsw-alias-label-tertiary)',
|
|
512
|
-
font: 'inherit', fontSize: 'var(--dsh-content-font-size-secondary, 13px)',
|
|
513
|
-
fontVariantNumeric: 'tabular-nums', lineHeight: '20px', whiteSpace: 'nowrap',
|
|
514
|
-
background: 'transparent', border: 'none', borderRadius:
|
|
568
|
+
font: 'inherit', fontSize: 'calc(var(--dsh-content-font-size-secondary, 13px) - 1px)',
|
|
569
|
+
fontVariantNumeric: 'tabular-nums', lineHeight: 'calc(20px + var(--dsh-content-font-delta-secondary, 0px))', whiteSpace: 'nowrap',
|
|
570
|
+
background: 'transparent', border: 'none', borderRadius: 999,
|
|
515
571
|
alignItems: 'center', gap: 6, padding: '1px 8px', display: 'inline-flex', cursor: 'pointer',
|
|
516
572
|
},
|
|
517
573
|
pillActive: {
|
|
@@ -531,7 +587,7 @@ const styles = {
|
|
|
531
587
|
maxHeight: 'min(560px, 100dvh - 24px)', overflowY: 'auto', overscrollBehavior: 'contain',
|
|
532
588
|
boxShadow: 'var(--dsw-elevation-prominent)',
|
|
533
589
|
color: 'var(--dsw-alias-label-secondary)', cursor: 'default',
|
|
534
|
-
border: 0, borderRadius:
|
|
590
|
+
border: 0, borderRadius: 'var(--dsw-radius-lg, 16px)', padding: 16, fontSize: 12, lineHeight: '18px',
|
|
535
591
|
},
|
|
536
592
|
title: {
|
|
537
593
|
color: 'var(--dsw-alias-label-primary)', display: 'flex',
|
|
@@ -41,6 +41,28 @@ export interface ProviderUsage {
|
|
|
41
41
|
supported: boolean;
|
|
42
42
|
windows?: UsageWindow[];
|
|
43
43
|
plan?: string;
|
|
44
|
+
/**
|
|
45
|
+
* Banked limit-reset grants. Codex entries carry id/expiry only. Claude
|
|
46
|
+
* entries are one row per grant: `expiresAt` is when the grant expires, not
|
|
47
|
+
* a window reset, and `resetsLeft` is the remaining credit count.
|
|
48
|
+
*/
|
|
49
|
+
resetCredits?: ResetCreditView[];
|
|
50
|
+
resetCreditsError?: string;
|
|
51
|
+
}
|
|
52
|
+
/** One banked reset grant as the usage RPC returns it. */
|
|
53
|
+
export interface ResetCreditView {
|
|
54
|
+
id?: string;
|
|
55
|
+
grantedAt?: number;
|
|
56
|
+
/** Epoch milliseconds when the grant expires. Never a periodic window reset. */
|
|
57
|
+
expiresAt?: number;
|
|
58
|
+
resetsTotal?: number;
|
|
59
|
+
resetsLeft?: number;
|
|
60
|
+
usableNow?: boolean;
|
|
61
|
+
paused?: boolean;
|
|
62
|
+
/** Server metadata only. Claude has no redemption control. */
|
|
63
|
+
claimable?: boolean;
|
|
64
|
+
/** Epoch milliseconds until which the grant is visible but not usable. */
|
|
65
|
+
cooldownUntil?: number;
|
|
44
66
|
}
|
|
45
67
|
/** One model's default-effort picker state as answered by `modelDefaults`. */
|
|
46
68
|
export interface ModelDefaultView {
|
|
@@ -10,15 +10,17 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
10
10
|
* design-platform.css values flip under `body[data-ds-dark-theme]`), and
|
|
11
11
|
* every user-visible string goes through the locale-bound `t` of the
|
|
12
12
|
* 'settings.subscriptions' namespace. Buttons and inputs take the
|
|
13
|
-
*
|
|
14
|
-
* express.
|
|
13
|
+
* native settings vocabulary with shared, scoped control states.
|
|
15
14
|
*/
|
|
16
15
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
17
16
|
import { en } from './locales.js';
|
|
17
|
+
import { resetActionBlock } from './reset-action.js';
|
|
18
18
|
import { ProviderAccountManager } from './ProviderAccountManager.js';
|
|
19
|
+
import { providerSettingsCss } from './provider-settings-styles.js';
|
|
19
20
|
import { ExternalUsageCards } from './ExternalUsageCards.js';
|
|
20
21
|
import { CursorCard } from './CursorCard.js';
|
|
21
22
|
import { UsageMeter } from './UsageMeter.js';
|
|
23
|
+
import { ResetCreditsDisclosure, ResetCreditsErrorLine, showsResetCredits } from './reset-credits-view.js';
|
|
22
24
|
import { displayUsedPercent } from './usage-pace.js';
|
|
23
25
|
import { UsageBadgeDisplaySetting } from './UsageBadgeDisplaySetting.js';
|
|
24
26
|
import { USAGE_BADGE_REFRESH_EVENT } from './usage-badge-preferences.js';
|
|
@@ -80,7 +82,10 @@ const styles = {
|
|
|
80
82
|
color: 'var(--dsw-alias-label-primary)',
|
|
81
83
|
},
|
|
82
84
|
intro: { margin: 0, color: 'var(--dsw-alias-label-tertiary)', fontSize: 14, lineHeight: '22px' },
|
|
83
|
-
card: cardStyles.card,
|
|
85
|
+
card: { ...cardStyles.card,
|
|
86
|
+
border: '0.5px solid var(--dsw-alias-settings-card-stroke, var(--dsw-alias-border-l4))',
|
|
87
|
+
borderRadius: 'var(--dsw-radius-xl, 20px)', background: 'var(--dsw-alias-settings-card-fill, var(--dsw-alias-bg-layer-2))',
|
|
88
|
+
},
|
|
84
89
|
cardHeader: cardStyles.header,
|
|
85
90
|
dot: cardStyles.dot,
|
|
86
91
|
name: cardStyles.name,
|
|
@@ -88,44 +93,46 @@ const styles = {
|
|
|
88
93
|
clientVersion: { fontSize: 12, lineHeight: '18px', color: 'var(--dsw-alias-label-tertiary)', fontVariantNumeric: 'tabular-nums' },
|
|
89
94
|
errorLine: cardStyles.error,
|
|
90
95
|
actions: cardStyles.actions,
|
|
91
|
-
|
|
92
|
-
usage: cardStyles.usage,
|
|
96
|
+
usage: { ...cardStyles.usage, borderTop: '0.5px solid var(--dsw-alias-border-l2)' },
|
|
93
97
|
usageHeader: cardStyles.usageHeader,
|
|
94
98
|
usageTitle: cardStyles.usageTitle,
|
|
95
99
|
usagePlan: cardStyles.usagePlan,
|
|
96
|
-
usageRefresh:
|
|
100
|
+
usageRefresh: { minHeight: 22, height: 22, padding: '0 8px', marginLeft: 'auto' },
|
|
97
101
|
usageRow: cardStyles.usageRow,
|
|
98
102
|
usageMeta: cardStyles.usageMeta,
|
|
99
|
-
accountRow: cardStyles.account,
|
|
103
|
+
accountRow: { ...cardStyles.account, border: '0.5px solid var(--dsw-alias-border-l2)', borderRadius: 'var(--dsw-radius-lg, 16px)' },
|
|
100
104
|
accountHeader: cardStyles.accountHeader,
|
|
101
105
|
accountName: cardStyles.accountName,
|
|
102
106
|
starButton: {
|
|
103
|
-
border: 'none',
|
|
107
|
+
border: 'none', padding: 0, width: 28,
|
|
104
108
|
font: 'inherit', fontSize: 14, lineHeight: '20px', cursor: 'pointer',
|
|
105
109
|
color: 'var(--dsw-alias-state-warn-label)',
|
|
106
110
|
},
|
|
107
111
|
deviceCode: {
|
|
108
112
|
marginTop: 4, display: 'flex', flexDirection: 'column', gap: 6,
|
|
109
|
-
border: '
|
|
113
|
+
border: '0.5px solid var(--dsw-alias-border-l2)', borderRadius: 'var(--dsw-radius-lg, 16px)',
|
|
110
114
|
padding: '10px 12px', background: 'var(--dsw-alias-bg-layer-1)',
|
|
111
115
|
},
|
|
112
116
|
deviceCodeText: {
|
|
113
117
|
fontFamily: 'monospace', fontSize: 18, lineHeight: '24px', letterSpacing: 2,
|
|
114
118
|
color: 'var(--dsw-alias-label-primary)', userSelect: 'all',
|
|
115
119
|
},
|
|
120
|
+
manual: { fontSize: 13, lineHeight: '20px' },
|
|
121
|
+
manualRow: { display: 'flex', flexWrap: 'wrap', gap: 8, alignItems: 'center', marginTop: 8 },
|
|
122
|
+
manualInput: { flex: '1 1 200px', minWidth: 0 },
|
|
116
123
|
modalOverlay: {
|
|
117
124
|
position: 'fixed', inset: 0, zIndex: 1000,
|
|
118
125
|
display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 16,
|
|
119
|
-
background: '
|
|
126
|
+
background: 'var(--dsw-alias-bg-mask-1, #0006)', backdropFilter: 'var(--dsw-mask-blur, none)',
|
|
120
127
|
},
|
|
121
128
|
modal: {
|
|
122
129
|
width: 460, maxWidth: '100%', maxHeight: '90vh', overflowY: 'auto',
|
|
123
130
|
boxSizing: 'border-box', display: 'flex', flexDirection: 'column', gap: 12,
|
|
124
|
-
padding: '16px 18px', borderRadius:
|
|
125
|
-
background: 'var(--dsw-alias-bg-layer-
|
|
131
|
+
padding: '16px 18px', borderRadius: 'var(--dsw-radius-panel, 28px)',
|
|
132
|
+
background: 'var(--dsw-alias-bg-layer-2)', border: 0, boxShadow: 'var(--dsw-elevation-prominent)',
|
|
126
133
|
},
|
|
127
134
|
modalHeader: { display: 'flex', alignItems: 'center', gap: 8 },
|
|
128
|
-
modalTitle: { fontWeight:
|
|
135
|
+
modalTitle: { fontWeight: 500, fontSize: 16, lineHeight: '24px', color: 'var(--dsw-alias-label-primary)' },
|
|
129
136
|
};
|
|
130
137
|
/** Status dot color for one provider state. */
|
|
131
138
|
function dotColor(status) {
|
|
@@ -265,10 +272,24 @@ export function SubscriptionsSection(props) {
|
|
|
265
272
|
const [observedAt, setObservedAt] = useState({});
|
|
266
273
|
const [usageErrors, setUsageErrors] = useState({});
|
|
267
274
|
const [usageLoading, setUsageLoading] = useState({});
|
|
275
|
+
const [resetConfirmation, setResetConfirmation] = useState();
|
|
276
|
+
const [resetBusy, setResetBusy] = useState(false);
|
|
277
|
+
const [resetAcknowledged, setResetAcknowledged] = useState(false);
|
|
278
|
+
const [resetMessage, setResetMessage] = useState('');
|
|
279
|
+
const [resetAccount, setResetAccount] = useState();
|
|
280
|
+
const resetDialogRef = useRef(null);
|
|
281
|
+
useEffect(() => {
|
|
282
|
+
if (resetConfirmation)
|
|
283
|
+
resetDialogRef.current?.showModal();
|
|
284
|
+
else
|
|
285
|
+
resetDialogRef.current?.close();
|
|
286
|
+
}, [resetConfirmation]);
|
|
287
|
+
const resetInflight = useRef(false);
|
|
268
288
|
const mountedRef = useRef(true);
|
|
269
289
|
const pollersRef = useRef(new Map());
|
|
270
290
|
/** Accounts with a `usage` call in flight; guards the auto-fetch effect against re-entry. */
|
|
271
|
-
|
|
291
|
+
/** In-flight usage reads per key, holding the promise so a forced read can wait one out. */
|
|
292
|
+
const usageInflightRef = useRef(new Map());
|
|
272
293
|
const usageRosterSignatureRef = useRef();
|
|
273
294
|
const [managedProvider, setManagedProvider] = useState();
|
|
274
295
|
const setProviderError = useCallback((provider, message) => {
|
|
@@ -362,34 +383,91 @@ export function SubscriptionsSection(props) {
|
|
|
362
383
|
};
|
|
363
384
|
}, [refresh, startPolling]);
|
|
364
385
|
const loadUsage = useCallback(async (provider, account, force = false) => {
|
|
386
|
+
if (rpc === undefined)
|
|
387
|
+
return;
|
|
365
388
|
const key = `${provider}:${account}`;
|
|
389
|
+
const pending = usageInflightRef.current.get(key);
|
|
390
|
+
// A forced read must never be dropped behind a poll that started before the
|
|
391
|
+
// state changed. The refresh after a credit redemption is the only thing
|
|
392
|
+
// that picks up the new numbers, and the next automatic poll is minutes
|
|
393
|
+
// away, so waiting the earlier read out is cheaper than showing a stale
|
|
394
|
+
// percentage next to a success message.
|
|
395
|
+
if (pending !== undefined) {
|
|
396
|
+
if (!force)
|
|
397
|
+
return;
|
|
398
|
+
await pending;
|
|
399
|
+
}
|
|
366
400
|
if (rpc === undefined || usageInflightRef.current.has(key))
|
|
367
401
|
return;
|
|
368
|
-
|
|
369
|
-
|
|
402
|
+
const read = (async () => {
|
|
403
|
+
setUsageLoading(prev => ({ ...prev, [key]: true }));
|
|
404
|
+
try {
|
|
405
|
+
const usage = await callSubscriptionsAuth(rpc, 'usage', { provider, account, ...force ? { force: true } : {} });
|
|
406
|
+
if (!mountedRef.current)
|
|
407
|
+
return;
|
|
408
|
+
setUsages(prev => ({ ...prev, [key]: usage }));
|
|
409
|
+
setObservedAt(prev => ({ ...prev, [key]: usage.observedAt ?? Date.now() }));
|
|
410
|
+
window.dispatchEvent(new Event(USAGE_BADGE_REFRESH_EVENT));
|
|
411
|
+
setUsageErrors((prev) => {
|
|
412
|
+
const next = { ...prev };
|
|
413
|
+
delete next[key];
|
|
414
|
+
return next;
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
catch (error) {
|
|
418
|
+
if (mountedRef.current)
|
|
419
|
+
setUsageErrors(prev => ({ ...prev, [key]: messageOf(error) }));
|
|
420
|
+
}
|
|
421
|
+
finally {
|
|
422
|
+
usageInflightRef.current.delete(key);
|
|
423
|
+
if (mountedRef.current)
|
|
424
|
+
setUsageLoading(prev => ({ ...prev, [key]: false }));
|
|
425
|
+
}
|
|
426
|
+
})();
|
|
427
|
+
usageInflightRef.current.set(key, read);
|
|
428
|
+
await read;
|
|
429
|
+
}, [rpc]);
|
|
430
|
+
async function prepareReset(account) {
|
|
431
|
+
if (!rpc || resetInflight.current)
|
|
432
|
+
return;
|
|
433
|
+
resetInflight.current = true;
|
|
434
|
+
setResetBusy(true);
|
|
435
|
+
setResetAccount(account);
|
|
436
|
+
setResetConfirmation(undefined);
|
|
437
|
+
setResetMessage('');
|
|
370
438
|
try {
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
setUsages(prev => ({ ...prev, [key]: usage }));
|
|
375
|
-
setObservedAt(prev => ({ ...prev, [key]: usage.observedAt ?? Date.now() }));
|
|
376
|
-
window.dispatchEvent(new Event(USAGE_BADGE_REFRESH_EVENT));
|
|
377
|
-
setUsageErrors((prev) => {
|
|
378
|
-
const next = { ...prev };
|
|
379
|
-
delete next[key];
|
|
380
|
-
return next;
|
|
381
|
-
});
|
|
439
|
+
const result = await callSubscriptionsAuth(rpc, 'prepareReset', { account });
|
|
440
|
+
setResetAcknowledged(false);
|
|
441
|
+
setResetConfirmation({ ...result, account });
|
|
382
442
|
}
|
|
383
443
|
catch (error) {
|
|
384
|
-
|
|
385
|
-
setUsageErrors(prev => ({ ...prev, [key]: messageOf(error) }));
|
|
444
|
+
setResetMessage(messageOf(error));
|
|
386
445
|
}
|
|
387
446
|
finally {
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
setUsageLoading(prev => ({ ...prev, [key]: false }));
|
|
447
|
+
resetInflight.current = false;
|
|
448
|
+
setResetBusy(false);
|
|
391
449
|
}
|
|
392
|
-
}
|
|
450
|
+
}
|
|
451
|
+
async function consumeReset() {
|
|
452
|
+
if (!rpc || !resetConfirmation || !resetAcknowledged || resetInflight.current)
|
|
453
|
+
return;
|
|
454
|
+
const { account, ticket } = resetConfirmation;
|
|
455
|
+
resetInflight.current = true;
|
|
456
|
+
setResetBusy(true);
|
|
457
|
+
setResetConfirmation(undefined);
|
|
458
|
+
try {
|
|
459
|
+
await callSubscriptionsAuth(rpc, 'consumeReset', { account, ticket });
|
|
460
|
+
setResetMessage(t('resetUseSuccess'));
|
|
461
|
+
}
|
|
462
|
+
catch (error) {
|
|
463
|
+
setResetMessage(messageOf(error));
|
|
464
|
+
}
|
|
465
|
+
finally {
|
|
466
|
+
await loadUsage('codex', account, true);
|
|
467
|
+
resetInflight.current = false;
|
|
468
|
+
setResetBusy(false);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
393
471
|
// Fetch usage once an account is logged in; drop the snapshots of accounts
|
|
394
472
|
// that vanished so a re-login refetches. A failed lookup does not auto-retry
|
|
395
473
|
// — the per-account Refresh button is the retry path.
|
|
@@ -514,7 +592,7 @@ export function SubscriptionsSection(props) {
|
|
|
514
592
|
if (rpc === undefined) {
|
|
515
593
|
return _jsx("p", { style: styles.intro, children: t('unavailable') });
|
|
516
594
|
}
|
|
517
|
-
return (_jsxs("div", { style: styles.section, children: [_jsx("p", { style: styles.intro, children: t('intro') }), _jsx(UsageBadgeDisplaySetting, { t: t }), PROVIDERS.map(({ id, name }) => {
|
|
595
|
+
return (_jsxs("div", { className: "dsh-subscriptions-settings", style: styles.section, children: [_jsx("style", { children: providerSettingsCss }), _jsx("p", { style: styles.intro, children: t('intro') }), _jsx(UsageBadgeDisplaySetting, { t: t }), PROVIDERS.map(({ id, name }) => {
|
|
518
596
|
const status = statuses[id];
|
|
519
597
|
const busy = status?.busy === true;
|
|
520
598
|
const deviceCode = deviceCodes[id];
|
|
@@ -524,17 +602,29 @@ export function SubscriptionsSection(props) {
|
|
|
524
602
|
const usage = usages[usageKey];
|
|
525
603
|
const usageError = usageErrors[usageKey];
|
|
526
604
|
const display = account.account ?? account.key;
|
|
605
|
+
const sortedCredits = [...(usage?.resetCredits ?? [])].sort((a, b) => (a.expiresAt ?? Infinity) - (b.expiresAt ?? Infinity));
|
|
606
|
+
const nextReset = sortedCredits.find(c => c.expiresAt === undefined || c.expiresAt > Date.now());
|
|
607
|
+
const resetBlock = usage ? resetActionBlock(usage) : 'resetUseRefresh';
|
|
608
|
+
const resetDisabled = resetBusy || resetBlock !== undefined;
|
|
527
609
|
// Providers without a usage endpoint answer supported:false — no block.
|
|
528
610
|
const showUsage = usage?.supported !== false
|
|
529
611
|
&& (usage !== undefined || usageError !== undefined || usageLoading[usageKey] === true);
|
|
530
612
|
return (_jsxs("div", { style: styles.accountRow, children: [_jsxs("div", { style: styles.accountHeader, children: [_jsx("button", { type: "button", style: styles.starButton, title: account.isDefault ? t('defaultBadge') : t('setDefault'), onClick: () => {
|
|
531
613
|
if (!account.isDefault)
|
|
532
614
|
void setDefault(id, account.key);
|
|
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: {
|
|
615
|
+
}, 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: { 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, 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) => {
|
|
534
616
|
const percent = displayUsedPercent(window.usedPercent);
|
|
535
617
|
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
618
|
&& ` · ${t('usageResets', { date: new Date(window.resetsAt).toLocaleString() })}`] })] }), _jsx(UsageMeter, { window: window, t: t, observedAt: observedAt[usageKey], stale: usageError !== undefined || usage.stale === true })] }, index));
|
|
537
|
-
})
|
|
538
|
-
|
|
539
|
-
|
|
619
|
+
}), (id === 'codex' || id === 'claude') && usage?.resetCreditsError !== undefined && (_jsx(ResetCreditsErrorLine, { provider: id, message: usage.resetCreditsError, t: t })), (id === 'codex' || id === 'claude') && showsResetCredits(id, usage?.resetCredits) && (_jsx(ResetCreditsDisclosure, { mode: id === 'claude' ? 'grants' : 'credits', credits: sortedCredits, t: t, ...nextReset !== undefined ? { nextCredit: nextReset } : {}, ...id === 'codex' ? { useAction: {
|
|
620
|
+
accountKey: account.key,
|
|
621
|
+
busy: resetBusy && resetAccount === account.key,
|
|
622
|
+
disabled: resetDisabled,
|
|
623
|
+
title: t(resetBusy ? 'resetUseChecking' : resetBlock ?? 'resetUseReady'),
|
|
624
|
+
label: t('resetUseButton'),
|
|
625
|
+
onUse: () => { if (!resetDisabled)
|
|
626
|
+
void prepareReset(account.key); },
|
|
627
|
+
} } : {}, ...resetAccount === account.key && resetMessage !== '' ? { statusMessage: resetMessage } : {} }))] }))] }, account.key));
|
|
628
|
+
}), _jsxs("div", { style: styles.actions, children: [!busy && accounts.length === 0 && (_jsx("button", { type: "button", onClick: () => { void login(id); }, children: t('login') })), !busy && accounts.length > 0 && id === 'claude' && (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", onClick: () => { void login(id, 'oauth'); }, children: t('addAccountOAuth') }), _jsx("button", { type: "button", onClick: () => { void login(id, 'keychain'); }, children: t('addAccountKeychain') })] })), !busy && accounts.length > 0 && id !== 'claude' && (_jsx("button", { type: "button", onClick: () => { void login(id); }, children: t('addAccount') })), _jsx("button", { type: "button", "aria-haspopup": "dialog", onClick: () => setManagedProvider({ id, name }), children: t('accountsManage') }), busy && (_jsx("button", { type: "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", onClick: () => { copyDeviceCode(id, deviceCode.userCode); }, children: copiedCode === id ? t('deviceCodeCopied') : t('deviceCodeCopy') }), _jsx("button", { type: "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", onClick: () => { void submitManual(id); }, children: t('submit') })] })] }))] }, id));
|
|
629
|
+
}), _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) }), _jsx("style", { children: '.subscriptions-reset-dialog::backdrop { background: var(--dsw-alias-bg-mask-1, #0006); backdrop-filter: var(--dsw-mask-blur, none); }' }), _jsx("dialog", { ref: resetDialogRef, className: "subscriptions-reset-dialog", "aria-labelledby": "subscriptions-reset-title", onCancel: () => setResetConfirmation(undefined), onClose: () => setResetConfirmation(undefined), style: { ...styles.modal, display: resetConfirmation ? 'flex' : 'none', margin: 'auto', color: 'var(--dsw-alias-label-primary)' }, children: resetConfirmation && _jsxs(_Fragment, { children: [_jsx("h3", { id: "subscriptions-reset-title", style: { ...styles.modalTitle, margin: 0 }, children: t('resetUseConfirmTitle') }), _jsx("p", { style: { ...styles.statusLine, overflowWrap: 'anywhere' }, children: statuses.codex?.accounts.find(a => a.key === resetConfirmation.account)?.account ?? resetConfirmation.account }), _jsxs("div", { style: { ...styles.usageMeta, gap: 12 }, children: [_jsx("span", { children: t('usageWeekly') }), _jsxs("strong", { children: [Math.round(resetConfirmation.weeklyUsedPercent), "%"] })] }), _jsx("div", { style: styles.usageMeta, children: _jsx("span", { children: resetConfirmation.creditExpiresAt === undefined ? t('resetCreditExpiryUnknown') : t('resetCreditExpires', { date: new Date(resetConfirmation.creditExpiresAt).toLocaleString() }) }) }), _jsx("p", { style: { ...styles.statusLine, margin: '4px 0' }, children: t('resetUseWarning') }), _jsxs("label", { style: { display: 'flex', gap: 8, alignItems: 'flex-start', fontSize: 12 }, children: [_jsx("input", { type: "checkbox", checked: resetAcknowledged, onChange: e => setResetAcknowledged(e.target.checked) }), t('resetUseAcknowledge')] }), _jsxs("div", { className: "dsh-subscription-dialog-actions", style: { ...styles.actions, justifyContent: 'flex-end', marginTop: 8 }, children: [_jsx("button", { type: "button", autoFocus: true, onClick: () => setResetConfirmation(undefined), children: t('cancel') }), _jsx("button", { type: "button", className: "dsh-subscription-primary", disabled: !resetAcknowledged || resetBusy, onClick: () => { void consumeReset(); }, children: t('resetUseConfirm') })] })] }) })] }));
|
|
540
630
|
}
|
|
@@ -9,7 +9,7 @@ export function UsageBadgeDisplaySetting({ t }) {
|
|
|
9
9
|
const [failed, setFailed] = useState(false);
|
|
10
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 => {
|
|
11
11
|
const value = event.currentTarget.value;
|
|
12
|
-
if (value !== 'recent' && value !== 'hidden')
|
|
12
|
+
if (value !== 'recent' && value !== 'always' && value !== 'hidden')
|
|
13
13
|
return;
|
|
14
14
|
try {
|
|
15
15
|
setUsageBadgeMode(value);
|
|
@@ -20,7 +20,7 @@ export function UsageBadgeDisplaySetting({ t }) {
|
|
|
20
20
|
throw error;
|
|
21
21
|
setFailed(true);
|
|
22
22
|
}
|
|
23
|
-
}, children: [_jsx("option", { value: "recent", children: t('usageBadgeDisplayRecent') }), _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 => {
|
|
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
24
|
const value = event.currentTarget.value;
|
|
25
25
|
if (!isUsageColorPreset(value))
|
|
26
26
|
return;
|
|
@@ -39,15 +39,11 @@ export function UsageBadgeDisplaySetting({ t }) {
|
|
|
39
39
|
}
|
|
40
40
|
const styles = {
|
|
41
41
|
card: {
|
|
42
|
-
border: '
|
|
42
|
+
border: '0.5px solid var(--dsw-alias-settings-card-stroke, var(--dsw-alias-border-l4))',
|
|
43
|
+
borderRadius: 'var(--dsw-radius-xl, 20px)', background: 'var(--dsw-alias-settings-card-fill, var(--dsw-alias-bg-layer-2))',
|
|
43
44
|
padding: '12px 14px', display: 'flex', flexDirection: 'column', gap: 6,
|
|
44
45
|
},
|
|
45
|
-
field: { display: 'flex', flexDirection: 'column', gap:
|
|
46
|
-
select: {
|
|
47
|
-
height: 32, width: '100%', boxSizing: 'border-box',
|
|
48
|
-
border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 8,
|
|
49
|
-
padding: '0 10px', font: 'inherit',
|
|
50
|
-
background: 'var(--dsw-alias-bg-layer-1)', color: 'var(--dsw-alias-label-primary)',
|
|
51
|
-
},
|
|
46
|
+
field: { display: 'flex', flexDirection: 'column', gap: 6, fontSize: 13, fontWeight: 500, lineHeight: '20px' },
|
|
47
|
+
select: { width: '100%' },
|
|
52
48
|
hint: { margin: 0, fontSize: 12, lineHeight: '18px', color: 'var(--dsw-alias-label-tertiary)' },
|
|
53
49
|
};
|
package/lib/client/UsageMeter.js
CHANGED
|
@@ -20,17 +20,22 @@ export function UsageMeter({ window: quota, t, style, observedAt, stale }) {
|
|
|
20
20
|
const valid = validUsage(quota.usedPercent);
|
|
21
21
|
const used = valid ? quota.usedPercent : 0;
|
|
22
22
|
const fresh = isUsageFresh(quota, { observedAt, stale }, current);
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const
|
|
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);
|
|
26
31
|
const label = [
|
|
27
32
|
valid ? t('usageMeterUsed', { used: Math.round(used), remaining: Math.round(100 - used) }) : t('usageMeterInvalid'),
|
|
28
33
|
// An invalid reading already states unavailability; the stale line would repeat it.
|
|
29
34
|
!valid ? '' : !fresh ? t('usageMeterStale') : state === 'red' ? t('usageMeterNearLimit')
|
|
30
|
-
:
|
|
35
|
+
: pace === undefined && preset !== 'remaining' ? t('usageMeterUnknownTime')
|
|
31
36
|
: state === 'yellow' ? t('usageMeterAhead') : t('usageMeterNoWarning'),
|
|
32
37
|
elapsed === undefined ? '' : t('usageMeterPace', { elapsed: Math.round(elapsed) }),
|
|
33
|
-
|
|
38
|
+
pace === undefined ? '' : lead > 0 ? t('usageMeterPaceAhead', { points: lead })
|
|
34
39
|
: lead < 0 ? t('usageMeterPaceBehind', { points: -lead }) : t('usageMeterPaceEven'),
|
|
35
40
|
quota.resetsAt !== undefined && quota.resetsAt > current ? t('usageMeterReset', {
|
|
36
41
|
duration: resetCountdownParts(quota.resetsAt - current)
|
|
@@ -43,7 +48,7 @@ export function UsageMeter({ window: quota, t, style, observedAt, stale }) {
|
|
|
43
48
|
background: 'var(--dsw-alias-bg-layer-1)', border: '1px solid var(--dsw-alias-border-l2)',
|
|
44
49
|
...style, position: 'relative',
|
|
45
50
|
}, children: [_jsx("div", { style: { height: '100%', borderRadius: 'inherit', width: `${used}%`,
|
|
46
|
-
background: usageBarColor(quota.usedPercent,
|
|
51
|
+
background: usageBarColor(quota.usedPercent, pace, preset, fresh) } }), elapsed !== undefined && _jsx("span", { "aria-hidden": "true", "data-usage-time-marker": true, style: {
|
|
47
52
|
position: 'absolute', left: `clamp(0px, calc(${elapsed}% - 1px), calc(100% - 2px))`,
|
|
48
53
|
top: 0, bottom: 0, width: 2, background: 'var(--dsw-alias-label-primary)',
|
|
49
54
|
boxShadow: '0 0 0 1px var(--dsw-alias-bg-layer-1)',
|
|
@@ -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-menu-backdrop-filter", "--dsw-specific-menu"];
|
|
86
|
+
readonly names: readonly ["--dsw-alias-bg-layer-1", "--dsw-alias-bg-layer-2", "--dsw-alias-bg-layer-3", "--dsw-alias-bg-mask-1", "--dsw-alias-bg-skeleton", "--dsw-alias-border-l1", "--dsw-alias-border-l2", "--dsw-alias-border-l2-darkmode-thin", "--dsw-alias-border-l3", "--dsw-alias-border-l4", "--dsw-alias-brand-primary", "--dsw-alias-button-primary-fill", "--dsw-alias-button-primary-hover", "--dsw-alias-interactive-bg-active", "--dsw-alias-interactive-bg-hover", "--dsw-alias-interactive-bg-hover-solid", "--dsw-alias-label-dimmed", "--dsw-alias-label-primary", "--dsw-alias-label-primary-foreground", "--dsw-alias-label-secondary", "--dsw-alias-label-tertiary", "--dsw-alias-settings-card-fill", "--dsw-alias-settings-card-stroke", "--dsw-alias-state-business-primary", "--dsw-alias-state-error-primary", "--dsw-alias-state-success-primary", "--dsw-alias-state-warn-label", "--dsw-elevation-prominent", "--dsw-focus-ring-color", "--dsw-focus-ring-width", "--dsw-mask-blur", "--dsw-menu-backdrop-filter", "--dsw-radius-lg", "--dsw-radius-md", "--dsw-radius-panel", "--dsw-radius-sm", "--dsw-radius-xl", "--dsw-specific-menu"];
|
|
87
87
|
};
|
|
88
88
|
};
|
|
89
89
|
/** A glyph name listed in {@link HOST_CONTRACT}; hostIcon() accepts no other. */
|
|
@@ -82,20 +82,42 @@ export const HOST_CONTRACT = {
|
|
|
82
82
|
package: '@deepseek-ai/dsh-client-ui-theme',
|
|
83
83
|
names: [
|
|
84
84
|
'--dsw-alias-bg-layer-1',
|
|
85
|
+
'--dsw-alias-bg-layer-2',
|
|
86
|
+
'--dsw-alias-bg-layer-3',
|
|
87
|
+
'--dsw-alias-bg-mask-1',
|
|
85
88
|
'--dsw-alias-bg-skeleton',
|
|
89
|
+
'--dsw-alias-border-l1',
|
|
86
90
|
'--dsw-alias-border-l2',
|
|
87
91
|
'--dsw-alias-border-l2-darkmode-thin',
|
|
92
|
+
'--dsw-alias-border-l3',
|
|
93
|
+
'--dsw-alias-border-l4',
|
|
94
|
+
'--dsw-alias-brand-primary',
|
|
95
|
+
'--dsw-alias-button-primary-fill',
|
|
96
|
+
'--dsw-alias-button-primary-hover',
|
|
97
|
+
'--dsw-alias-interactive-bg-active',
|
|
88
98
|
'--dsw-alias-interactive-bg-hover',
|
|
89
99
|
'--dsw-alias-interactive-bg-hover-solid',
|
|
90
100
|
'--dsw-alias-label-dimmed',
|
|
91
101
|
'--dsw-alias-label-primary',
|
|
102
|
+
'--dsw-alias-label-primary-foreground',
|
|
92
103
|
'--dsw-alias-label-secondary',
|
|
93
104
|
'--dsw-alias-label-tertiary',
|
|
105
|
+
'--dsw-alias-settings-card-fill',
|
|
106
|
+
'--dsw-alias-settings-card-stroke',
|
|
107
|
+
'--dsw-alias-state-business-primary',
|
|
94
108
|
'--dsw-alias-state-error-primary',
|
|
95
109
|
'--dsw-alias-state-success-primary',
|
|
96
110
|
'--dsw-alias-state-warn-label',
|
|
97
111
|
'--dsw-elevation-prominent',
|
|
112
|
+
'--dsw-focus-ring-color',
|
|
113
|
+
'--dsw-focus-ring-width',
|
|
114
|
+
'--dsw-mask-blur',
|
|
98
115
|
'--dsw-menu-backdrop-filter',
|
|
116
|
+
'--dsw-radius-lg',
|
|
117
|
+
'--dsw-radius-md',
|
|
118
|
+
'--dsw-radius-panel',
|
|
119
|
+
'--dsw-radius-sm',
|
|
120
|
+
'--dsw-radius-xl',
|
|
99
121
|
'--dsw-specific-menu',
|
|
100
122
|
],
|
|
101
123
|
},
|