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
package/lib/client.js CHANGED
@@ -114,6 +114,30 @@ const en = {
114
114
  usageMonthly: "Monthly",
115
115
  usageWindow: "Window",
116
116
  usageResets: "resets {date}",
117
+ usageMeterUsed: "Used {used}% · Remaining {remaining}%",
118
+ usageMeterPace: "Time elapsed {elapsed}% (vertical marker)",
119
+ usageMeterPaceAhead: "{points} points ahead of pace",
120
+ usageMeterPaceBehind: "{points} points behind pace",
121
+ usageMeterPaceEven: "On pace",
122
+ usageMeterInvalid: "Quota unavailable",
123
+ usageMeterStale: "Reading is stale or unavailable; refresh to confirm",
124
+ usageMeterNearLimit: "Near exhaustion",
125
+ usageMeterAhead: "Usage ahead of pace",
126
+ usageMeterNoWarning: "No quota warning",
127
+ usageMeterUnknownTime: "Time progress unknown",
128
+ usageMeterReset: "Resets in {duration}",
129
+ usageUnitDay: "{count} day",
130
+ usageUnitDays: "{count} days",
131
+ usageUnitHour: "{count} hour",
132
+ usageUnitHours: "{count} hours",
133
+ usageUnitMinute: "{count} minute",
134
+ usageUnitMinutes: "{count} minutes",
135
+ usageColorLabel: "Usage coloring",
136
+ usageColorStandard: "Standard (default · 10-point lead)",
137
+ usageColorRelaxed: "Relaxed (15-point lead)",
138
+ usageColorRemaining: "Remaining quota only (no yellow)",
139
+ usageColorPaceHint: "Yellow when usage leads elapsed time by {allowance} points; red at 90% used takes priority. Saved in this browser; only colors change.",
140
+ usageColorRemainingHint: "Red at 90% used; no yellow pace warnings. Reliable time markers remain visible. Saved in this browser.",
117
141
  usagePlan: "Plan: {plan}",
118
142
  externalUsageTitle: "Built-in provider usage",
119
143
  externalUsageConnected: "API key configured",
@@ -166,8 +190,9 @@ const en = {
166
190
  usageBadgeTitle: "Provider usage",
167
191
  usageBadgeDisplay: "Status-bar quota display",
168
192
  usageBadgeDisplayRecent: "Current / most recent provider with usage",
193
+ usageBadgeDisplayAlways: "Always show one provider, rotating through the rest",
169
194
  usageBadgeDisplayHidden: "Hidden",
170
- usageBadgeDisplayHint: "Saved automatically in this browser. One provider is shown at a time. Models without a quota source keep the most recent provider selected in this view, or show nothing. Hiding does not affect usage details here.",
195
+ usageBadgeDisplayHint: "Saved automatically in this browser. One provider is shown at a time. Models without a quota source keep the most recent provider selected in this view, or show nothing. Always show instead rotates through the providers with usage every 10 seconds while the current model has no quota source. Hiding does not affect usage details here.",
171
196
  usageBadgeDisplaySaveFailed: "Could not save the display preference. Check browser storage permissions.",
172
197
  usageBadgeCurrent: "current",
173
198
  usageBadgeModelCount: "{count} model quotas",
@@ -261,6 +286,30 @@ const zh = {
261
286
  usageMonthly: "每月",
262
287
  usageWindow: "窗口",
263
288
  usageResets: "{date} 重置",
289
+ usageMeterUsed: "已用 {used}% · 剩余 {remaining}%",
290
+ usageMeterPace: "时间已过 {elapsed}%(竖线标记)",
291
+ usageMeterPaceAhead: "领先时间进度 {points} 个百分点",
292
+ usageMeterPaceBehind: "落后时间进度 {points} 个百分点",
293
+ usageMeterPaceEven: "与时间进度一致",
294
+ usageMeterInvalid: "额度数据不可用",
295
+ usageMeterStale: "读数已陈旧或不可用,请刷新确认",
296
+ usageMeterNearLimit: "额度接近耗尽",
297
+ usageMeterAhead: "用量领先时间进度",
298
+ usageMeterNoWarning: "未触发额度警告",
299
+ usageMeterUnknownTime: "时间进度未知",
300
+ usageMeterReset: "{duration}后重置",
301
+ usageUnitDay: "{count} 天",
302
+ usageUnitDays: "{count} 天",
303
+ usageUnitHour: "{count} 小时",
304
+ usageUnitHours: "{count} 小时",
305
+ usageUnitMinute: "{count} 分钟",
306
+ usageUnitMinutes: "{count} 分钟",
307
+ usageColorLabel: "用量配色",
308
+ usageColorStandard: "标准(默认 · 领先 10 个百分点)",
309
+ usageColorRelaxed: "宽松(领先 15 个百分点)",
310
+ usageColorRemaining: "仅剩余额度(不显示黄色)",
311
+ usageColorPaceHint: "用量领先时间进度 {allowance} 个百分点时变黄;已用达到 90% 时优先变红。自动保存到当前浏览器,仅改变配色。",
312
+ usageColorRemainingHint: "已用达到 90% 时变红,不显示黄色节奏警告。可靠的时间标记仍保留,自动保存到当前浏览器。",
264
313
  usagePlan: "计划:{plan}",
265
314
  externalUsageTitle: "内置供应商用量",
266
315
  externalUsageConnected: "已配置 API 密钥",
@@ -313,8 +362,9 @@ const zh = {
313
362
  usageBadgeTitle: "服务商用量",
314
363
  usageBadgeDisplay: "状态栏额度显示",
315
364
  usageBadgeDisplayRecent: "仅显示当前/最近使用的用量服务商",
365
+ usageBadgeDisplayAlways: "始终显示一家,其余服务商轮播",
316
366
  usageBadgeDisplayHidden: "不显示",
317
- usageBadgeDisplayHint: "自动保存到当前浏览器。状态栏一次显示一家服务商;没有额度来源的模型沿用本视图最近选择的服务商,没有记录则隐藏。关闭不影响本页的额度详情。",
367
+ usageBadgeDisplayHint: "自动保存到当前浏览器。状态栏一次显示一家服务商;没有额度来源的模型沿用本视图最近选择的服务商,没有记录则隐藏。选择“始终显示”时,当前模型没有额度来源,就每 10 秒轮播一家有额度的服务商。关闭不影响本页的额度详情。",
318
368
  usageBadgeDisplaySaveFailed: "显示设置保存失败,请检查浏览器存储权限。",
319
369
  usageBadgeCurrent: "当前",
320
370
  usageBadgeModelCount: "{count} 个模型配额",
@@ -1342,17 +1392,6 @@ const subscriptionCardStyles = {
1342
1392
  fontSize: 12,
1343
1393
  lineHeight: "18px",
1344
1394
  color: "var(--dsw-alias-label-tertiary)"
1345
- },
1346
- usageTrack: {
1347
- height: 6,
1348
- borderRadius: 3,
1349
- overflow: "hidden",
1350
- background: "var(--dsw-alias-bg-layer-1)",
1351
- border: "1px solid var(--dsw-alias-border-l2)"
1352
- },
1353
- usageFill: {
1354
- height: "100%",
1355
- borderRadius: 3
1356
1395
  }
1357
1396
  };
1358
1397
 
@@ -1365,7 +1404,8 @@ const USAGE_BADGE_REFRESH_EVENT = "dsh:subscriptions:usage-refresh";
1365
1404
  function readUsageBadgeMode() {
1366
1405
  if (typeof window === "undefined") return "recent";
1367
1406
  try {
1368
- return window.localStorage.getItem(MODE_KEY) === "hidden" ? "hidden" : "recent";
1407
+ const value = window.localStorage.getItem(MODE_KEY);
1408
+ return value === "hidden" || value === "always" ? value : "recent";
1369
1409
  } catch (error) {
1370
1410
  if (!(error instanceof DOMException) || error.name !== "SecurityError") throw error;
1371
1411
  return "recent";
@@ -1373,7 +1413,7 @@ function readUsageBadgeMode() {
1373
1413
  }
1374
1414
  /** Save first, then notify; the settings control reports a rejected write. */
1375
1415
  function setUsageBadgeMode(mode) {
1376
- if (mode !== "recent" && mode !== "hidden") throw new TypeError("Invalid subscription usage display mode");
1416
+ if (mode !== "recent" && mode !== "always" && mode !== "hidden") throw new TypeError("Invalid subscription usage display mode");
1377
1417
  window.localStorage.setItem(MODE_KEY, mode);
1378
1418
  window.dispatchEvent(new Event(MODE_EVENT));
1379
1419
  }
@@ -1394,30 +1434,233 @@ function useUsageBadgeMode() {
1394
1434
  return (0, react.useSyncExternalStore)(subscribeUsageBadgeMode, readUsageBadgeMode, () => "recent");
1395
1435
  }
1396
1436
 
1437
+ //#endregion
1438
+ //#region src/client/usage-pace.ts
1439
+ const USAGE_FRESHNESS_MS = 5 * 6e4;
1440
+ function validUsage(used) {
1441
+ return Number.isFinite(used) && used >= 0 && used <= 100;
1442
+ }
1443
+ /**
1444
+ * Rounded share for display, or undefined when the provider's number is
1445
+ * unusable. Callers must show the unavailable state rather than clamp an
1446
+ * out-of-range reading into 0% or 100%.
1447
+ */
1448
+ function displayUsedPercent(used) {
1449
+ return validUsage(used) ? Math.round(used) : void 0;
1450
+ }
1451
+ /**
1452
+ * Reset countdown as the two largest nonzero whole units, rounded up so a
1453
+ * window never announces zero remaining time. Always at least one minute.
1454
+ */
1455
+ function resetCountdownParts(ms) {
1456
+ const total = Math.max(1, Math.ceil(ms / 6e4));
1457
+ return [
1458
+ {
1459
+ unit: "day",
1460
+ count: Math.floor(total / 1440)
1461
+ },
1462
+ {
1463
+ unit: "hour",
1464
+ count: Math.floor(total % 1440 / 60)
1465
+ },
1466
+ {
1467
+ unit: "minute",
1468
+ count: total % 60
1469
+ }
1470
+ ].filter((part) => part.count > 0).slice(0, 2);
1471
+ }
1472
+ /**
1473
+ * Whether the reported percentage still describes the window now. Only a fresh
1474
+ * reading may be compared with the current time (pace warnings); an old one
1475
+ * keeps its absolute color. This does not decide whether the time cursor is
1476
+ * drawn: where the window stands in time never depends on the reading.
1477
+ */
1478
+ function isUsageFresh(window$1, observation, now = Date.now()) {
1479
+ return validUsage(window$1.usedPercent) && observation.stale !== true && observation.observedAt !== void 0 && Number.isFinite(observation.observedAt) && observation.observedAt <= now && now - observation.observedAt <= USAGE_FRESHNESS_MS && (window$1.resetsAt === void 0 || Number.isFinite(window$1.resetsAt) && window$1.resetsAt > now);
1480
+ }
1481
+ /**
1482
+ * How far through its window the clock is, from the window's own timing alone.
1483
+ * It ignores the percentage and how recently it was read, so an unused,
1484
+ * stale, aged or unobserved reading keeps its cursor whenever the timing
1485
+ * places it. Returns undefined when the timing cannot: only an explicit
1486
+ * interval or a provider-verified fixed duration supports linear pace, and no
1487
+ * start is invented from a window's kind or label.
1488
+ */
1489
+ function elapsedPercent(window$1, now = Date.now()) {
1490
+ const end = window$1.resetsAt;
1491
+ if (end === void 0 || !Number.isFinite(end) || end <= now) return void 0;
1492
+ const duration = window$1.windowDurationMs;
1493
+ const start = window$1.startsAt ?? (window$1.fixedWindow === true && duration !== void 0 && Number.isFinite(duration) && duration > 0 ? end - duration : void 0);
1494
+ if (start === void 0 || !Number.isFinite(start) || start >= end || start > now) return void 0;
1495
+ return Math.min(100, Math.max(0, (now - start) / (end - start) * 100));
1496
+ }
1497
+ function usageColorState(used, elapsed, preset, fresh) {
1498
+ if (!validUsage(used)) return "neutral";
1499
+ if (used >= 90) return "red";
1500
+ if (!fresh || preset === "remaining" || elapsed === void 0) return "green";
1501
+ return used - elapsed >= (preset === "relaxed" ? 15 : 10) ? "yellow" : "green";
1502
+ }
1503
+ function usageBarColor(used, elapsed, preset = "standard", fresh = true) {
1504
+ const state = usageColorState(used, elapsed, preset, fresh);
1505
+ return state === "red" ? "var(--dsw-alias-state-error-primary)" : state === "yellow" ? "var(--dsw-alias-state-warn-label)" : "var(--dsw-alias-state-success-primary)";
1506
+ }
1507
+
1508
+ //#endregion
1509
+ //#region src/client/usage-color-preferences.ts
1510
+ const KEY = "dsh.subscriptions.usageColorPreset";
1511
+ const EVENT = "dsh:subscriptions:usage-color-preset";
1512
+ function isUsageColorPreset(value) {
1513
+ return value === "standard" || value === "relaxed" || value === "remaining";
1514
+ }
1515
+ function readUsageColorPreset() {
1516
+ if (typeof window === "undefined") return "standard";
1517
+ try {
1518
+ const value = window.localStorage.getItem(KEY);
1519
+ return isUsageColorPreset(value) ? value : "standard";
1520
+ } catch (error) {
1521
+ if (!(error instanceof DOMException) || error.name !== "SecurityError") throw error;
1522
+ return "standard";
1523
+ }
1524
+ }
1525
+ function setUsageColorPreset(value) {
1526
+ if (!isUsageColorPreset(value)) throw new TypeError("Invalid usage coloring preset");
1527
+ window.localStorage.setItem(KEY, value);
1528
+ window.dispatchEvent(new Event(EVENT));
1529
+ }
1530
+ function subscribeUsageColorPreset(notify) {
1531
+ const storage = (event) => {
1532
+ if (event.key === KEY || event.key === null) notify();
1533
+ };
1534
+ window.addEventListener(EVENT, notify);
1535
+ window.addEventListener("storage", storage);
1536
+ return () => {
1537
+ window.removeEventListener(EVENT, notify);
1538
+ window.removeEventListener("storage", storage);
1539
+ };
1540
+ }
1541
+ function useUsageColorPreset() {
1542
+ return (0, react.useSyncExternalStore)(subscribeUsageColorPreset, readUsageColorPreset, () => "standard");
1543
+ }
1544
+
1545
+ //#endregion
1546
+ //#region src/client/UsageMeter.tsx
1547
+ /** Singular/plural locale keys per countdown unit (zh maps both to one string). */
1548
+ const UNIT_KEYS = {
1549
+ day: ["usageUnitDay", "usageUnitDays"],
1550
+ hour: ["usageUnitHour", "usageUnitHours"],
1551
+ minute: ["usageUnitMinute", "usageUnitMinutes"]
1552
+ };
1553
+ function UsageMeter({ window: quota, t, style, observedAt, stale }) {
1554
+ const preset = useUsageColorPreset();
1555
+ const [now, setNow] = (0, react.useState)(Date.now);
1556
+ (0, react.useEffect)(() => {
1557
+ const timer = setInterval(() => setNow(Date.now()), 6e4);
1558
+ return () => clearInterval(timer);
1559
+ }, []);
1560
+ const current = Math.max(now, Date.now());
1561
+ const valid = validUsage(quota.usedPercent);
1562
+ const used = valid ? quota.usedPercent : 0;
1563
+ const fresh = isUsageFresh(quota, {
1564
+ observedAt,
1565
+ stale
1566
+ }, current);
1567
+ const elapsed = elapsedPercent(quota, current);
1568
+ const pace = fresh ? elapsed : void 0;
1569
+ const state = usageColorState(quota.usedPercent, pace, preset, fresh);
1570
+ const lead = pace === void 0 ? 0 : Math.round(used - pace);
1571
+ const label = [
1572
+ valid ? t("usageMeterUsed", {
1573
+ used: Math.round(used),
1574
+ remaining: Math.round(100 - used)
1575
+ }) : t("usageMeterInvalid"),
1576
+ !valid ? "" : !fresh ? t("usageMeterStale") : state === "red" ? t("usageMeterNearLimit") : pace === void 0 && preset !== "remaining" ? t("usageMeterUnknownTime") : state === "yellow" ? t("usageMeterAhead") : t("usageMeterNoWarning"),
1577
+ elapsed === void 0 ? "" : t("usageMeterPace", { elapsed: Math.round(elapsed) }),
1578
+ pace === void 0 ? "" : lead > 0 ? t("usageMeterPaceAhead", { points: lead }) : lead < 0 ? t("usageMeterPaceBehind", { points: -lead }) : t("usageMeterPaceEven"),
1579
+ quota.resetsAt !== void 0 && quota.resetsAt > current ? t("usageMeterReset", { duration: resetCountdownParts(quota.resetsAt - current).map((part) => t(UNIT_KEYS[part.unit][part.count === 1 ? 0 : 1], { count: part.count })).join(" ") }) : ""
1580
+ ].filter(Boolean).join(" · ");
1581
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1582
+ role: "img",
1583
+ "aria-label": label,
1584
+ title: label,
1585
+ "data-usage-color": state,
1586
+ style: {
1587
+ height: 6,
1588
+ borderRadius: 3,
1589
+ overflow: "hidden",
1590
+ background: "var(--dsw-alias-bg-layer-1)",
1591
+ border: "1px solid var(--dsw-alias-border-l2)",
1592
+ ...style,
1593
+ position: "relative"
1594
+ },
1595
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { style: {
1596
+ height: "100%",
1597
+ borderRadius: "inherit",
1598
+ width: `${used}%`,
1599
+ background: usageBarColor(quota.usedPercent, pace, preset, fresh)
1600
+ } }), elapsed !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1601
+ "aria-hidden": "true",
1602
+ "data-usage-time-marker": true,
1603
+ style: {
1604
+ position: "absolute",
1605
+ left: `clamp(0px, calc(${elapsed}% - 1px), calc(100% - 2px))`,
1606
+ top: 0,
1607
+ bottom: 0,
1608
+ width: 2,
1609
+ background: "var(--dsw-alias-label-primary)",
1610
+ boxShadow: "0 0 0 1px var(--dsw-alias-bg-layer-1)"
1611
+ }
1612
+ })]
1613
+ });
1614
+ }
1615
+
1397
1616
  //#endregion
