dsh-subscriptions 0.1.0 → 0.1.2

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 (38) hide show
  1. package/README.md +99 -6
  2. package/icon.svg +28 -0
  3. package/lib/client/CursorCard.js +15 -11
  4. package/lib/client/ExternalUsageCards.js +11 -11
  5. package/lib/client/SubscriptionUsageBadge.d.ts +6 -2
  6. package/lib/client/SubscriptionUsageBadge.js +56 -24
  7. package/lib/client/SubscriptionsSection.d.ts +7 -2
  8. package/lib/client/SubscriptionsSection.js +9 -13
  9. package/lib/client/UsageBadgeDisplaySetting.js +18 -1
  10. package/lib/client/UsageMeter.d.ts +8 -0
  11. package/lib/client/UsageMeter.js +51 -0
  12. package/lib/client/host-contract.d.ts +1 -1
  13. package/lib/client/host-contract.js +1 -0
  14. package/lib/client/locales.d.ts +48 -0
  15. package/lib/client/locales.js +48 -0
  16. package/lib/client/subscription-card-styles.js +0 -5
  17. package/lib/client/usage-color-preferences.d.ts +6 -0
  18. package/lib/client/usage-color-preferences.js +38 -0
  19. package/lib/client/usage-pace.d.ts +29 -0
  20. package/lib/client/usage-pace.js +58 -0
  21. package/lib/client.js +396 -114
  22. package/lib/client.js.map +1 -1
  23. package/lib/index.js +225 -35
  24. package/lib/providers/claude.js +7 -0
  25. package/lib/providers/codex.js +5 -0
  26. package/lib/providers/common.d.ts +10 -0
  27. package/lib/providers/cursor-usage.js +6 -2
  28. package/lib/providers/external-usage-controller.d.ts +1 -1
  29. package/lib/providers/external-usage-controller.js +8 -3
  30. package/lib/providers/external-usage.d.ts +1 -1
  31. package/lib/providers/external-usage.js +62 -2
  32. package/lib/providers/grok.js +3 -1
  33. package/lib/providers/minimax-usage.d.ts +3 -0
  34. package/lib/providers/minimax-usage.js +55 -0
  35. package/lib/providers/pool-usage.js +6 -4
  36. package/locale/en.json +6 -0
  37. package/locale/zh.json +6 -0
  38. package/package.json +10 -6
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",
@@ -261,6 +285,30 @@ const zh = {
261
285
  usageMonthly: "每月",
262
286
  usageWindow: "窗口",
263
287
  usageResets: "{date} 重置",
288
+ usageMeterUsed: "已用 {used}% · 剩余 {remaining}%",
289
+ usageMeterPace: "时间已过 {elapsed}%(竖线标记)",
290
+ usageMeterPaceAhead: "领先时间进度 {points} 个百分点",
291
+ usageMeterPaceBehind: "落后时间进度 {points} 个百分点",
292
+ usageMeterPaceEven: "与时间进度一致",
293
+ usageMeterInvalid: "额度数据不可用",
294
+ usageMeterStale: "读数已陈旧或不可用,请刷新确认",
295
+ usageMeterNearLimit: "额度接近耗尽",
296
+ usageMeterAhead: "用量领先时间进度",
297
+ usageMeterNoWarning: "未触发额度警告",
298
+ usageMeterUnknownTime: "时间进度未知",
299
+ usageMeterReset: "{duration}后重置",
300
+ usageUnitDay: "{count} 天",
301
+ usageUnitDays: "{count} 天",
302
+ usageUnitHour: "{count} 小时",
303
+ usageUnitHours: "{count} 小时",
304
+ usageUnitMinute: "{count} 分钟",
305
+ usageUnitMinutes: "{count} 分钟",
306
+ usageColorLabel: "用量配色",
307
+ usageColorStandard: "标准(默认 · 领先 10 个百分点)",
308
+ usageColorRelaxed: "宽松(领先 15 个百分点)",
309
+ usageColorRemaining: "仅剩余额度(不显示黄色)",
310
+ usageColorPaceHint: "用量领先时间进度 {allowance} 个百分点时变黄;已用达到 90% 时优先变红。自动保存到当前浏览器,仅改变配色。",
311
+ usageColorRemainingHint: "已用达到 90% 时变红,不显示黄色节奏警告。可靠的时间标记仍保留,自动保存到当前浏览器。",
264
312
  usagePlan: "计划:{plan}",
265
313
  externalUsageTitle: "内置供应商用量",
266
314
  externalUsageConnected: "已配置 API 密钥",
@@ -1342,17 +1390,6 @@ const subscriptionCardStyles = {
1342
1390
  fontSize: 12,
1343
1391
  lineHeight: "18px",
1344
1392
  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
1393
  }
1357
1394
  };
