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.
- package/README.md +115 -4
- package/icon.svg +28 -0
- package/lib/client/CursorCard.js +15 -11
- package/lib/client/ExternalUsageCards.d.ts +11 -1
- package/lib/client/ExternalUsageCards.js +28 -13
- package/lib/client/SubscriptionUsageBadge.d.ts +25 -4
- package/lib/client/SubscriptionUsageBadge.js +115 -28
- package/lib/client/SubscriptionsSection.d.ts +7 -2
- package/lib/client/SubscriptionsSection.js +9 -13
- package/lib/client/UsageBadgeDisplaySetting.js +19 -2
- package/lib/client/UsageMeter.d.ts +8 -0
- package/lib/client/UsageMeter.js +56 -0
- package/lib/client/host-contract.d.ts +1 -1
- package/lib/client/host-contract.js +1 -0
- package/lib/client/locales.d.ts +50 -0
- package/lib/client/locales.js +52 -2
- package/lib/client/subscription-card-styles.js +0 -5
- package/lib/client/usage-badge-preferences.d.ts +7 -2
- package/lib/client/usage-badge-preferences.js +3 -2
- package/lib/client/usage-color-preferences.d.ts +6 -0
- package/lib/client/usage-color-preferences.js +38 -0
- package/lib/client/usage-pace.d.ts +42 -0
- package/lib/client/usage-pace.js +71 -0
- package/lib/client.js +489 -132
- package/lib/client.js.map +1 -1
- package/lib/index.js +252 -35
- package/lib/providers/claude.js +7 -0
- package/lib/providers/codex.js +5 -0
- package/lib/providers/common.d.ts +10 -0
- package/lib/providers/cursor-usage.js +6 -2
- package/lib/providers/external-usage-controller.d.ts +1 -1
- package/lib/providers/external-usage-controller.js +8 -3
- package/lib/providers/external-usage.d.ts +1 -1
- package/lib/providers/external-usage.js +62 -2
- package/lib/providers/grok.js +3 -1
- package/lib/providers/minimax-usage.d.ts +24 -0
- package/lib/providers/minimax-usage.js +85 -0
- package/lib/providers/pool-usage.js +6 -4
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- 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
|
-
|
|
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
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
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
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
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 =
|
|
1543
|
-
const label =
|
|
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
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
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: [
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
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 =
|
|
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: [`${
|
|
2551
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
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 [
|
|
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
|
-
/**
|
|
3806
|
-
|
|
3807
|
-
|
|
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) =>
|
|
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) =>
|
|
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))
|
|
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)
|
|
3929
|
-
|
|
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
|
|
4023
|
-
const
|
|
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
|
-
|
|
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}:${
|
|
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 =
|
|
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
|
-
|
|
4169
|
-
"
|
|
4170
|
-
|
|
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)(
|
|
4538
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageMeter, {
|
|
4539
|
+
window: w,
|
|
4540
|
+
t,
|
|
4177
4541
|
style: styles.bar,
|
|
4178
|
-
|
|
4179
|
-
|
|
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
|
|