1398
1617
  //#region src/client/ExternalUsageCards.tsx
1399
- const SOURCES = [{
1400
- id: "opencode-go",
1401
- name: "OpenCode Go",
1402
- ref: "OPENCODE_GO_API_KEY"
1403
- }, {
1404
- id: "kimi-code",
1405
- name: "Kimi Code",
1406
- ref: "KIMI_CODING_API_KEY"
1407
- }];
1618
+ const SOURCES = [
1619
+ {
1620
+ id: "opencode-go",
1621
+ name: "OpenCode Go",
1622
+ ref: "OPENCODE_GO_API_KEY"
1623
+ },
1624
+ {
1625
+ id: "kimi-code",
1626
+ name: "Kimi Code",
1627
+ ref: "KIMI_CODING_API_KEY"
1628
+ },
1629
+ {
1630
+ id: "minimax",
1631
+ name: "MiniMax",
1632
+ ref: "MINIMAX_API_KEY"
1633
+ },
1634
+ {
1635
+ id: "minimax-cn",
1636
+ name: "MiniMax CN",
1637
+ ref: "MINIMAX_CN_API_KEY"
1638
+ }
1639
+ ];
1408
1640
  function errorText(error) {
1409
1641
  return error instanceof Error ? error.message : String(error);
1410
1642
  }
