dsh-subscriptions 0.1.2 → 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.
package/lib/client.js CHANGED
@@ -190,8 +190,9 @@ const en = {
190
190
  usageBadgeTitle: "Provider usage",
191
191
  usageBadgeDisplay: "Status-bar quota display",
192
192
  usageBadgeDisplayRecent: "Current / most recent provider with usage",
193
+ usageBadgeDisplayAlways: "Always show one provider, rotating through the rest",
193
194
  usageBadgeDisplayHidden: "Hidden",
194
- 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.",
195
196
  usageBadgeDisplaySaveFailed: "Could not save the display preference. Check browser storage permissions.",
196
197
  usageBadgeCurrent: "current",
197
198
  usageBadgeModelCount: "{count} model quotas",
@@ -361,8 +362,9 @@ const zh = {
361
362
  usageBadgeTitle: "服务商用量",
362
363
  usageBadgeDisplay: "状态栏额度显示",
363
364
  usageBadgeDisplayRecent: "仅显示当前/最近使用的用量服务商",
365
+ usageBadgeDisplayAlways: "始终显示一家,其余服务商轮播",
364
366
  usageBadgeDisplayHidden: "不显示",
365
- usageBadgeDisplayHint: "自动保存到当前浏览器。状态栏一次显示一家服务商;没有额度来源的模型沿用本视图最近选择的服务商,没有记录则隐藏。关闭不影响本页的额度详情。",
367
+ usageBadgeDisplayHint: "自动保存到当前浏览器。状态栏一次显示一家服务商;没有额度来源的模型沿用本视图最近选择的服务商,没有记录则隐藏。选择“始终显示”时,当前模型没有额度来源,就每 10 秒轮播一家有额度的服务商。关闭不影响本页的额度详情。",
366
368
  usageBadgeDisplaySaveFailed: "显示设置保存失败,请检查浏览器存储权限。",
367
369
  usageBadgeCurrent: "当前",
368
370
  usageBadgeModelCount: "{count} 个模型配额",
@@ -1402,7 +1404,8 @@ const USAGE_BADGE_REFRESH_EVENT = "dsh:subscriptions:usage-refresh";
1402
1404
  function readUsageBadgeMode() {
1403
1405
  if (typeof window === "undefined") return "recent";
1404
1406
  try {
1405
- 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";
1406
1409
  } catch (error) {
1407
1410
  if (!(error instanceof DOMException) || error.name !== "SecurityError") throw error;
1408
1411
  return "recent";
@@ -1410,7 +1413,7 @@ function readUsageBadgeMode() {
1410
1413
  }
1411
1414
  /** Save first, then notify; the settings control reports a rejected write. */
1412
1415
  function setUsageBadgeMode(mode) {
1413
- 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");
1414
1417
  window.localStorage.setItem(MODE_KEY, mode);
1415
1418
  window.dispatchEvent(new Event(MODE_EVENT));
1416
1419
  }
@@ -1466,10 +1469,23 @@ function resetCountdownParts(ms) {
1466
1469
  }
1467
1470
  ].filter((part) => part.count > 0).slice(0, 2);
1468
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
+ */
1469
1478
  function isUsageFresh(window$1, observation, now = Date.now()) {
1470
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);
1471
1480
  }
1472
- /** Only explicit intervals or provider-verified fixed durations support linear pace. */
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
+ */
1473
1489
  function elapsedPercent(window$1, now = Date.now()) {
1474
1490
  const end = window$1.resetsAt;
1475
1491
  if (end === void 0 || !Number.isFinite(end) || end <= now) return void 0;
@@ -1548,17 +1564,18 @@ function UsageMeter({ window: quota, t, style, observedAt, stale }) {
1548
1564
  observedAt,
1549
1565
  stale
1550
1566
  }, 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);
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);
1554
1571
  const label = [
1555
1572
  valid ? t("usageMeterUsed", {
1556
1573
  used: Math.round(used),
1557
1574
  remaining: Math.round(100 - used)
1558
1575
  }) : t("usageMeterInvalid"),
1559
- !valid ? "" : !fresh ? t("usageMeterStale") : state === "red" ? t("usageMeterNearLimit") : elapsed === void 0 && preset !== "remaining" ? t("usageMeterUnknownTime") : state === "yellow" ? t("usageMeterAhead") : t("usageMeterNoWarning"),
1576
+ !valid ? "" : !fresh ? t("usageMeterStale") : state === "red" ? t("usageMeterNearLimit") : pace === void 0 && preset !== "remaining" ? t("usageMeterUnknownTime") : state === "yellow" ? t("usageMeterAhead") : t("usageMeterNoWarning"),
1560
1577
  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"),
1578
+ pace === void 0 ? "" : lead > 0 ? t("usageMeterPaceAhead", { points: lead }) : lead < 0 ? t("usageMeterPaceBehind", { points: -lead }) : t("usageMeterPaceEven"),
1562
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(" ") }) : ""
1563
1580
  ].filter(Boolean).join(" · ");
1564
1581
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -1579,7 +1596,7 @@ function UsageMeter({ window: quota, t, style, observedAt, stale }) {
1579
1596
  height: "100%",
1580
1597
  borderRadius: "inherit",
1581
1598
  width: `${used}%`,
1582
- background: usageBarColor(quota.usedPercent, elapsed, preset, fresh)
1599
+ background: usageBarColor(quota.usedPercent, pace, preset, fresh)
1583
1600
  } }), elapsed !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1584
1601
  "aria-hidden": "true",
1585
1602
  "data-usage-time-marker": true,
@@ -1623,6 +1640,21 @@ const SOURCES = [
1623
1640
  function errorText(error) {
1624
1641
  return error instanceof Error ? error.message : String(error);
1625
1642
  }
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;
1657
+ }
1626
1658
  /** Quota cards for API-key providers already available through DSH itself. */