1358
1395
 
@@ -1394,30 +1431,204 @@ function useUsageBadgeMode() {
1394
1431
  return (0, react.useSyncExternalStore)(subscribeUsageBadgeMode, readUsageBadgeMode, () => "recent");
1395
1432
  }
1396
1433
 
1434
+ //#endregion
1435
+ //#region src/client/usage-pace.ts
1436
+ const USAGE_FRESHNESS_MS = 5 * 6e4;
1437
+ function validUsage(used) {
1438
+ return Number.isFinite(used) && used >= 0 && used <= 100;
1439
+ }
1440
+ /**
1441
+ * Rounded share for display, or undefined when the provider's number is
1442
+ * unusable. Callers must show the unavailable state rather than clamp an
1443
+ * out-of-range reading into 0% or 100%.
1444
+ */
1445
+ function displayUsedPercent(used) {
1446
+ return validUsage(used) ? Math.round(used) : void 0;
1447
+ }
1448
+ /**
1449
+ * Reset countdown as the two largest nonzero whole units, rounded up so a
1450
+ * window never announces zero remaining time. Always at least one minute.
1451
+ */
1452
+ function resetCountdownParts(ms) {
1453
+ const total = Math.max(1, Math.ceil(ms / 6e4));
1454
+ return [
1455
+ {
1456
+ unit: "day",
1457
+ count: Math.floor(total / 1440)
1458
+ },
1459
+ {
1460
+ unit: "hour",
1461
+ count: Math.floor(total % 1440 / 60)
1462
+ },
1463
+ {
1464
+ unit: "minute",
1465
+ count: total % 60
1466
+ }
1467
+ ].filter((part) => part.count > 0).slice(0, 2);
1468
+ }
1469
+ function isUsageFresh(window$1, observation, now = Date.now()) {
1470
+ 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);
1471
+ }
1472
+ /** Only explicit intervals or provider-verified fixed durations support linear pace. */
1473
+ function elapsedPercent(window$1, now = Date.now()) {
1474
+ const end = window$1.resetsAt;
1475
+ if (end === void 0 || !Number.isFinite(end) || end <= now) return void 0;
1476
+ const duration = window$1.windowDurationMs;
1477
+ const start = window$1.startsAt ?? (window$1.fixedWindow === true && duration !== void 0 && Number.isFinite(duration) && duration > 0 ? end - duration : void 0);
1478
+ if (start === void 0 || !Number.isFinite(start) || start >= end || start > now) return void 0;
1479
+ return Math.min(100, Math.max(0, (now - start) / (end - start) * 100));
1480
+ }
1481
+ function usageColorState(used, elapsed, preset, fresh) {
1482
+ if (!validUsage(used)) return "neutral";
1483
+ if (used >= 90) return "red";
1484
+ if (!fresh || preset === "remaining" || elapsed === void 0) return "green";
1485
+ return used - elapsed >= (preset === "relaxed" ? 15 : 10) ? "yellow" : "green";
1486
+ }
1487
+ function usageBarColor(used, elapsed, preset = "standard", fresh = true) {
1488
+ const state = usageColorState(used, elapsed, preset, fresh);
1489
+ return state === "red" ? "var(--dsw-alias-state-error-primary)" : state === "yellow" ? "var(--dsw-alias-state-warn-label)" : "var(--dsw-alias-state-success-primary)";
1490
+ }
1491
+
1492
+ //#endregion
1493
+ //#region src/client/usage-color-preferences.ts
1494
+ const KEY = "dsh.subscriptions.usageColorPreset";
1495
+ const EVENT = "dsh:subscriptions:usage-color-preset";
1496
+ function isUsageColorPreset(value) {
1497
+ return value === "standard" || value === "relaxed" || value === "remaining";
1498
+ }
1499
+ function readUsageColorPreset() {
1500
+ if (typeof window === "undefined") return "standard";
1501
+ try {
1502
+ const value = window.localStorage.getItem(KEY);
1503
+ return isUsageColorPreset(value) ? value : "standard";
1504
+ } catch (error) {
1505
+ if (!(error instanceof DOMException) || error.name !== "SecurityError") throw error;
1506
+ return "standard";
1507
+ }
1508
+ }
1509
+ function setUsageColorPreset(value) {
1510
+ if (!isUsageColorPreset(value)) throw new TypeError("Invalid usage coloring preset");
1511
+ window.localStorage.setItem(KEY, value);
1512
+ window.dispatchEvent(new Event(EVENT));
1513
+ }
1514
+ function subscribeUsageColorPreset(notify) {
1515
+ const storage = (event) => {
1516
+ if (event.key === KEY || event.key === null) notify();
1517
+ };
1518
+ window.addEventListener(EVENT, notify);
1519
+ window.addEventListener("storage", storage);
1520
+ return () => {
1521
+ window.removeEventListener(EVENT, notify);
1522
+ window.removeEventListener("storage", storage);
1523
+ };
1524
+ }
1525
+ function useUsageColorPreset() {
1526
+ return (0, react.useSyncExternalStore)(subscribeUsageColorPreset, readUsageColorPreset, () => "standard");
1527
+ }
1528
+
1529
+ //#endregion
1530
+ //#region src/client/UsageMeter.tsx
1531
+ /** Singular/plural locale keys per countdown unit (zh maps both to one string). */
1532
+ const UNIT_KEYS = {
1533
+ day: ["usageUnitDay", "usageUnitDays"],
1534
+ hour: ["usageUnitHour", "usageUnitHours"],
1535
+ minute: ["usageUnitMinute", "usageUnitMinutes"]
1536
+ };
1537
+ function UsageMeter({ window: quota, t, style, observedAt, stale }) {
1538
+ const preset = useUsageColorPreset();
1539
+ const [now, setNow] = (0, react.useState)(Date.now);
1540
+ (0, react.useEffect)(() => {
1541
+ const timer = setInterval(() => setNow(Date.now()), 6e4);
1542
+ return () => clearInterval(timer);
1543
+ }, []);
1544
+ const current = Math.max(now, Date.now());
1545
+ const valid = validUsage(quota.usedPercent);
1546
+ const used = valid ? quota.usedPercent : 0;
1547
+ const fresh = isUsageFresh(quota, {
1548
+ observedAt,
1549
+ stale
1550
+ }, current);
1551
+ const elapsed = fresh ? elapsedPercent(quota, current) : void 0;
1552
+ const state = usageColorState(quota.usedPercent, elapsed, preset, fresh);
1553
+ const lead = elapsed === void 0 ? 0 : Math.round(used - elapsed);
1554
+ const label = [
1555
+ valid ? t("usageMeterUsed", {
1556
+ used: Math.round(used),
1557
+ remaining: Math.round(100 - used)
1558
+ }) : t("usageMeterInvalid"),
1559
+ !valid ? "" : !fresh ? t("usageMeterStale") : state === "red" ? t("usageMeterNearLimit") : elapsed === void 0 && preset !== "remaining" ? t("usageMeterUnknownTime") : state === "yellow" ? t("usageMeterAhead") : t("usageMeterNoWarning"),
1560
+ elapsed === void 0 ? "" : t("usageMeterPace", { elapsed: Math.round(elapsed) }),
1561
+ elapsed === void 0 ? "" : lead > 0 ? t("usageMeterPaceAhead", { points: lead }) : lead < 0 ? t("usageMeterPaceBehind", { points: -lead }) : t("usageMeterPaceEven"),
1562
+ 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(" ") }) : ""
1563
+ ].filter(Boolean).join(" · ");
1564
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1565
+ role: "img",
1566
+ "aria-label": label,
1567
+ title: label,
1568
+ "data-usage-color": state,
1569
+ style: {
1570
+ height: 6,
1571
+ borderRadius: 3,
1572
+ overflow: "hidden",
1573
+ background: "var(--dsw-alias-bg-layer-1)",
1574
+ border: "1px solid var(--dsw-alias-border-l2)",
1575
+ ...style,
1576
+ position: "relative"
1577
+ },
1578
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { style: {
1579
+ height: "100%",
1580
+ borderRadius: "inherit",
1581
+ width: `${used}%`,
1582
+ background: usageBarColor(quota.usedPercent, elapsed, preset, fresh)
1583
+ } }), elapsed !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1584
+ "aria-hidden": "true",
1585
+ "data-usage-time-marker": true,
1586
+ style: {
1587
+ position: "absolute",
1588
+ left: `clamp(0px, calc(${elapsed}% - 1px), calc(100% - 2px))`,
1589
+ top: 0,
1590
+ bottom: 0,
1591
+ width: 2,
1592
+ background: "var(--dsw-alias-label-primary)",
1593
+ boxShadow: "0 0 0 1px var(--dsw-alias-bg-layer-1)"
1594
+ }
1595
+ })]
1596
+ });
1597
+ }
1598
+
1397
1599
  //#endregion