1411
- function barColor$1(percent) {
1412
- if (percent >= 95) return "var(--dsw-alias-state-error-primary)";
1413
- if (percent >= 80) return "var(--dsw-alias-state-warn-label)";
1414
- return "var(--dsw-alias-state-success-primary)";
1643
+ /**
1644
+ * Row label of one window on an external usage card. A session or weekly
1645
+ * window appends the model it belongs to when the provider names one (MiniMax
1646
+ * reports a window per model), so two models' weekly rows stay distinguishable.
1647
+ * The monthly pool is a scope on an `other` window, not a model.
1648
+ * @param t - section translate.
1649
+ * @param window - the reported window.
1650
+ * @returns the localized label, e.g. "Weekly · general".
1651
+ */
1652
+ function externalUsageWindowLabel(t, window$1) {
1653
+ const named = window$1.scope !== void 0 && window$1.scope !== "";
1654
+ if (window$1.kind === "other") return window$1.scope === "Monthly" ? t("usageMonthly") : named ? window$1.scope : t("usageWindow");
1655
+ const base = window$1.kind === "session" ? t("usageSession") : t("usageWeekly");
1656
+ return named ? `${base} · ${window$1.scope}` : base;
1415
1657
  }
1416
1658
  /** Quota cards for API-key providers already available through DSH itself. */
1417
1659
  function ExternalUsageCards({ rpc, t }) {
1418
1660
  const [status, setStatus] = (0, react.useState)();
1419
1661
  const [statusError, setStatusError] = (0, react.useState)();
1420
1662
  const [usage, setUsage] = (0, react.useState)({});
1663
+ const [observedAt, setObservedAt] = (0, react.useState)({});
1421
1664
  const [errors, setErrors] = (0, react.useState)({});
1422
1665
  const [loading, setLoading] = (0, react.useState)({});
1423
1666
  const refresh = (0, react.useCallback)(async (source) => {
@@ -1425,16 +1668,20 @@ function ExternalUsageCards({ rpc, t }) {
1425
1668
  ...prev,
1426
1669
  [source]: true
1427
1670
  }));
1428
- setErrors((prev) => ({
1429
- ...prev,
1430
- [source]: void 0
1431
- }));
1432
1671
  try {
1433
1672
  const value = await callSubscriptionsAuth(rpc, "externalUsage", { source });
1434
1673
  setUsage((prev) => ({
1435
1674
  ...prev,
1436
1675
  [source]: value
1437
1676
  }));
1677
+ setObservedAt((prev) => ({
1678
+ ...prev,
1679
+ [source]: value.observedAt ?? Date.now()
1680
+ }));
1681
+ setErrors((prev) => ({
1682
+ ...prev,
1683
+ [source]: void 0
1684
+ }));
1438
1685
  window.dispatchEvent(new Event(USAGE_BADGE_REFRESH_EVENT));
1439
1686
  } catch (error) {
1440
1687
  setErrors((prev) => ({
@@ -1539,24 +1786,18 @@ function ExternalUsageCards({ rpc, t }) {
1539
1786
  children: t("usageEmpty")
1540
1787
  }),
1541
1788
  snapshot?.windows?.map((window$1, index) => {
1542
- const percent = Math.min(100, Math.max(0, window$1.usedPercent));
1543
- const label = window$1.kind === "session" ? t("usageSession") : window$1.kind === "weekly" ? t("usageWeekly") : window$1.scope === "Monthly" ? t("usageMonthly") : window$1.scope ?? t("usageWindow");
1789
+ const percent = displayUsedPercent(window$1.usedPercent);
1790
+ const label = externalUsageWindowLabel(t, window$1);
1544
1791
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1545
1792
  style: subscriptionCardStyles.usageRow,
1546
1793
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1547
1794
  style: subscriptionCardStyles.usageMeta,
1548
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
1549
- Math.round(percent),
1550
- "%",
1551
- window$1.resetsAt === void 0 ? "" : ` · ${t("usageResets", { date: new Date(window$1.resetsAt).toLocaleString() })}`
1552
- ] })]
1553
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1554
- style: subscriptionCardStyles.usageTrack,
1555
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { style: {
1556
- ...subscriptionCardStyles.usageFill,
1557
- width: `${percent}%`,
1558
- background: barColor$1(percent)
1559
- } })
1795
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [percent === void 0 ? t("usageMeterInvalid") : `${percent}%`, window$1.resetsAt === void 0 ? "" : ` · ${t("usageResets", { date: new Date(window$1.resetsAt).toLocaleString() })}`] })]
1796
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageMeter, {
1797
+ window: window$1,
1798
+ t,
1799
+ observedAt: observedAt[id],
1800
+ stale: errors[id] !== void 0 || snapshot?.stale === true
1560
1801
  })]
