dsh-subscriptions 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +115 -4
  2. package/icon.svg +28 -0
  3. package/lib/client/CursorCard.js +15 -11
  4. package/lib/client/ExternalUsageCards.d.ts +11 -1
  5. package/lib/client/ExternalUsageCards.js +28 -13
  6. package/lib/client/SubscriptionUsageBadge.d.ts +25 -4
  7. package/lib/client/SubscriptionUsageBadge.js +115 -28
  8. package/lib/client/SubscriptionsSection.d.ts +7 -2
  9. package/lib/client/SubscriptionsSection.js +9 -13
  10. package/lib/client/UsageBadgeDisplaySetting.js +19 -2
  11. package/lib/client/UsageMeter.d.ts +8 -0
  12. package/lib/client/UsageMeter.js +56 -0
  13. package/lib/client/host-contract.d.ts +1 -1
  14. package/lib/client/host-contract.js +1 -0
  15. package/lib/client/locales.d.ts +50 -0
  16. package/lib/client/locales.js +52 -2
  17. package/lib/client/subscription-card-styles.js +0 -5
  18. package/lib/client/usage-badge-preferences.d.ts +7 -2
  19. package/lib/client/usage-badge-preferences.js +3 -2
  20. package/lib/client/usage-color-preferences.d.ts +6 -0
  21. package/lib/client/usage-color-preferences.js +38 -0
  22. package/lib/client/usage-pace.d.ts +42 -0
  23. package/lib/client/usage-pace.js +71 -0
  24. package/lib/client.js +489 -132
  25. package/lib/client.js.map +1 -1
  26. package/lib/index.js +252 -35
  27. package/lib/providers/claude.js +7 -0
  28. package/lib/providers/codex.js +5 -0
  29. package/lib/providers/common.d.ts +10 -0
  30. package/lib/providers/cursor-usage.js +6 -2
  31. package/lib/providers/external-usage-controller.d.ts +1 -1
  32. package/lib/providers/external-usage-controller.js +8 -3
  33. package/lib/providers/external-usage.d.ts +1 -1
  34. package/lib/providers/external-usage.js +62 -2
  35. package/lib/providers/grok.js +3 -1
  36. package/lib/providers/minimax-usage.d.ts +24 -0
  37. package/lib/providers/minimax-usage.js +85 -0
  38. package/lib/providers/pool-usage.js +6 -4
  39. package/locale/en.json +6 -0
  40. package/locale/zh.json +6 -0
  41. package/package.json +7 -3