1398
1600
  //#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
- }];
1601
+ const SOURCES = [
1602
+ {
1603
+ id: "opencode-go",
1604
+ name: "OpenCode Go",
1605
+ ref: "OPENCODE_GO_API_KEY"
1606
+ },
1607
+ {
1608
+ id: "kimi-code",
1609
+ name: "Kimi Code",
1610
+ ref: "KIMI_CODING_API_KEY"
1611
+ },
1612
+ {
1613
+ id: "minimax",
1614
+ name: "MiniMax",
1615
+ ref: "MINIMAX_API_KEY"
1616
+ },
1617
+ {
1618
+ id: "minimax-cn",
1619
+ name: "MiniMax CN",
1620
+ ref: "MINIMAX_CN_API_KEY"
1621
+ }
1622
+ ];
1408
1623
  function errorText(error) {
1409
1624
  return error instanceof Error ? error.message : String(error);
1410
1625
  }
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)";
1415
- }
1416
1626
  /** Quota cards for API-key providers already available through DSH itself. */
1417
1627
  function ExternalUsageCards({ rpc, t }) {
1418
1628
  const [status, setStatus] = (0, react.useState)();
1419
1629
  const [statusError, setStatusError] = (0, react.useState)();
1420
1630
  const [usage, setUsage] = (0, react.useState)({});
1631
+ const [observedAt, setObservedAt] = (0, react.useState)({});
1421
1632
  const [errors, setErrors] = (0, react.useState)({});
1422
1633
  const [loading, setLoading] = (0, react.useState)({});
1423
1634
  const refresh = (0, react.useCallback)(async (source) => {
@@ -1425,16 +1636,20 @@ function ExternalUsageCards({ rpc, t }) {
1425
1636
  ...prev,
1426
1637
  [source]: true
1427
1638
  }));
1428
- setErrors((prev) => ({
1429
- ...prev,
1430
- [source]: void 0
1431
- }));
1432
1639
  try {
1433
1640
  const value = await callSubscriptionsAuth(rpc, "externalUsage", { source });
1434
1641
  setUsage((prev) => ({
1435
1642
  ...prev,
1436
1643
  [source]: value
1437
1644
  }));
1645
+ setObservedAt((prev) => ({
1646
+ ...prev,
1647
+ [source]: value.observedAt ?? Date.now()
1648
+ }));
1649
+ setErrors((prev) => ({
1650
+ ...prev,
1651
+ [source]: void 0
1652
+ }));
1438
1653
  window.dispatchEvent(new Event(USAGE_BADGE_REFRESH_EVENT));
1439
1654
  } catch (error) {
1440
1655
  setErrors((prev) => ({
@@ -1539,24 +1754,18 @@ function ExternalUsageCards({ rpc, t }) {
1539
1754
  children: t("usageEmpty")
1540
1755
  }),
1541
1756
  snapshot?.windows?.map((window$1, index) => {
1542
- const percent = Math.min(100, Math.max(0, window$1.usedPercent));
1757
+ const percent = displayUsedPercent(window$1.usedPercent);
1543
1758
  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");
1544
1759
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1545
1760
  style: subscriptionCardStyles.usageRow,
1546
1761
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1547
1762
  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
- } })
1763
+ 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() })}`] })]
1764
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageMeter, {
1765
+ window: window$1,
1766
+ t,
1767
+ observedAt: observedAt[id],
1768
+ stale: errors[id] !== void 0 || snapshot?.stale === true
1560
1769
  })]
1561
1770
  }, index);
1562
1771
  })
@@ -1584,15 +1793,12 @@ function ExternalUsageCards({ rpc, t }) {
1584
1793
  function message(error) {
1585
1794
  return error instanceof Error ? error.message : String(error);
1586
1795
  }
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
1796
  /** Cursor's single connected account, rendered with the same card and Manage dialog as the other providers. */
1593
1797
  function CursorCard({ rpc, t }) {
1594
1798
  const [status, setStatus] = (0, react.useState)();
1595
1799
  const [usage, setUsage] = (0, react.useState)();
1800
+ const [observedAt, setObservedAt] = (0, react.useState)();
1801
+ const [usageStale, setUsageStale] = (0, react.useState)(false);
1596
1802
  const [authorizeUrl, setAuthorizeUrl] = (0, react.useState)();
1597
1803
  const [error, setError] = (0, react.useState)();
1598
1804
  const [loading, setLoading] = (0, react.useState)(false);
@@ -1615,11 +1821,15 @@ function CursorCard({ rpc, t }) {
1615
1821
  const refreshUsage = (0, react.useCallback)(async () => {
1616
1822
  setLoading(true);
1617
1823
  try {
1618
- setUsage(await callSubscriptionsAuth(rpc, "cursorUsage", {}));
1824
+ const value = await callSubscriptionsAuth(rpc, "cursorUsage", {});
1825
+ setUsage(value);
1826
+ setObservedAt(value.observedAt ?? Date.now());
1827
+ setUsageStale(value.stale === true);
1619
1828
  setError(void 0);
1620
1829
  window.dispatchEvent(new Event(USAGE_BADGE_REFRESH_EVENT));
1621
1830
  } catch (failure) {
1622
1831
  setError(message(failure));
1832
+ setUsageStale(true);
1623
1833
  } finally {
1624
1834
  setLoading(false);
1625
1835
  }
@@ -1665,6 +1875,8 @@ function CursorCard({ rpc, t }) {
1665
1875
  try {
1666
1876
  await callSubscriptionsAuth(rpc, "cursorLogout", {});
1667
1877
  setUsage(void 0);
1878
+ setObservedAt(void 0);
1879
+ setUsageStale(false);
1668
1880
  await refreshStatus();
1669
1881
  } catch (failure) {
1670
1882
  setError(message(failure));
@@ -1757,25 +1969,18 @@ function CursorCard({ rpc, t }) {
1757
1969
  children: t("usageEmpty")
1758
1970
  }),
1759
1971
  usage?.windows?.map((window$1, index) => {
1760
- const percent = Math.min(100, Math.max(0, window$1.usedPercent));
1972
+ const percent = displayUsedPercent(window$1.usedPercent);
1761
1973
  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
1974
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1763
1975
  style: subscriptionCardStyles.usageRow,
1764
1976
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1765
1977
  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
- } })
1978
+ 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() })}`] })]
1979
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageMeter, {
1980
+ window: window$1,
1981
+ t,
1982
+ observedAt,
1983
+ stale: usageStale
1779
1984
  })]