1561
1802
  }, index);
1562
1803
  })
@@ -1584,15 +1825,12 @@ function ExternalUsageCards({ rpc, t }) {
1584
1825
  function message(error) {
1585
1826
  return error instanceof Error ? error.message : String(error);
1586
1827
  }
1587
- function barColor(percent) {
1588
- if (percent >= 95) return "var(--dsw-alias-state-error-primary)";
1589
- if (percent >= 80) return "var(--dsw-alias-state-warn-label)";
1590
- return "var(--dsw-alias-state-success-primary)";
1591
- }
1592
1828
  /** Cursor's single connected account, rendered with the same card and Manage dialog as the other providers. */
1593
1829
  function CursorCard({ rpc, t }) {
1594
1830
  const [status, setStatus] = (0, react.useState)();
1595
1831
  const [usage, setUsage] = (0, react.useState)();
1832
+ const [observedAt, setObservedAt] = (0, react.useState)();
1833
+ const [usageStale, setUsageStale] = (0, react.useState)(false);
1596
1834
  const [authorizeUrl, setAuthorizeUrl] = (0, react.useState)();
1597
1835
  const [error, setError] = (0, react.useState)();
1598
1836
  const [loading, setLoading] = (0, react.useState)(false);
@@ -1615,11 +1853,15 @@ function CursorCard({ rpc, t }) {
1615
1853
  const refreshUsage = (0, react.useCallback)(async () => {
1616
1854
  setLoading(true);
1617
1855
  try {
1618
- setUsage(await callSubscriptionsAuth(rpc, "cursorUsage", {}));
1856
+ const value = await callSubscriptionsAuth(rpc, "cursorUsage", {});
1857
+ setUsage(value);
1858
+ setObservedAt(value.observedAt ?? Date.now());
1859
+ setUsageStale(value.stale === true);
1619
1860
  setError(void 0);
1620
1861
  window.dispatchEvent(new Event(USAGE_BADGE_REFRESH_EVENT));
1621
1862
  } catch (failure) {
1622
1863
  setError(message(failure));
1864
+ setUsageStale(true);
1623
1865
  } finally {
1624
1866
  setLoading(false);
1625
1867
  }
@@ -1665,6 +1907,8 @@ function CursorCard({ rpc, t }) {
1665
1907
  try {
1666
1908
  await callSubscriptionsAuth(rpc, "cursorLogout", {});
1667
1909
  setUsage(void 0);
1910
+ setObservedAt(void 0);
1911
+ setUsageStale(false);
1668
1912
  await refreshStatus();
1669
1913
  } catch (failure) {
1670
1914
  setError(message(failure));
@@ -1757,25 +2001,18 @@ function CursorCard({ rpc, t }) {
1757
2001
  children: t("usageEmpty")
1758
2002
  }),
1759
2003
  usage?.windows?.map((window$1, index) => {
1760
- const percent = Math.min(100, Math.max(0, window$1.usedPercent));
2004
+ const percent = displayUsedPercent(window$1.usedPercent);
1761
2005
  const label = window$1.scope === "Included" ? t("cursorIncluded") : window$1.scope === "Cursor Models" ? t("cursorModels") : window$1.scope === "Other Models" ? t("cursorOtherModels") : window$1.scope === "Included requests" ? t("cursorIncludedRequests") : window$1.scope ?? t("usageWindow");
1762
2006
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1763
2007
  style: subscriptionCardStyles.usageRow,
1764
2008
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1765
2009
  style: subscriptionCardStyles.usageMeta,
1766
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
1767
- Math.round(percent),
1768
- "%",
1769
- window$1.resetsAt === void 0 ? "" : ` · ${t("usageResets", { date: new Date(window$1.resetsAt).toLocaleString() })}`
1770
- ] })]
1771
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1772
- style: subscriptionCardStyles.usageTrack,
1773
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { style: {
1774
- width: `${percent}%`,
1775
- height: "100%",
1776
- borderRadius: 3,
1777
- background: barColor(percent)
1778
- } })
2010
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [percent === void 0 ? t("usageMeterInvalid") : `${percent}%`, window$1.resetsAt === void 0 ? "" : ` · ${t("usageResets", { date: new Date(window$1.resetsAt).toLocaleString() })}`] })]
2011
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageMeter, {
2012
+ window: window$1,
2013
+ t,
2014
+ observedAt,
2015
+ stale: usageStale
1779
2016
  })]
