dsh-subscriptions 0.1.1 → 0.1.3

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