1780
1985
  }, index);
1781
1986
  })
@@ -1832,6 +2037,7 @@ function CursorCard({ rpc, t }) {
1832
2037
  /** Browser-local display preference; never changes provider or account settings. */
1833
2038
  function UsageBadgeDisplaySetting({ t }) {
1834
2039
  const mode = useUsageBadgeMode();
2040
+ const preset = useUsageColorPreset();
1835
2041
  const [failed, setFailed] = (0, react.useState)(false);
1836
2042
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1837
2043
  style: styles$6.card,
@@ -1866,6 +2072,43 @@ function UsageBadgeDisplaySetting({ t }) {
1866
2072
  style: styles$6.hint,
1867
2073
  children: t("usageBadgeDisplayHint")
1868
2074
  }),
2075
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2076
+ style: styles$6.field,
2077
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("usageColorLabel") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
2078
+ style: styles$6.select,
2079
+ "aria-label": t("usageColorLabel"),
2080
+ value: preset,
2081
+ onChange: (event) => {
2082
+ const value = event.currentTarget.value;
2083
+ if (!isUsageColorPreset(value)) return;
2084
+ try {
2085
+ setUsageColorPreset(value);
2086
+ setFailed(false);
2087
+ } catch (error) {
2088
+ if (!(error instanceof DOMException) || !["SecurityError", "QuotaExceededError"].includes(error.name)) throw error;
2089
+ setFailed(true);
2090
+ }
2091
+ },
2092
+ children: [
2093
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2094
+ value: "standard",
2095
+ children: t("usageColorStandard")
2096
+ }),
2097
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2098
+ value: "relaxed",
2099
+ children: t("usageColorRelaxed")
2100
+ }),
2101
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2102
+ value: "remaining",
2103
+ children: t("usageColorRemaining")
2104
+ })
2105
+ ]
2106
+ })]
2107
+ }),
2108
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2109
+ style: styles$6.hint,
2110
+ children: t(preset === "remaining" ? "usageColorRemainingHint" : "usageColorPaceHint", { allowance: preset === "relaxed" ? 15 : 10 })
2111
+ }),
1869
2112
  failed && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1870