1780
2017
  }, index);
1781
2018
  })
@@ -1832,6 +2069,7 @@ function CursorCard({ rpc, t }) {
1832
2069
  /** Browser-local display preference; never changes provider or account settings. */
1833
2070
  function UsageBadgeDisplaySetting({ t }) {
1834
2071
  const mode = useUsageBadgeMode();
2072
+ const preset = useUsageColorPreset();
1835
2073
  const [failed, setFailed] = (0, react.useState)(false);
1836
2074
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1837
2075
  style: styles$6.card,
@@ -1844,7 +2082,7 @@ function UsageBadgeDisplaySetting({ t }) {
1844
2082
  value: mode,
1845
2083
  onChange: (event) => {
1846
2084
  const value = event.currentTarget.value;
1847
- if (value !== "recent" && value !== "hidden") return;
2085
+ if (value !== "recent" && value !== "always" && value !== "hidden") return;
1848
2086
  try {
1849
2087
  setUsageBadgeMode(value);
1850
2088
  setFailed(false);
@@ -1853,19 +2091,63 @@ function UsageBadgeDisplaySetting({ t }) {
1853
2091
  setFailed(true);
1854
2092
  }
1855
2093
  },
1856
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1857
- value: "recent",
1858
- children: t("usageBadgeDisplayRecent")
1859
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1860
- value: "hidden",
1861
- children: t("usageBadgeDisplayHidden")
1862
- })]
2094
+ children: [
2095
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2096
+ value: "recent",
2097
+ children: t("usageBadgeDisplayRecent")
2098
+ }),
2099
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2100
+ value: "always",
2101
+ children: t("usageBadgeDisplayAlways")
2102
+ }),
2103
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2104
+ value: "hidden",
2105
+ children: t("usageBadgeDisplayHidden")
2106
+ })
2107
+ ]
1863
2108
  })]
1864
2109
  }),
1865
2110
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1866
2111
  style: styles$6.hint,
1867
2112
  children: t("usageBadgeDisplayHint")
1868
2113
  }),
2114
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2115
+ style: styles$6.field,
2116
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("usageColorLabel") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
2117
+ style: styles$6.select,
2118
+ "aria-label": t("usageColorLabel"),
2119
+ value: preset,
2120
+ onChange: (event) => {
2121
+ const value = event.currentTarget.value;
2122
+ if (!isUsageColorPreset(value)) return;
2123
+ try {
2124
+ setUsageColorPreset(value);
2125
+ setFailed(false);
2126
+ } catch (error) {
2127
+ if (!(error instanceof DOMException) || !["SecurityError", "QuotaExceededError"].includes(error.name)) throw error;
2128
+ setFailed(true);
2129
+ }
2130
+ },
2131
+ children: [
2132
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2133
+ value: "standard",
2134
+ children: t("usageColorStandard")
2135
+ }),
2136
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2137
+ value: "relaxed",
2138
+ children: t("usageColorRelaxed")
2139
+ }),
2140
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2141
+ value: "remaining",
2142
+ children: t("usageColorRemaining")
2143
+ })
2144
+ ]
2145
+ })]
2146
+ }),
2147
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2148
+ style: styles$6.hint,
2149
+ children: t(preset === "remaining" ? "usageColorRemainingHint" : "usageColorPaceHint", { allowance: preset === "relaxed" ? 15 : 10 })
2150
+ }),
1869
2151
  failed && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1870
2152
  role: "alert",
1871
2153
  style: {
@@ -2017,8 +2299,6 @@ const styles$5 = {
2017
2299
  cursor: "pointer",
2018
2300
  color: "var(--dsw-alias-state-warn-label)"
2019
2301
  },
2020
- usageTrack: subscriptionCardStyles.usageTrack,
2021
- usageFill: subscriptionCardStyles.usageFill,
2022
2302
  deviceCode: {
2023
2303
  marginTop: 4,
2024
2304
  display: "flex",
@@ -2114,12 +2394,6 @@ function usageWindowLabel$1(t, window$1) {
2114
2394
  const base = window$1.kind === "session" ? t("usageSession") : window$1.kind === "weekly" ? t("usageWeekly") : t("usageWindow");
2115
2395
  return window$1.scope !== void 0 && window$1.scope !== "" ? `${base} · ${window$1.scope}` : base;
2116
2396
  }
2117
- /** Bar fill color: success normally, warn from 80%, error from 95%. Shared with the composer badge. */
2118
- function usageBarColor(usedPercent$1) {
2119
- if (usedPercent$1 >= 95) return "var(--dsw-alias-state-error-primary)";
2120
- if (usedPercent$1 >= 80) return "var(--dsw-alias-state-warn-label)";
2121
- return "var(--dsw-alias-state-success-primary)";
2122
- }
2123
2397
  /**
2124
2398
  * The Subscriptions settings page component.
2125
2399
  * @param props - the slot inject face ({@link SubscriptionsSectionInjected}).
@@ -2142,6 +2416,7 @@ function SubscriptionsSection(props) {
2142
2416
  const [copiedCode, setCopiedCode] = (0, react.useState)(void 0);
2143
2417
  /** Usage snapshots keyed `${provider}:${accountKey}` — every account tracks its own windows. */
2144
2418
  const [usages, setUsages] = (0, react.useState)({});
2419
+ const [observedAt, setObservedAt] = (0, react.useState)({});
2145
2420
  const [usageErrors, setUsageErrors] = (0, react.useState)({});
2146
2421
  const [usageLoading, setUsageLoading] = (0, react.useState)({});
2147
2422
  const mountedRef = (0, react.useRef)(true);
@@ -2242,6 +2517,10 @@ function SubscriptionsSection(props) {
2242
2517
  ...prev,
2243
2518
  [key]: usage
2244
2519
  }));
2520
+ setObservedAt((prev) => ({
2521
+ ...prev,
2522
+ [key]: usage.observedAt ?? Date.now()
2523
+ }));
2245
2524
  window.dispatchEvent(new Event(USAGE_BADGE_REFRESH_EVENT));
