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.
Files changed (68) hide show
  1. package/README.md +92 -10
  2. package/lib/auth/rpc.d.ts +3 -1
  3. package/lib/auth/rpc.js +10 -1
  4. package/lib/client/ExternalUsageCards.d.ts +11 -1
  5. package/lib/client/ExternalUsageCards.js +18 -3
  6. package/lib/client/ProviderAccountManager.js +34 -41
  7. package/lib/client/ProviderModelEditor.js +5 -9
  8. package/lib/client/SpeedSelect.js +8 -7
  9. package/lib/client/SubscriptionUsageBadge.d.ts +19 -2
  10. package/lib/client/SubscriptionUsageBadge.js +65 -9
  11. package/lib/client/SubscriptionsSection.d.ts +22 -0
  12. package/lib/client/SubscriptionsSection.js +128 -38
  13. package/lib/client/UsageBadgeDisplaySetting.js +6 -10
  14. package/lib/client/UsageMeter.js +11 -6
  15. package/lib/client/host-contract.d.ts +1 -1
  16. package/lib/client/host-contract.js +22 -0
  17. package/lib/client/locales.d.ts +60 -0
  18. package/lib/client/locales.js +62 -2
  19. package/lib/client/provider-settings-styles.d.ts +3 -0
  20. package/lib/client/provider-settings-styles.js +206 -0
  21. package/lib/client/reset-action.d.ts +14 -0
  22. package/lib/client/reset-action.js +15 -0
  23. package/lib/client/reset-credits-view.d.ts +53 -0
  24. package/lib/client/reset-credits-view.js +103 -0
  25. package/lib/client/usage-badge-preferences.d.ts +7 -2
  26. package/lib/client/usage-badge-preferences.js +3 -2
  27. package/lib/client/usage-pace.d.ts +14 -1
  28. package/lib/client/usage-pace.js +14 -1
  29. package/lib/client.js +1036 -334
  30. package/lib/client.js.map +1 -1
  31. package/lib/index.d.ts +9 -0
  32. package/lib/index.js +1120 -602
  33. package/lib/providers/account-preferences.js +3 -2
  34. package/lib/providers/catalog-store.d.ts +3 -2
  35. package/lib/providers/catalog-store.js +57 -26
  36. package/lib/providers/claude.d.ts +22 -3
  37. package/lib/providers/claude.js +113 -15
  38. package/lib/providers/codex.d.ts +15 -2
  39. package/lib/providers/codex.js +72 -4
  40. package/lib/providers/common.d.ts +25 -0
  41. package/lib/providers/common.js +22 -18
  42. package/lib/providers/copilot.d.ts +2 -1
  43. package/lib/providers/copilot.js +10 -3
  44. package/lib/providers/grok.d.ts +2 -2
  45. package/lib/providers/grok.js +2 -2
  46. package/lib/providers/minimax-usage.d.ts +22 -1
  47. package/lib/providers/minimax-usage.js +37 -7
  48. package/lib/providers/pool-health.d.ts +2 -0
  49. package/lib/providers/pool-health.js +9 -0
  50. package/lib/providers/pool-usage.d.ts +13 -1
  51. package/lib/providers/pool-usage.js +39 -7
  52. package/lib/providers/pool.d.ts +7 -5
  53. package/lib/providers/pool.js +89 -12
  54. package/lib/providers/rate-limit.d.ts +0 -14
  55. package/lib/providers/rate-limit.js +0 -26
  56. package/lib/providers/replay.d.ts +4 -0
  57. package/lib/providers/replay.js +50 -0
  58. package/lib/providers/reset-redemption.d.ts +51 -0
  59. package/lib/providers/reset-redemption.js +102 -0
  60. package/lib/translate/anthropic.d.ts +21 -2
  61. package/lib/translate/anthropic.js +35 -11
  62. package/lib/translate/antigravity.js +13 -6
  63. package/lib/translate/chat-completions.js +1 -1
  64. package/lib/translate/resolved.d.ts +21 -0
  65. package/lib/translate/resolved.js +63 -3
  66. package/lib/translate/responses.js +8 -6
  67. package/lib/translate/sse.js +8 -0
  68. 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
- const collapsed = collapsedDisplays(displays, badgeSelection?.provider);
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
- const label = compactSegment(collapsed[0], badgeSelection?.model, translate);
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
- const expanded = expandedDisplays(displays, badgeSelection?.provider);
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 === badgeSelection?.provider ? badgeSelection.model : undefined, provider: d.provider, translate: translate }, `${d.provider}:${badgeSelection?.model ?? ''}`)] }, account.key)))] }, d.provider)))] }), document.body)] }));
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: 24,
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: 12, padding: 16, fontSize: 12, lineHeight: '18px',
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
- * ModelsSection vocabulary minus hover rules, which inline styles cannot
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
- button: cardStyles.button,
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: cardStyles.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', background: 'transparent', padding: 0,
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: '1px solid var(--dsw-alias-border-l2)', borderRadius: 8,
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: 'rgba(0, 0, 0, 0.45)',
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: 12,
125
- background: 'var(--dsw-alias-bg-layer-1)', border: '1px solid var(--dsw-alias-border-l2)',
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: 600, fontSize: 15, lineHeight: '22px', color: 'var(--dsw-alias-label-primary)' },
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
- const usageInflightRef = useRef(new Set());
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
- usageInflightRef.current.add(key);
369
- setUsageLoading(prev => ({ ...prev, [key]: true }));
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 usage = await callSubscriptionsAuth(rpc, 'usage', { provider, account, ...force ? { force: true } : {} });
372
- if (!mountedRef.current)
373
- return;
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
- if (mountedRef.current)
385
- setUsageErrors(prev => ({ ...prev, [key]: messageOf(error) }));
444
+ setResetMessage(messageOf(error));
386
445
  }
387
446
  finally {
388
- usageInflightRef.current.delete(key);
389
- if (mountedRef.current)
390
- setUsageLoading(prev => ({ ...prev, [key]: false }));
447
+ resetInflight.current = false;
448
+ setResetBusy(false);
391
449
  }
392
- }, [rpc]);
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: { ...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) => {
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
- })] }))] }, account.key));
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));
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) })] }));
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: '1px solid var(--dsw-alias-border-l2)', borderRadius: 12,
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: 4, fontSize: 14, lineHeight: '22px' },
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
  };
@@ -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
- const elapsed = fresh ? elapsedPercent(quota, current) : undefined;
24
- const state = usageColorState(quota.usedPercent, elapsed, preset, fresh);
25
- const lead = elapsed === undefined ? 0 : Math.round(used - elapsed);
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
- : elapsed === undefined && preset !== 'remaining' ? t('usageMeterUnknownTime')
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
- elapsed === undefined ? '' : lead > 0 ? t('usageMeterPaceAhead', { points: lead })
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, elapsed, preset, fresh) } }), elapsed !== undefined && _jsx("span", { "aria-hidden": "true", "data-usage-time-marker": true, style: {
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
  },