2113
  role: "alert",
1871
2114
  style: {
@@ -2017,8 +2260,6 @@ const styles$5 = {
2017
2260
  cursor: "pointer",
2018
2261
  color: "var(--dsw-alias-state-warn-label)"
2019
2262
  },
2020
- usageTrack: subscriptionCardStyles.usageTrack,
2021
- usageFill: subscriptionCardStyles.usageFill,
2022
2263
  deviceCode: {
2023
2264
  marginTop: 4,
2024
2265
  display: "flex",
@@ -2114,12 +2355,6 @@ function usageWindowLabel$1(t, window$1) {
2114
2355
  const base = window$1.kind === "session" ? t("usageSession") : window$1.kind === "weekly" ? t("usageWeekly") : t("usageWindow");
2115
2356
  return window$1.scope !== void 0 && window$1.scope !== "" ? `${base} · ${window$1.scope}` : base;
2116
2357
  }
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
2358
  /**
2124
2359
  * The Subscriptions settings page component.
2125
2360
  * @param props - the slot inject face ({@link SubscriptionsSectionInjected}).
@@ -2142,6 +2377,7 @@ function SubscriptionsSection(props) {
2142
2377
  const [copiedCode, setCopiedCode] = (0, react.useState)(void 0);
2143
2378
  /** Usage snapshots keyed `${provider}:${accountKey}` — every account tracks its own windows. */
2144
2379
  const [usages, setUsages] = (0, react.useState)({});
2380
+ const [observedAt, setObservedAt] = (0, react.useState)({});
2145
2381
  const [usageErrors, setUsageErrors] = (0, react.useState)({});
2146
2382
  const [usageLoading, setUsageLoading] = (0, react.useState)({});
2147
2383
  const mountedRef = (0, react.useRef)(true);
@@ -2242,6 +2478,10 @@ function SubscriptionsSection(props) {
2242
2478
  ...prev,
2243
2479
  [key]: usage
2244
2480
  }));