2246
2525
  setUsageErrors((prev) => {
2247
2526
  const next = { ...prev };
@@ -2542,19 +2821,17 @@ function SubscriptionsSection(props) {
2542
2821
  children: t("usageEmpty")
2543
2822
  }),
2544
2823
  (usage?.windows ?? []).map((window$1, index) => {
2545
- const percent = Math.min(100, Math.max(0, window$1.usedPercent));
2824
+ const percent = displayUsedPercent(window$1.usedPercent);
2546
2825
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2547
2826
  style: styles$5.usageRow,
2548
2827
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2549
2828
  style: styles$5.usageMeta,
2550
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: usageWindowLabel$1(t, window$1) }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [`${String(Math.round(percent))}%`, window$1.resetsAt !== void 0 && ` · ${t("usageResets", { date: new Date(window$1.resetsAt).toLocaleString() })}`] })]
2551
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2552
- style: styles$5.usageTrack,
2553
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { style: {
2554
- ...styles$5.usageFill,
2555
- width: `${String(percent)}%`,
2556
- background: usageBarColor(percent)
2557
- } })
2829
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: usageWindowLabel$1(t, window$1) }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [percent === void 0 ? t("usageMeterInvalid") : `${percent}%`, window$1.resetsAt !== void 0 && ` · ${t("usageResets", { date: new Date(window$1.resetsAt).toLocaleString() })}`] })]
2830
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageMeter, {
2831
+ window: window$1,
2832
+ t,
2833
+ observedAt: observedAt[usageKey],
2834
+ stale: usageError !== void 0 || usage.stale === true
2558
2835
  })]
2559
2836
  }, index);
2560
2837
  })
@@ -2989,6 +3266,7 @@ const HOST_CONTRACT = {
2989
3266
  "--dsw-alias-state-success-primary",
2990
3267
  "--dsw-alias-state-warn-label",
2991
3268
  "--dsw-elevation-prominent",
3269
+ "--dsw-menu-backdrop-filter",
2992
3270
  "--dsw-specific-menu"
2993
3271
  ]
2994
3272
  }
@@ -3654,6 +3932,8 @@ const UsageBadgeIcon = usageBadgeIcon(__deepseek_ai_dsh_client_ui_primitives);
3654
3932
  const USAGE_POLL_INTERVAL_MS = 15 * 6e4;
3655
3933
  /** How often the badge re-reads the session's current model (model switches arrive only by asking). */
3656
3934
  const MODEL_POLL_INTERVAL_MS = 3e3;
3935
+ /** How long the rotation holds one provider in `always` mode (a display cadence, not a data poll). */
3936
+ const BADGE_ROTATION_INTERVAL_MS = 1e4;
3657
3937
  /** Distance between the trigger's top edge and the dialog's bottom (host stat dialogs use the same). */
3658
3938
  const PANEL_GAP = 8;
3659
3939
  /** Distance kept between the dialog and each viewport edge. */
@@ -3670,7 +3950,9 @@ const PROVIDER_NAMES = {
3670
3950
  antigravity: "Antigravity",
3671
3951
  "cursor-subscription": "Cursor",
3672
3952
  "opencode-go": "OpenCode Go",
3673
- "kimi-coding": "Kimi Code"
3953
+ "kimi-coding": "Kimi Code",
3954
+ "minimax": "MiniMax",
3955
+ "minimax-cn": "MiniMax CN"
3674
3956
  };
3675
3957
  /** Key of one account's cached windows: `provider:accountKey`. */
3676
3958
  function usageKeyOf(provider, account) {
@@ -3742,7 +4024,12 @@ async function loadBadgeRoster(rpc) {
3742
4024
  }
3743
4025
  });
3744
4026
  }