@@ -79,6 +79,30 @@ export const en = {
79
79
  usageMonthly: 'Monthly',
80
80
  usageWindow: 'Window',
81
81
  usageResets: 'resets {date}',
82
+ usageMeterUsed: 'Used {used}% · Remaining {remaining}%',
83
+ usageMeterPace: 'Time elapsed {elapsed}% (vertical marker)',
84
+ usageMeterPaceAhead: '{points} points ahead of pace',
85
+ usageMeterPaceBehind: '{points} points behind pace',
86
+ usageMeterPaceEven: 'On pace',
87
+ usageMeterInvalid: 'Quota unavailable',
88
+ usageMeterStale: 'Reading is stale or unavailable; refresh to confirm',
89
+ usageMeterNearLimit: 'Near exhaustion',
90
+ usageMeterAhead: 'Usage ahead of pace',
91
+ usageMeterNoWarning: 'No quota warning',
92
+ usageMeterUnknownTime: 'Time progress unknown',
93
+ usageMeterReset: 'Resets in {duration}',
94
+ usageUnitDay: '{count} day',
95
+ usageUnitDays: '{count} days',
96
+ usageUnitHour: '{count} hour',
97
+ usageUnitHours: '{count} hours',
98
+ usageUnitMinute: '{count} minute',
99
+ usageUnitMinutes: '{count} minutes',
100
+ usageColorLabel: 'Usage coloring',
101
+ usageColorStandard: 'Standard (default · 10-point lead)',
102
+ usageColorRelaxed: 'Relaxed (15-point lead)',
103
+ usageColorRemaining: 'Remaining quota only (no yellow)',
104
+ usageColorPaceHint: 'Yellow when usage leads elapsed time by {allowance} points; red at 90% used takes priority. Saved in this browser; only colors change.',
105
+ usageColorRemainingHint: 'Red at 90% used; no yellow pace warnings. Reliable time markers remain visible. Saved in this browser.',
82
106
  usagePlan: 'Plan: {plan}',
83
107
  externalUsageTitle: 'Built-in provider usage',
84
108
  externalUsageConnected: 'API key configured',
@@ -131,8 +155,9 @@ export const en = {
131
155
  usageBadgeTitle: 'Provider usage',
132
156
  usageBadgeDisplay: 'Status-bar quota display',
133
157
  usageBadgeDisplayRecent: 'Current / most recent provider with usage',
158
+ usageBadgeDisplayAlways: 'Always show one provider, rotating through the rest',
134
159
  usageBadgeDisplayHidden: 'Hidden',
135
- 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.',
160
+ 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.',
136
161
  usageBadgeDisplaySaveFailed: 'Could not save the display preference. Check browser storage permissions.',
137
162
  usageBadgeCurrent: 'current',
138
163
  usageBadgeModelCount: '{count} model quotas',
@@ -226,6 +251,30 @@ export const zh = {
226
251
  usageMonthly: '每月',
227
252
  usageWindow: '窗口',
228
253
  usageResets: '{date} 重置',
254
+ usageMeterUsed: '已用 {used}% · 剩余 {remaining}%',
255
+ usageMeterPace: '时间已过 {elapsed}%(竖线标记)',
256
+ usageMeterPaceAhead: '领先时间进度 {points} 个百分点',
257
+ usageMeterPaceBehind: '落后时间进度 {points} 个百分点',
258
+ usageMeterPaceEven: '与时间进度一致',
259
+ usageMeterInvalid: '额度数据不可用',
260
+ usageMeterStale: '读数已陈旧或不可用,请刷新确认',
261
+ usageMeterNearLimit: '额度接近耗尽',
262
+ usageMeterAhead: '用量领先时间进度',
263
+ usageMeterNoWarning: '未触发额度警告',
264
+ usageMeterUnknownTime: '时间进度未知',
265
+ usageMeterReset: '{duration}后重置',
266
+ usageUnitDay: '{count} 天',
267
+ usageUnitDays: '{count} 天',
268
+ usageUnitHour: '{count} 小时',
269
+ usageUnitHours: '{count} 小时',
270
+ usageUnitMinute: '{count} 分钟',
271
+ usageUnitMinutes: '{count} 分钟',
272
+ usageColorLabel: '用量配色',
273
+ usageColorStandard: '标准(默认 · 领先 10 个百分点)',
274
+ usageColorRelaxed: '宽松(领先 15 个百分点)',
275
+ usageColorRemaining: '仅剩余额度(不显示黄色)',
276
+ usageColorPaceHint: '用量领先时间进度 {allowance} 个百分点时变黄;已用达到 90% 时优先变红。自动保存到当前浏览器,仅改变配色。',
277
+ usageColorRemainingHint: '已用达到 90% 时变红,不显示黄色节奏警告。可靠的时间标记仍保留,自动保存到当前浏览器。',
229
278
  usagePlan: '计划:{plan}',
230
279
  externalUsageTitle: '内置供应商用量',
231
280
  externalUsageConnected: '已配置 API 密钥',
@@ -278,8 +327,9 @@ export const zh = {
278
327
  usageBadgeTitle: '服务商用量',
279
328
  usageBadgeDisplay: '状态栏额度显示',
280
329
  usageBadgeDisplayRecent: '仅显示当前/最近使用的用量服务商',
330
+ usageBadgeDisplayAlways: '始终显示一家,其余服务商轮播',
281
331
  usageBadgeDisplayHidden: '不显示',
282
- usageBadgeDisplayHint: '自动保存到当前浏览器。状态栏一次显示一家服务商;没有额度来源的模型沿用本视图最近选择的服务商,没有记录则隐藏。关闭不影响本页的额度详情。',
332
+ usageBadgeDisplayHint: '自动保存到当前浏览器。状态栏一次显示一家服务商;没有额度来源的模型沿用本视图最近选择的服务商,没有记录则隐藏。选择“始终显示”时,当前模型没有额度来源,就每 10 秒轮播一家有额度的服务商。关闭不影响本页的额度详情。',
283
333
  usageBadgeDisplaySaveFailed: '显示设置保存失败,请检查浏览器存储权限。',
284
334
  usageBadgeCurrent: '当前',
285
335
  usageBadgeModelCount: '{count} 个模型配额',
@@ -45,9 +45,4 @@ export const subscriptionCardStyles = {
45
45
  display: 'flex', justifyContent: 'space-between', gap: 8,
46
46
  fontSize: 12, lineHeight: '18px', color: 'var(--dsw-alias-label-tertiary)',
47
47
  },
48
- usageTrack: {
49
- height: 6, borderRadius: 3, overflow: 'hidden',
50
- background: 'var(--dsw-alias-bg-layer-1)', border: '1px solid var(--dsw-alias-border-l2)',
51
- },
52
- usageFill: { height: '100%', borderRadius: 3 },
53
48
  };
@@ -1,5 +1,10 @@
1
- /** Display-only preference: no credentials or account history are stored. */
2
- export type UsageBadgeMode = 'recent' | 'hidden';
1
+ /**
2
+ * Display-only preference: no credentials or account history are stored.
3
+ * `recent` pins the current/most-recent provider; `always` keeps a provider
4
+ * visible at all times, rotating through the others when the current model
5
+ * has no quota source; `hidden` shows nothing.
6
+ */
7
+ export type UsageBadgeMode = 'recent' | 'always' | 'hidden';
3
8
  export declare const USAGE_BADGE_REFRESH_EVENT = "dsh:subscriptions:usage-refresh";
4
9
  /** Read the browser preference; blocked storage retains the default display. */
5
10
  export declare function readUsageBadgeMode(): UsageBadgeMode;
@@ -7,7 +7,8 @@ export function readUsageBadgeMode() {
7
7
  if (typeof window === 'undefined')
8
8
  return 'recent';
9
9
  try {
10
- return window.localStorage.getItem(MODE_KEY) === 'hidden' ? 'hidden' : 'recent';
10
+ const value = window.localStorage.getItem(MODE_KEY);
11
+ return value === 'hidden' || value === 'always' ? value : 'recent';
11
12
  }
12
13
  catch (error) {
13
14
  if (!(error instanceof DOMException) || error.name !== 'SecurityError')
@@ -17,7 +18,7 @@ export function readUsageBadgeMode() {
17
18
  }
18
19
  /** Save first, then notify; the settings control reports a rejected write. */
19
20
  export function setUsageBadgeMode(mode) {
20
- if (mode !== 'recent' && mode !== 'hidden')
21
+ if (mode !== 'recent' && mode !== 'always' && mode !== 'hidden')
21
22
  throw new TypeError('Invalid subscription usage display mode');
22
23
  window.localStorage.setItem(MODE_KEY, mode);
23
24
  window.dispatchEvent(new Event(MODE_EVENT));
@@ -0,0 +1,6 @@
1
+ export type UsageColorPreset = 'standard' | 'relaxed' | 'remaining';
2
+ export declare function isUsageColorPreset(value: unknown): value is UsageColorPreset;
3
+ export declare function readUsageColorPreset(): UsageColorPreset;
4
+ export declare function setUsageColorPreset(value: UsageColorPreset): void;
5
+ export declare function subscribeUsageColorPreset(notify: () => void): () => void;
6
+ export declare function useUsageColorPreset(): UsageColorPreset;
@@ -0,0 +1,38 @@
1
+ import { useSyncExternalStore } from 'react';
2
+ const KEY = 'dsh.subscriptions.usageColorPreset';
3
+ const EVENT = 'dsh:subscriptions:usage-color-preset';
4
+ export function isUsageColorPreset(value) {
5
+ return value === 'standard' || value === 'relaxed' || value === 'remaining';
6
+ }
7
+ export function readUsageColorPreset() {
8
+ if (typeof window === 'undefined')
9
+ return 'standard';
10
+ try {
11
+ const value = window.localStorage.getItem(KEY);
12
+ return isUsageColorPreset(value) ? value : 'standard';
13
+ }
14
+ catch (error) {
15
+ if (!(error instanceof DOMException) || error.name !== 'SecurityError')
16
+ throw error;
17
+ return 'standard';
18
+ }
19
+ }
20
+ export function setUsageColorPreset(value) {
21
+ if (!isUsageColorPreset(value))
22
+ throw new TypeError('Invalid usage coloring preset');
23
+ window.localStorage.setItem(KEY, value);
24
+ window.dispatchEvent(new Event(EVENT));
25
+ }
26
+ export function subscribeUsageColorPreset(notify) {
27
+ const storage = (event) => { if (event.key === KEY || event.key === null)
28
+ notify(); };
29
+ window.addEventListener(EVENT, notify);
30
+ window.addEventListener('storage', storage);
31
+ return () => {
32
+ window.removeEventListener(EVENT, notify);
33
+ window.removeEventListener('storage', storage);
34
+ };
35
+ }
36
+ export function useUsageColorPreset() {
37
+ return useSyncExternalStore(subscribeUsageColorPreset, readUsageColorPreset, () => 'standard');
38
+ }
@@ -0,0 +1,42 @@
1
+ import type { UsageWindow } from './SubscriptionsSection.js';
2
+ import type { UsageColorPreset } from './usage-color-preferences.js';
3
+ export interface UsageObservation {
4
+ observedAt?: number | undefined;
5
+ stale?: boolean | undefined;
6
+ }
7
+ export type UsageColorState = 'neutral' | 'red' | 'yellow' | 'green';
8
+ export declare const USAGE_FRESHNESS_MS: number;
9
+ export declare function validUsage(used: number): boolean;
10
+ /**
11
+ * Rounded share for display, or undefined when the provider's number is
12
+ * unusable. Callers must show the unavailable state rather than clamp an
13
+ * out-of-range reading into 0% or 100%.
14
+ */
15
+ export declare function displayUsedPercent(used: number): number | undefined;
16
+ export interface CountdownPart {
17
+ unit: 'day' | 'hour' | 'minute';
18
+ count: number;
19
+ }
20
+ /**
21
+ * Reset countdown as the two largest nonzero whole units, rounded up so a
22
+ * window never announces zero remaining time. Always at least one minute.
23
+ */
24
+ export declare function resetCountdownParts(ms: number): CountdownPart[];
25
+ /**
26
+ * Whether the reported percentage still describes the window now. Only a fresh
27
+ * reading may be compared with the current time (pace warnings); an old one
28
+ * keeps its absolute color. This does not decide whether the time cursor is
29
+ * drawn: where the window stands in time never depends on the reading.
30
+ */
31
+ export declare function isUsageFresh(window: UsageWindow, observation: UsageObservation, now?: number): boolean;
32
+ /**
33
+ * How far through its window the clock is, from the window's own timing alone.
34
+ * It ignores the percentage and how recently it was read, so an unused,
35
+ * stale, aged or unobserved reading keeps its cursor whenever the timing
36
+ * places it. Returns undefined when the timing cannot: only an explicit
37
+ * interval or a provider-verified fixed duration supports linear pace, and no
38
+ * start is invented from a window's kind or label.
39
+ */
40
+ export declare function elapsedPercent(window: UsageWindow, now?: number): number | undefined;
41
+ export declare function usageColorState(used: number, elapsed: number | undefined, preset: UsageColorPreset, fresh: boolean): UsageColorState;
42
+ export declare function usageBarColor(used: number, elapsed?: number, preset?: UsageColorPreset, fresh?: boolean): string;
@@ -0,0 +1,71 @@
1
+ export const USAGE_FRESHNESS_MS = 5 * 60_000;
2
+ export function validUsage(used) {
3
+ return Number.isFinite(used) && used >= 0 && used <= 100;
4
+ }
5
+ /**
6
+ * Rounded share for display, or undefined when the provider's number is
7
+ * unusable. Callers must show the unavailable state rather than clamp an
8
+ * out-of-range reading into 0% or 100%.
9
+ */
10
+ export function displayUsedPercent(used) {
11
+ return validUsage(used) ? Math.round(used) : undefined;
12
+ }
13
+ /**
14
+ * Reset countdown as the two largest nonzero whole units, rounded up so a
15
+ * window never announces zero remaining time. Always at least one minute.
16
+ */
17
+ export function resetCountdownParts(ms) {
18
+ const total = Math.max(1, Math.ceil(ms / 60_000));
19
+ const parts = [
20
+ { unit: 'day', count: Math.floor(total / 1440) },
21
+ { unit: 'hour', count: Math.floor((total % 1440) / 60) },
22
+ { unit: 'minute', count: total % 60 },
23
+ ];
24
+ return parts.filter(part => part.count > 0).slice(0, 2);
25
+ }
26
+ /**
27
+ * Whether the reported percentage still describes the window now. Only a fresh
28
+ * reading may be compared with the current time (pace warnings); an old one
29
+ * keeps its absolute color. This does not decide whether the time cursor is
30
+ * drawn: where the window stands in time never depends on the reading.
31
+ */
32
+ export function isUsageFresh(window, observation, now = Date.now()) {
33
+ return validUsage(window.usedPercent) && observation.stale !== true
34
+ && observation.observedAt !== undefined && Number.isFinite(observation.observedAt)
35
+ && observation.observedAt <= now && now - observation.observedAt <= USAGE_FRESHNESS_MS
36
+ && (window.resetsAt === undefined || Number.isFinite(window.resetsAt) && window.resetsAt > now);
37
+ }
38
+ /**
39
+ * How far through its window the clock is, from the window's own timing alone.
40
+ * It ignores the percentage and how recently it was read, so an unused,
41
+ * stale, aged or unobserved reading keeps its cursor whenever the timing
42
+ * places it. Returns undefined when the timing cannot: only an explicit
43
+ * interval or a provider-verified fixed duration supports linear pace, and no
44
+ * start is invented from a window's kind or label.
45
+ */
46
+ export function elapsedPercent(window, now = Date.now()) {
47
+ const end = window.resetsAt;
48
+ if (end === undefined || !Number.isFinite(end) || end <= now)
49
+ return undefined;
50
+ const duration = window.windowDurationMs;
51
+ const start = window.startsAt ?? (window.fixedWindow === true && duration !== undefined && Number.isFinite(duration) && duration > 0 ? end - duration : undefined);
52
+ if (start === undefined || !Number.isFinite(start) || start >= end || start > now)
53
+ return undefined;
54
+ return Math.min(100, Math.max(0, (now - start) / (end - start) * 100));
55
+ }
56
+ export function usageColorState(used, elapsed, preset, fresh) {
57
+ if (!validUsage(used))
58
+ return 'neutral';
59
+ if (used >= 90)
60
+ return 'red';
61
+ // Missing pace or stale observations retain the last reported absolute color.
62
+ if (!fresh || preset === 'remaining' || elapsed === undefined)
63
+ return 'green';
64
+ return used - elapsed >= (preset === 'relaxed' ? 15 : 10) ? 'yellow' : 'green';
65
+ }
66
+ export function usageBarColor(used, elapsed, preset = 'standard', fresh = true) {
67
+ const state = usageColorState(used, elapsed, preset, fresh);
68
+ return state === 'red' ? 'var(--dsw-alias-state-error-primary)'
69
+ : state === 'yellow' ? 'var(--dsw-alias-state-warn-label)'
70
+ : 'var(--dsw-alias-state-success-primary)';
71
+ }