2481
+ setObservedAt((prev) => ({
2482
+ ...prev,
2483
+ [key]: usage.observedAt ?? Date.now()
2484
+ }));
2245
2485
  window.dispatchEvent(new Event(USAGE_BADGE_REFRESH_EVENT));
2246
2486
  setUsageErrors((prev) => {
2247
2487
  const next = { ...prev };
@@ -2542,19 +2782,17 @@ function SubscriptionsSection(props) {
2542
2782
  children: t("usageEmpty")
2543
2783
  }),
2544
2784
  (usage?.windows ?? []).map((window$1, index) => {
2545
- const percent = Math.min(100, Math.max(0, window$1.usedPercent));
2785
+ const percent = displayUsedPercent(window$1.usedPercent);
2546
2786
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2547
2787
  style: styles$5.usageRow,
2548
2788
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2549
2789
  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
- } })
2790
+ 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() })}`] })]
2791
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageMeter, {
2792
+ window: window$1,
2793
+ t,
2794
+ observedAt: observedAt[usageKey],
2795
+ stale: usageError !== void 0 || usage.stale === true
2558
2796
  })]
2559
2797
  }, index);
2560
2798
  })
@@ -2989,6 +3227,7 @@ const HOST_CONTRACT = {
2989
3227
  "--dsw-alias-state-success-primary",
2990
3228
  "--dsw-alias-state-warn-label",
2991
3229
  "--dsw-elevation-prominent",
3230
+ "--dsw-menu-backdrop-filter",
2992
3231
  "--dsw-specific-menu"
2993
3232
  ]
2994
3233
  }
@@ -3670,7 +3909,9 @@ const PROVIDER_NAMES = {
3670
3909
  antigravity: "Antigravity",
3671
3910
  "cursor-subscription": "Cursor",
3672
3911
  "opencode-go": "OpenCode Go",
3673
- "kimi-coding": "Kimi Code"
3912
+ "kimi-coding": "Kimi Code",
3913
+ "minimax": "MiniMax",
3914
+ "minimax-cn": "MiniMax CN"
3674
3915
  };
3675
3916
  /** Key of one account's cached windows: `provider:accountKey`. */
3676
3917
  function usageKeyOf(provider, account) {
@@ -3742,7 +3983,12 @@ async function loadBadgeRoster(rpc) {
3742
3983
  }
3743
3984
  });
3744
3985
  }
3745
- if (external.status === "fulfilled") for (const [source, provider] of [["opencode-go", "opencode-go"], ["kimi-code", "kimi-coding"]]) {
3986
+ if (external.status === "fulfilled") for (const [source, provider] of [
3987
+ ["opencode-go", "opencode-go"],
3988
+ ["kimi-code", "kimi-coding"],
3989
+ ["minimax", "minimax"],
3990
+ ["minimax-cn", "minimax-cn"]
3991
+ ]) {
3746
3992
  refreshed.add(provider);
3747
3993
  if (external.value[source]?.configured) roster.push({
3748
3994
  provider,
@@ -3759,7 +4005,7 @@ async function loadBadgeRoster(rpc) {
3759
4005
  }
3760
4006
  async function usageOf(rpc, { provider, account }) {
3761
4007
  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 });
4008
+ if (provider === "opencode-go" || provider === "kimi-coding" || provider === "minimax" || provider === "minimax-cn") return callSubscriptionsAuth(rpc, "externalUsage", { source: provider === "kimi-coding" ? "kimi-code" : provider });
3763
4009
  return callSubscriptionsAuth(rpc, "usage", {
3764
4010
  provider,
3765
4011
  account: account.key
@@ -3786,7 +4032,7 @@ function createCurrentModelReader(models, sessionId) {
3786
4032
  * Falls back to the scope/kind abbreviation when no reset time is known.
3787
4033
  */
3788
4034
  function windowLabel(w) {
3789
- if (w.resetsAt === void 0) {
4035
+ if (w.resetsAt === void 0 || !Number.isFinite(w.resetsAt)) {
3790
4036
  if (w.scope !== void 0 && w.scope !== "") return w.scope;
3791
4037
  switch (w.kind) {
3792
4038
  case "session": return "5h";
@@ -3802,9 +4048,13 @@ function windowLabel(w) {
3802
4048
  if (hours > 0) return `${hours}h${minutes % 60}m`;
3803
4049
  return `${Math.max(1, minutes)}m`;
3804
4050
  }
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)));
4051
+ /**
4052
+ * One compact segment piece (`6d1h 25%`); an unusable provider percentage
4053
+ * renders as the no-data dash, consistent with the meter's unavailable state.
4054
+ */
4055
+ function windowSegment(label, w) {
4056
+ const percent = displayUsedPercent(w.usedPercent);
4057
+ return percent === void 0 ? `${label} —` : `${label} ${percent}%`;
3808
4058
  }
3809
4059
  /** Keep model quotas separate: matching percentages do not imply a shared pool. */
3810
4060
  function prioritizeWindows(windows, model) {
@@ -3826,10 +4076,10 @@ function compactSegment(d, model, t = fallbackTranslate) {
3826
4076
  if (d.provider === "antigravity") {
3827
4077
  const matching = model === void 0 ? [] : windows.filter((w) => w.scope === model);
3828
4078
  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)}%`);