3745
- if (external.status === "fulfilled") for (const [source, provider] of [["opencode-go", "opencode-go"], ["kimi-code", "kimi-coding"]]) {
4027
+ if (external.status === "fulfilled") for (const [source, provider] of [
4028
+ ["opencode-go", "opencode-go"],
4029
+ ["kimi-code", "kimi-coding"],
4030
+ ["minimax", "minimax"],
4031
+ ["minimax-cn", "minimax-cn"]
4032
+ ]) {
3746
4033
  refreshed.add(provider);
3747
4034
  if (external.value[source]?.configured) roster.push({
3748
4035
  provider,
@@ -3759,7 +4046,7 @@ async function loadBadgeRoster(rpc) {
3759
4046
  }
3760
4047
  async function usageOf(rpc, { provider, account }) {
3761
4048
  if (provider === "cursor-subscription") return callSubscriptionsAuth(rpc, "cursorUsage", {});
3762
- if (provider === "opencode-go" || provider === "kimi-coding") return callSubscriptionsAuth(rpc, "externalUsage", { source: provider === "kimi-coding" ? "kimi-code" : provider });
4049
+ if (provider === "opencode-go" || provider === "kimi-coding" || provider === "minimax" || provider === "minimax-cn") return callSubscriptionsAuth(rpc, "externalUsage", { source: provider === "kimi-coding" ? "kimi-code" : provider });
3763
4050
  return callSubscriptionsAuth(rpc, "usage", {
3764
4051
  provider,
3765
4052
  account: account.key
@@ -3786,7 +4073,7 @@ function createCurrentModelReader(models, sessionId) {
3786
4073
  * Falls back to the scope/kind abbreviation when no reset time is known.
3787
4074
  */
3788
4075
  function windowLabel(w) {
3789
- if (w.resetsAt === void 0) {
4076
+ if (w.resetsAt === void 0 || !Number.isFinite(w.resetsAt)) {
3790
4077
  if (w.scope !== void 0 && w.scope !== "") return w.scope;
3791
4078
  switch (w.kind) {
3792
4079
  case "session": return "5h";
@@ -3802,9 +4089,13 @@ function windowLabel(w) {
3802
4089
  if (hours > 0) return `${hours}h${minutes % 60}m`;
3803
4090
  return `${Math.max(1, minutes)}m`;
3804
4091
  }
3805
- /** Clamp and round a window's used share for display. */
3806
- function usedPercent(w) {
3807
- return Math.round(Math.min(100, Math.max(0, w.usedPercent)));
4092
+ /**
4093
+ * One compact segment piece (`6d1h 25%`); an unusable provider percentage
4094
+ * renders as the no-data dash, consistent with the meter's unavailable state.
4095
+ */
4096
+ function windowSegment(label, w) {
4097
+ const percent = displayUsedPercent(w.usedPercent);
4098
+ return percent === void 0 ? `${label} —` : `${label} ${percent}%`;
3808
4099
  }
3809
4100
  /** Keep model quotas separate: matching percentages do not imply a shared pool. */
3810
4101
  function prioritizeWindows(windows, model) {
@@ -3826,10 +4117,10 @@ function compactSegment(d, model, t = fallbackTranslate) {
3826
4117
  if (d.provider === "antigravity") {
3827
4118
  const matching = model === void 0 ? [] : windows.filter((w) => w.scope === model);
3828
4119
  if (matching.length === 0) return `${d.name} ${t(model === void 0 ? "usageBadgeModelCount" : "usageBadgeModelUnavailable", { count: new Set(windows.map((w) => w.scope).filter(Boolean)).size })}`;
3829
- const parts$1 = matching.slice(0, 2).map((w) => `${w.kind === "weekly" ? t("usageWeekly") : t("usageWindow")} ${usedPercent(w)}%`);
4120
+ const parts$1 = matching.slice(0, 2).map((w) => windowSegment(w.kind === "weekly" ? t("usageWeekly") : t("usageWindow"), w));
3830
4121
  return `${d.name} ${parts$1.join(" · ")}`;
3831
4122
  }
3832
- const parts = windows.slice(0, 2).map((w) => `${windowLabel(w)} ${usedPercent(w)}%`);
4123
+ const parts = windows.slice(0, 2).map((w) => windowSegment(windowLabel(w), w));
3833
4124
  if (windows.length > 2) parts.push(`+${windows.length - 2}`);
3834
4125
  return `${d.name} ${parts.join(" · ")}`;
3835
4126
  }
@@ -3842,6 +4133,24 @@ function collapsedDisplays(displays, current) {
3842
4133
  const match = displays.find((d) => d.provider === current);
3843
4134
  return match === void 0 ? [] : [match];
3844
4135
  }
4136
+ /**
4137
+ * `always` mode: the current model's provider while it reports usage,
4138
+ * otherwise the rotation steps through the providers that do, in display
4139
+ * order, wrapping around. The index is taken modulo the current provider
4140
+ * count, so providers appearing or dropping out stay inside range; the
4141
+ * sequence itself may shift when the roster changes. Unlike `recent`, the
4142
+ * retained subscription does not pin the pill — that retention is exactly
4143
+ * what this mode exists to escape.
4144
+ * @param displays - providers with usage, in display order.
4145
+ * @param current - the provider of the session's current model.
4146
+ * @param step - rotation tick; consecutive ticks advance one provider.
4147
+ */
4148
+ function rotatingDisplay(displays, current, step) {
4149
+ if (displays.length === 0) return [];
4150
+ const match = displays.find((d) => d.provider === current);
4151
+ if (match !== void 0) return [match];
4152
+ return [displays[((Number.isFinite(step) ? Math.floor(step) : 0) % displays.length + displays.length) % displays.length]];
4153
+ }
3845
4154
  /** Order for the expanded dialog: the current provider first, the rest in poll order. */
3846
4155
  function expandedDisplays(displays, current) {
3847
4156
  const match = displays.find((d) => d.provider === current);
@@ -3875,7 +4184,7 @@ function usageWindowLabel(t, window$1) {
3875
4184
  * every provider's accounts and their windows. Returns null when no data is
3876
4185
  * available.
3877
4186
  */
3878
- function SubscriptionUsageBadge({ rpc, currentModel, t }) {
4187
+ function SubscriptionUsageBadge({ rpc, currentModel, t, rotationMs }) {
3879
4188
  const translate = t ?? fallbackTranslate;
3880
4189
  const [displays, setDisplays] = (0, react.useState)([]);
3881
4190
  const [selection, setSelection] = (0, react.useState)(void 0);
@@ -3885,6 +4194,8 @@ function SubscriptionUsageBadge({ rpc, currentModel, t }) {
3885
4194
  const badgeSelection = retainSubscriptionSelection(lastSubscription, selection);
3886
4195
  const [open, setOpen] = (0, react.useState)(false);
3887
4196
  const [hover, setHover] = (0, react.useState)(false);
4197
+ const [rotation, setRotation] = (0, react.useState)(0);
4198
+ const [dialogLead, setDialogLead] = (0, react.useState)(void 0);
3888
4199
  const inflightRef = (0, react.useRef)(false);
3889
4200
  const mountedRef = (0, react.useRef)(true);
3890
4201
  const rootRef = (0, react.useRef)(null);
@@ -3893,6 +4204,7 @@ function SubscriptionUsageBadge({ rpc, currentModel, t }) {
3893
4204
  const currentRef = (0, react.useRef)(currentModel);
3894
4205
  currentRef.current = currentModel;
3895
4206
  const lastKnownRef = (0, react.useRef)(/* @__PURE__ */ new Map());
4207
+ const observationsRef = (0, react.useRef)(/* @__PURE__ */ new Map());
3896
4208
  const refresh = (0, react.useCallback)(async () => {
3897
4209
  if (rpc === void 0 || inflightRef.current) return;
3898
4210
  inflightRef.current = true;
@@ -3901,7 +4213,10 @@ function SubscriptionUsageBadge({ rpc, currentModel, t }) {
3901
4213
  if (!mountedRef.current) return;
3902
4214
  const lastKnown = lastKnownRef.current;
3903
4215
  const live = new Set(roster.map(({ provider, account }) => usageKeyOf(provider, account)));
3904
- for (const key of lastKnown.keys()) if (refreshed.has(key.split(":", 1)[0]) && !live.has(key)) lastKnown.delete(key);
4216
+ for (const key of lastKnown.keys()) if (refreshed.has(key.split(":", 1)[0]) && !live.has(key)) {
4217
+ lastKnown.delete(key);
4218
+ observationsRef.current.delete(key);
4219
+ }
3905
4220
  const results = await Promise.allSettled(roster.map(async ({ provider, account }) => {
3906
4221
  return {
3907
4222
  provider,
@@ -3913,6 +4228,13 @@ function SubscriptionUsageBadge({ rpc, currentModel, t }) {
3913
4228
  };
3914
4229
  }));
3915
4230
  if (!mountedRef.current) return;
4231
+ for (const { provider, account } of roster) {
4232
+ const key = usageKeyOf(provider, account);
4233
+ observationsRef.current.set(key, {
4234
+ ...observationsRef.current.get(key),
4235
+ stale: true
4236
+ });
4237
+ }
3916
4238
  const plans = /* @__PURE__ */ new Map();
3917
4239
  for (const r of results) {
3918
4240
  if (r.status !== "fulfilled") continue;
@@ -3921,12 +4243,37 @@ function SubscriptionUsageBadge({ rpc, currentModel, t }) {
3921
4243
  if (usage.plan !== void 0) plans.set(key, usage.plan);
3922
4244
  if (!usage.supported || !usage.windows || usage.windows.length === 0) {
3923
4245
  lastKnown.delete(key);
4246
+ observationsRef.current.delete(key);
3924
4247
  continue;
3925
4248
  }
3926
4249
  lastKnown.set(key, usage.windows);
4250
+ observationsRef.current.set(key, {
4251
+ observedAt: usage.observedAt ?? Date.now(),
4252
+ stale: usage.stale === true
4253
+ });
3927
4254
  }
3928
- setDisplays((previous) => [...groupUsageDisplays(roster, lastKnown, plans), ...previous.filter((display) => !refreshed.has(display.provider))]);
3929
- } catch {} finally {
4255
+ setDisplays((previous) => [...groupUsageDisplays(roster, lastKnown, plans).map((display) => ({
4256
+ ...display,
4257
+ accounts: display.accounts.map((account) => ({
4258
+ ...account,
4259
+ ...observationsRef.current.get(`${display.provider}:${account.key}`)
4260
+ }))
4261
+ })), ...previous.filter((display) => !refreshed.has(display.provider)).map((display) => ({
4262
+ ...display,
4263
+ accounts: display.accounts.map((account) => ({
4264
+ ...account,
4265
+ stale: true
4266
+ }))
4267
+ }))]);
4268
+ } catch {
4269
+ if (mountedRef.current) setDisplays((previous) => previous.map((display) => ({
4270
+ ...display,
4271
+ accounts: display.accounts.map((account) => ({
4272
+ ...account,
4273
+ stale: true
4274
+ }))
4275
+ })));
4276
+ } finally {
3930
4277
  inflightRef.current = false;
3931
4278
  }
3932
4279
  }, [rpc]);
@@ -4012,18 +4359,32 @@ function SubscriptionUsageBadge({ rpc, currentModel, t }) {
4012
4359
  (0, react.useEffect)(() => {
4013
4360
  if (displayMode === "hidden") setOpen(false);
4014
4361
  }, [displayMode]);
4362
+ (0, react.useEffect)(() => {
4363
+ if (displayMode !== "always") return;
4364
+ const interval = rotationMs !== void 0 && Number.isFinite(rotationMs) && rotationMs > 0 ? rotationMs : BADGE_ROTATION_INTERVAL_MS;
4365
+ const timer = setInterval(() => {
4366
+ setRotation((step) => step + 1);
4367
+ }, interval);
4368
+ return () => {
4369
+ clearInterval(timer);
4370
+ };
4371
+ }, [displayMode, rotationMs]);
4015
4372
  const seat = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4016
4373
  ref: seatRef,
4017
4374
  style: styles.seat,
4018
4375
  "aria-hidden": true
4019
4376
  });
4020
- const collapsed = collapsedDisplays(displays, badgeSelection?.provider);
4377
+ const collapsed = displayMode === "always" ? rotatingDisplay(displays, current, rotation) : collapsedDisplays(displays, badgeSelection?.provider);
4021
4378
  if (displayMode === "hidden" || collapsed.length === 0) return seat;
4022
- const label = compactSegment(collapsed[0], badgeSelection?.model, translate);
4023
- const expanded = expandedDisplays(displays, badgeSelection?.provider);
4379
+ const pillModel = displayMode === "always" ? current === collapsed[0]?.provider ? selection?.model : void 0 : badgeSelection?.model;
4380
+ const label = compactSegment(collapsed[0], pillModel, translate);
4381
+ const dialogProvider = open && displayMode === "always" ? dialogLead : badgeSelection?.provider;
4382
+ const dialogModel = open && displayMode === "always" ? dialogLead === current ? selection?.model : void 0 : badgeSelection?.model;
4383
+ const expanded = expandedDisplays(displays, dialogProvider);
4024
4384
  const title = translate("usageBadgeTitle");
4025
4385
  const toggle = () => {
4026
4386
  const next = !open;
4387
+ setDialogLead(collapsed[0]?.provider);
4027
4388
  setOpen(next);
4028
4389
  if (next) refresh();
4029
4390
  };
@@ -4095,10 +4456,12 @@ function SubscriptionUsageBadge({ rpc, currentModel, t }) {
4095
4456
  })
4096
4457
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccountWindows, {
4097
4458
  windows: account.windows,
4098
- model: d.provider === badgeSelection?.provider ? badgeSelection.model : void 0,
4459
+ observedAt: account.observedAt,
4460
+ stale: account.stale,
4461
+ model: d.provider === dialogProvider ? dialogModel : void 0,
4099
4462
  provider: d.provider,
4100
4463
  translate
4101
- }, `${d.provider}:${badgeSelection?.model ?? ""}`)]
4464
+ }, `${d.provider}:${dialogModel ?? ""}`)]
4102
4465
  }, account.key))]
4103
4466
  }, d.provider))
4104
4467
  ]
@@ -4137,13 +4500,16 @@ function AccountMeta({ account, translate }) {
4137
4500
  });
4138
4501
  }
4139
4502
  /** Preview each account independently; all remaining quotas stay accessible. */
4140
- function AccountWindows({ windows, model, provider, translate }) {
4503
+ function AccountWindows({ windows, model, provider, translate, observedAt, stale }) {
4141
4504
  const { shown, hidden } = previewWindows(windows, model, provider);
4142
4505
  const rows = (items) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dl", {
4143
4506
  style: styles.details,
4144
4507
  children: items.map((w, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WindowRow, {
4145
4508
  label: `${usageWindowLabel(translate, w)}${model !== void 0 && w.scope === model ? ` · ${translate("usageBadgeCurrent")}` : ""}`,
4146
- window: w
4509
+ window: w,
4510
+ t: translate,
4511
+ observedAt,
4512
+ stale
4147
4513
  }, i))
4148
4514
  });
4149
4515
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [rows(shown), hidden.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
@@ -4155,8 +4521,8 @@ function AccountWindows({ windows, model, provider, translate }) {
4155
4521
  })] });
4156
4522
  }
4157
4523
  /** One `dt`/`dd` pair: window name → `25% · 6d1h`, with the bar underneath. */
4158
- function WindowRow({ label, window: w }) {
4159
- const percent = usedPercent(w);
4524
+ function WindowRow({ label, window: w, t, observedAt, stale }) {
4525
+ const percent = displayUsedPercent(w.usedPercent);
4160
4526
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
4161
4527
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", {
4162
4528
  style: styles.dt,
@@ -4164,23 +4530,17 @@ function WindowRow({ label, window: w }) {
4164
4530
  }),
4165
4531
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dd", {
4166
4532
  style: styles.dd,
4167
- children: [
4168
- percent,
4169
- "%",
4170
- w.resetsAt !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4171
- style: styles.reset,
4172
- children: [" · ", windowLabel(w)]
4173
- })
4174
- ]
4533
+ children: [percent === void 0 ? t("usageMeterInvalid") : `${percent}%`, w.resetsAt !== void 0 && Number.isFinite(w.resetsAt) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4534
+ style: styles.reset,
4535
+ children: [" · ", windowLabel(w)]
4536
+ })]
4175
4537
  }),