1627
1659
  function ExternalUsageCards({ rpc, t }) {
1628
1660
  const [status, setStatus] = (0, react.useState)();
@@ -1755,7 +1787,7 @@ function ExternalUsageCards({ rpc, t }) {
1755
1787
  }),
1756
1788
  snapshot?.windows?.map((window$1, index) => {
1757
1789
  const percent = displayUsedPercent(window$1.usedPercent);
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");
1790
+ const label = externalUsageWindowLabel(t, window$1);
1759
1791
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1760
1792
  style: subscriptionCardStyles.usageRow,
1761
1793
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -2050,7 +2082,7 @@ function UsageBadgeDisplaySetting({ t }) {
2050
2082
  value: mode,
2051
2083
  onChange: (event) => {
2052
2084
  const value = event.currentTarget.value;
2053
- if (value !== "recent" && value !== "hidden") return;
2085
+ if (value !== "recent" && value !== "always" && value !== "hidden") return;
2054
2086
  try {
2055
2087
  setUsageBadgeMode(value);
2056
2088
  setFailed(false);
@@ -2059,13 +2091,20 @@ function UsageBadgeDisplaySetting({ t }) {
2059
2091
  setFailed(true);
2060
2092
  }
2061
2093
  },
2062
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2063
- value: "recent",
2064
- children: t("usageBadgeDisplayRecent")
2065
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2066
- value: "hidden",
2067
- children: t("usageBadgeDisplayHidden")
2068
- })]
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
+ ]
2069
2108
  })]
2070
2109
  }),
2071
2110
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
@@ -3893,6 +3932,8 @@ const UsageBadgeIcon = usageBadgeIcon(__deepseek_ai_dsh_client_ui_primitives);
3893
3932
  const USAGE_POLL_INTERVAL_MS = 15 * 6e4;
3894
3933
  /** How often the badge re-reads the session's current model (model switches arrive only by asking). */
3895
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;
3896
3937
  /** Distance between the trigger's top edge and the dialog's bottom (host stat dialogs use the same). */
3897
3938
  const PANEL_GAP = 8;
3898
3939
  /** Distance kept between the dialog and each viewport edge. */
@@ -4092,6 +4133,24 @@ function collapsedDisplays(displays, current) {
4092
4133
  const match = displays.find((d) => d.provider === current);
4093
4134
  return match === void 0 ? [] : [match];
4094
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
+ }
4095
4154
  /** Order for the expanded dialog: the current provider first, the rest in poll order. */
4096
4155
  function expandedDisplays(displays, current) {
4097
4156
  const match = displays.find((d) => d.provider === current);
@@ -4125,7 +4184,7 @@ function usageWindowLabel(t, window$1) {
4125
4184
  * every provider's accounts and their windows. Returns null when no data is
4126
4185
  * available.
4127
4186
  */
4128
- function SubscriptionUsageBadge({ rpc, currentModel, t }) {
4187
+ function SubscriptionUsageBadge({ rpc, currentModel, t, rotationMs }) {
4129
4188
  const translate = t ?? fallbackTranslate;
4130
4189
  const [displays, setDisplays] = (0, react.useState)([]);
4131
4190
  const [selection, setSelection] = (0, react.useState)(void 0);
@@ -4135,6 +4194,8 @@ function SubscriptionUsageBadge({ rpc, currentModel, t }) {
4135
4194
  const badgeSelection = retainSubscriptionSelection(lastSubscription, selection);
4136
4195
  const [open, setOpen] = (0, react.useState)(false);
4137
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);
4138
4199
  const inflightRef = (0, react.useRef)(false);
4139
4200
  const mountedRef = (0, react.useRef)(true);
4140
4201
  const rootRef = (0, react.useRef)(null);
@@ -4298,18 +4359,32 @@ function SubscriptionUsageBadge({ rpc, currentModel, t }) {
4298
4359
  (0, react.useEffect)(() => {
4299
4360
  if (displayMode === "hidden") setOpen(false);
4300
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]);
4301
4372
  const seat = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4302
4373
  ref: seatRef,
4303
4374
  style: styles.seat,
4304
4375
  "aria-hidden": true
4305
4376
  });
4306
- const collapsed = collapsedDisplays(displays, badgeSelection?.provider);
4377
+ const collapsed = displayMode === "always" ? rotatingDisplay(displays, current, rotation) : collapsedDisplays(displays, badgeSelection?.provider);
4307
4378
  if (displayMode === "hidden" || collapsed.length === 0) return seat;
4308
- const label = compactSegment(collapsed[0], badgeSelection?.model, translate);
4309
- 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);
4310
4384
  const title = translate("usageBadgeTitle");
4311
4385
  const toggle = () => {
4312
4386
  const next = !open;
4387
+ setDialogLead(collapsed[0]?.provider);
4313
4388
  setOpen(next);
4314
4389
  if (next) refresh();
4315
4390
  };
@@ -4383,10 +4458,10 @@ function SubscriptionUsageBadge({ rpc, currentModel, t }) {
4383
4458
  windows: account.windows,
4384
4459
  observedAt: account.observedAt,
4385
4460
  stale: account.stale,
4386
- model: d.provider === badgeSelection?.provider ? badgeSelection.model : void 0,
4461
+ model: d.provider === dialogProvider ? dialogModel : void 0,
4387
4462
  provider: d.provider,
4388
4463
  translate
4389
- }, `${d.provider}:${badgeSelection?.model ?? ""}`)]
4464
+ }, `${d.provider}:${dialogModel ?? ""}`)]
4390
4465
  }, account.key))]
4391
4466
  }, d.provider))
4392
4467
  ]