4079
+ const parts$1 = matching.slice(0, 2).map((w) => windowSegment(w.kind === "weekly" ? t("usageWeekly") : t("usageWindow"), w));
3830
4080
  return `${d.name} ${parts$1.join(" · ")}`;
3831
4081
  }
3832
- const parts = windows.slice(0, 2).map((w) => `${windowLabel(w)} ${usedPercent(w)}%`);
4082
+ const parts = windows.slice(0, 2).map((w) => windowSegment(windowLabel(w), w));
3833
4083
  if (windows.length > 2) parts.push(`+${windows.length - 2}`);
3834
4084
  return `${d.name} ${parts.join(" · ")}`;
3835
4085
  }
@@ -3893,6 +4143,7 @@ function SubscriptionUsageBadge({ rpc, currentModel, t }) {
3893
4143
  const currentRef = (0, react.useRef)(currentModel);
3894
4144
  currentRef.current = currentModel;
3895
4145
  const lastKnownRef = (0, react.useRef)(/* @__PURE__ */ new Map());
4146
+ const observationsRef = (0, react.useRef)(/* @__PURE__ */ new Map());
3896
4147
  const refresh = (0, react.useCallback)(async () => {
3897
4148
  if (rpc === void 0 || inflightRef.current) return;
3898
4149
  inflightRef.current = true;
@@ -3901,7 +4152,10 @@ function SubscriptionUsageBadge({ rpc, currentModel, t }) {
3901
4152
  if (!mountedRef.current) return;
3902
4153
  const lastKnown = lastKnownRef.current;
3903
4154
  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);
4155
+ for (const key of lastKnown.keys()) if (refreshed.has(key.split(":", 1)[0]) && !live.has(key)) {
4156
+ lastKnown.delete(key);
4157
+ observationsRef.current.delete(key);
4158
+ }
3905
4159
  const results = await Promise.allSettled(roster.map(async ({ provider, account }) => {
3906
4160
  return {
3907
4161
  provider,
@@ -3913,6 +4167,13 @@ function SubscriptionUsageBadge({ rpc, currentModel, t }) {
3913
4167
  };
3914
4168
  }));
3915
4169
  if (!mountedRef.current) return;
4170
+ for (const { provider, account } of roster) {
4171
+ const key = usageKeyOf(provider, account);
4172
+ observationsRef.current.set(key, {
4173
+ ...observationsRef.current.get(key),
4174
+ stale: true
4175
+ });
4176
+ }
3916
4177
  const plans = /* @__PURE__ */ new Map();
3917
4178
  for (const r of results) {
3918
4179
  if (r.status !== "fulfilled") continue;
@@ -3921,12 +4182,37 @@ function SubscriptionUsageBadge({ rpc, currentModel, t }) {
3921
4182
  if (usage.plan !== void 0) plans.set(key, usage.plan);
3922
4183
  if (!usage.supported || !usage.windows || usage.windows.length === 0) {
3923
4184
  lastKnown.delete(key);
4185
+ observationsRef.current.delete(key);
3924
4186
  continue;
3925
4187
  }
3926
4188
  lastKnown.set(key, usage.windows);
4189
+ observationsRef.current.set(key, {
4190
+ observedAt: usage.observedAt ?? Date.now(),
4191
+ stale: usage.stale === true
4192
+ });
3927
4193
  }
3928
- setDisplays((previous) => [...groupUsageDisplays(roster, lastKnown, plans), ...previous.filter((display) => !refreshed.has(display.provider))]);
3929
- } catch {} finally {
4194
+ setDisplays((previous) => [...groupUsageDisplays(roster, lastKnown, plans).map((display) => ({
4195
+ ...display,
4196
+ accounts: display.accounts.map((account) => ({
4197
+ ...account,
4198
+ ...observationsRef.current.get(`${display.provider}:${account.key}`)
4199
+ }))
4200
+ })), ...previous.filter((display) => !refreshed.has(display.provider)).map((display) => ({
4201
+ ...display,
4202
+ accounts: display.accounts.map((account) => ({
4203
+ ...account,
4204
+ stale: true
4205
+ }))
4206
+ }))]);
4207
+ } catch {
4208
+ if (mountedRef.current) setDisplays((previous) => previous.map((display) => ({
4209
+ ...display,
4210
+ accounts: display.accounts.map((account) => ({
4211
+ ...account,
4212
+ stale: true
4213
+ }))
4214
+ })));
4215
+ } finally {
3930
4216
  inflightRef.current = false;
3931
4217
  }
3932
4218
  }, [rpc]);
@@ -4095,6 +4381,8 @@ function SubscriptionUsageBadge({ rpc, currentModel, t }) {
4095
4381
  })
4096
4382
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccountWindows, {
4097
4383
  windows: account.windows,
4384
+ observedAt: account.observedAt,
4385
+ stale: account.stale,
4098
4386
  model: d.provider === badgeSelection?.provider ? badgeSelection.model : void 0,
4099
4387
  provider: d.provider,
4100
4388
  translate
@@ -4137,13 +4425,16 @@ function AccountMeta({ account, translate }) {
4137
4425
  });
4138
4426
  }