4176
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4538
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageMeter, {
4539
+ window: w,
4540
+ t,
4177
4541
  style: styles.bar,
4178
- "aria-hidden": true,
4179
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { style: {
4180
- ...styles.barFill,
4181
- width: `${percent}%`,
4182
- background: usageBarColor(percent)
4183
- } })
4542
+ observedAt,
4543
+ stale
4184
4544
  })
4185
4545
  ] });
4186
4546
  }
@@ -4232,6 +4592,7 @@ const styles = {
4232
4592
  zIndex: 1100,
4233
4593
  boxSizing: "border-box",
4234
4594
  background: "var(--dsw-specific-menu)",
4595
+ backdropFilter: "var(--dsw-menu-backdrop-filter)",
4235
4596
  width: "max-content",
4236
4597
  minWidth: "min(300px, 100vw - 24px)",
4237
4598
  maxWidth: "min(440px, 100vw - 24px)",
@@ -4339,10 +4700,6 @@ const styles = {
4339
4700
  overflow: "hidden",
4340
4701
  background: "var(--dsw-alias-border-l2)",
4341
4702
  marginBottom: 2
4342
- },
4343
- barFill: {
4344
- height: "100%",
4345
- borderRadius: 2
4346
4703
  }
4347
4704
  };
4348
4705