4139
4427
  /** Preview each account independently; all remaining quotas stay accessible. */
4140
- function AccountWindows({ windows, model, provider, translate }) {
4428
+ function AccountWindows({ windows, model, provider, translate, observedAt, stale }) {
4141
4429
  const { shown, hidden } = previewWindows(windows, model, provider);
4142
4430
  const rows = (items) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dl", {
4143
4431
  style: styles.details,
4144
4432
  children: items.map((w, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WindowRow, {
4145
4433
  label: `${usageWindowLabel(translate, w)}${model !== void 0 && w.scope === model ? ` · ${translate("usageBadgeCurrent")}` : ""}`,
4146
- window: w
4434
+ window: w,
4435
+ t: translate,
4436
+ observedAt,
4437
+ stale
4147
4438
  }, i))
4148
4439
  });
4149
4440
  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 +4446,8 @@ function AccountWindows({ windows, model, provider, translate }) {
4155
4446
  })] });
4156
4447
  }
4157
4448
  /** One `dt`/`dd` pair: window name → `25% · 6d1h`, with the bar underneath. */
4158
- function WindowRow({ label, window: w }) {
4159
- const percent = usedPercent(w);
4449
+ function WindowRow({ label, window: w, t, observedAt, stale }) {
4450
+ const percent = displayUsedPercent(w.usedPercent);
4160
4451
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
4161
4452
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", {
4162
4453
  style: styles.dt,
@@ -4164,23 +4455,17 @@ function WindowRow({ label, window: w }) {
4164
4455
  }),
4165
4456
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dd", {
4166
4457
  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
- ]
4458
+ children: [percent === void 0 ? t("usageMeterInvalid") : `${percent}%`, w.resetsAt !== void 0 && Number.isFinite(w.resetsAt) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4459
+ style: styles.reset,
4460
+ children: [" · ", windowLabel(w)]
4461
+ })]
4175
4462
  }),
4176
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4463
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageMeter, {
4464
+ window: w,
4465
+ t,
4177
4466
  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
- } })
4467
+ observedAt,
4468
+ stale
4184
4469
  })
4185
4470
  ] });
4186
4471
  }
@@ -4232,6 +4517,7 @@ const styles = {
4232
4517
  zIndex: 1100,
4233
4518
  boxSizing: "border-box",
4234
4519
  background: "var(--dsw-specific-menu)",
4520
+ backdropFilter: "var(--dsw-menu-backdrop-filter)",
4235
4521
  width: "max-content",
4236
4522
  minWidth: "min(300px, 100vw - 24px)",
4237
4523
  maxWidth: "min(440px, 100vw - 24px)",
@@ -4339,10 +4625,6 @@ const styles = {
4339
4625
  overflow: "hidden",
4340
4626
  background: "var(--dsw-alias-border-l2)",
4341
4627
  marginBottom: 2
4342
- },
4343
- barFill: {
4344
- height: "100%",
4345
- borderRadius: 2
4346
4628
  }
4347
4629
  };
4348
4630