dsh-subscriptions 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +115 -4
  2. package/icon.svg +28 -0
  3. package/lib/client/CursorCard.js +15 -11
  4. package/lib/client/ExternalUsageCards.d.ts +11 -1
  5. package/lib/client/ExternalUsageCards.js +28 -13
  6. package/lib/client/SubscriptionUsageBadge.d.ts +25 -4
  7. package/lib/client/SubscriptionUsageBadge.js +115 -28
  8. package/lib/client/SubscriptionsSection.d.ts +7 -2
  9. package/lib/client/SubscriptionsSection.js +9 -13
  10. package/lib/client/UsageBadgeDisplaySetting.js +19 -2
  11. package/lib/client/UsageMeter.d.ts +8 -0
  12. package/lib/client/UsageMeter.js +56 -0
  13. package/lib/client/host-contract.d.ts +1 -1
  14. package/lib/client/host-contract.js +1 -0
  15. package/lib/client/locales.d.ts +50 -0
  16. package/lib/client/locales.js +52 -2
  17. package/lib/client/subscription-card-styles.js +0 -5
  18. package/lib/client/usage-badge-preferences.d.ts +7 -2
  19. package/lib/client/usage-badge-preferences.js +3 -2
  20. package/lib/client/usage-color-preferences.d.ts +6 -0
  21. package/lib/client/usage-color-preferences.js +38 -0
  22. package/lib/client/usage-pace.d.ts +42 -0
  23. package/lib/client/usage-pace.js +71 -0
  24. package/lib/client.js +489 -132
  25. package/lib/client.js.map +1 -1
  26. package/lib/index.js +252 -35
  27. package/lib/providers/claude.js +7 -0
  28. package/lib/providers/codex.js +5 -0
  29. package/lib/providers/common.d.ts +10 -0
  30. package/lib/providers/cursor-usage.js +6 -2
  31. package/lib/providers/external-usage-controller.d.ts +1 -1
  32. package/lib/providers/external-usage-controller.js +8 -3
  33. package/lib/providers/external-usage.d.ts +1 -1
  34. package/lib/providers/external-usage.js +62 -2
  35. package/lib/providers/grok.js +3 -1
  36. package/lib/providers/minimax-usage.d.ts +24 -0
  37. package/lib/providers/minimax-usage.js +85 -0
  38. package/lib/providers/pool-usage.js +6 -4
  39. package/locale/en.json +6 -0
  40. package/locale/zh.json +6 -0
  41. package/package.json +7 -3
package/README.md CHANGED
@@ -1,4 +1,7 @@
1
- # DSH Subscription Hub
1
+ # DSH Subscriptions
2
+
3
+ [![npm version](https://img.shields.io/npm/v/dsh-subscriptions)](https://www.npmjs.com/package/dsh-subscriptions)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
2
5
 
3
6
  Bring your AI subscriptions into
4
7
  [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (DSH). One
@@ -23,12 +26,86 @@ reported quota in **Settings → Subscriptions**.
23
26
  image generation and editing, and Grok video generation when the matching
24
27
  provider is enabled.
25
28
 
29
+ ## Screenshots
30
+
31
+ Captured against DSH 0.2.0-rc.2 in the web UI with a local stub serving
32
+ frozen provider responses, so each image shows a product state rather than
33
+ a real account. Identities in the images are fabricated, and the quota
34
+ percentages, catalogs, and reset countdowns are sample data.
35
+
36
+ ### Subscriptions
37
+
38
+ Per-provider quota windows, account controls, and the status-bar display
39
+ preference live in **Settings → Subscriptions**.
40
+
41
+ ![Subscriptions settings page](docs/images/subscriptions.png)
42
+
43
+ ### Model selection
44
+
45
+ Logged-in providers join the session model picker with their live catalogs.
46
+
47
+ ![Model picker with subscription models](docs/images/model-picker.png)
48
+
49
+ Models that advertise reasoning levels get an **Effort** selector in the
50
+ same menu. Levels and defaults come from the provider's live catalog;
51
+ without discovery Codex falls back to a built-in list, and a configured
52
+ default overrides either.
53
+
54
+ ![Reasoning effort selector](docs/images/model-effort.png)
55
+
56
+ Codex models whose catalog advertises the fast tier get a **Speed** toggle
57
+ in the composer's tool row: Standard or Fast, per session.
58
+
59
+ ![Speed toggle with the Standard/Fast menu open](docs/images/speed-toggle.png)
60
+
61
+ **Settings → Subscriptions → provider → Manage** edits account aliases,
62
+ pool participation, model allowlists, and the per-model effort, context,
63
+ and tool settings.
64
+
65
+ ![Model settings](docs/images/model-settings.png)
66
+
67
+ ![Provider tools](docs/images/provider-tools.png)
68
+
69
+ ### Quota while you work
70
+
71
+ The composer's stats row gains a **provider usage** pill showing the
72
+ used percentage and reset window for the provider of the session's current
73
+ model. It shows at most one provider; switching to a non-subscription model
74
+ keeps the most recent subscription selected in the mounted conversation
75
+ view, or stays hidden if there is none. **Always show** instead rotates
76
+ through the providers that report usage every 10 seconds while the current
77
+ model has no quota source, so the pill never disappears.
78
+
79
+ ![Provider usage pill in the stats row](docs/images/usage-pill.png)
80
+
81
+ Click the pill to expand every provider and account that reports usage —
82
+ the default account is starred, and the current provider is listed first.
83
+ A provider with no usage endpoint, such as GitHub Copilot, has no row, and
84
+ neither do accounts without a usage window. Antigravity previews only the
85
+ current model's windows; the other model windows stay in a closed
86
+ disclosure.
87
+
88
+ Quota bars include a dark elapsed-time cursor whenever the provider's
89
+ timing places the window in time, including at 0% used and for stale or old
90
+ readings. Compare the colored fill (quota used) with the cursor (time
91
+ elapsed); hover for the percentages and reset countdown. Standard coloring
92
+ turns the fill orange when a fresh reading leads elapsed time by 10 points,
93
+ and Relaxed uses 15; 90% used turns it red and takes priority. Windows
94
+ without known timing omit the cursor, and a stale or old reading keeps its
95
+ cursor without a pace warning. Choose Standard, Relaxed, or Remaining colors
96
+ in **Settings → Subscriptions**.
97
+
98
+ ![Usage dialog with elapsed-time cursors](docs/images/usage-badge.png)
99
+
26
100
  ## Install
27
101
 
28
102
  Current source is tested with DSH `0.1.7-rc.2`, `0.2.0-rc.1`, and
29
103
  `0.2.0-rc.2`. The package's peers admit `>=0.1.7-rc.2 <0.3.0-0`: untested
30
- versions inside that range install but are unsupported, and versions outside
31
- it need an explicit `dsh plugin allow-version` exemption. See
104
+ versions inside that range install but are unsupported, and versions
105
+ outside it need an explicit `dsh plugin allow-version` exemption.
106
+ `dsh plugin add` checks those ranges with `includePrerelease` semantics, so
107
+ the admitted set covers every host build from `0.1.7-rc.2` through the
108
+ whole `0.2.x` line, prereleases included, and blocks the `0.3.0` line. See
32
109
  [docs/compatibility.md](docs/compatibility.md)
33
110
  for the support window and [docs/testing.md](docs/testing.md) for the test
34
111
  layers. With `dsh` available, install the plugin into the web profile from
@@ -74,7 +151,7 @@ spec (`#vX.Y.Z`); see the
74
151
 
75
152
  ## Verification and limits
76
153
 
77
- CI builds, tests, and boots a packed tarball on both supported DSH versions;
154
+ CI builds, tests, and boots a packed tarball on all supported DSH versions;
78
155
  it does not run the GitHub source install's `prepare` step. Offline
79
156
  provider tests check recorded API responses, so a passing CI run cannot prove
80
157
  that a provider still accepts live sign-in or model requests. See
@@ -106,6 +183,40 @@ usage.
106
183
  The plugin follows DSH network settings. Its former proxy configuration and
107
184
  UI have been retired; an old plugin proxy config file is ignored.
108
185
 
186
+ ## Usage coloring
187
+
188
+ **Settings → Subscriptions → Usage coloring** offers one browser-local dropdown:
189
+
190
+ - **Standard** (default): yellow when used quota leads elapsed time by at least 10 percentage points.
191
+ - **Relaxed**: yellow at a lead of at least 15 points.
192
+ - **Remaining quota only**: no yellow pace warnings.
193
+
194
+ Every preset shows reported usage of 90% or more in red, before considering pace. For example, 40% used after 25% of a window is a 15-point lead, not a consumption forecast. Preferences apply to all meters, survive reloads and synchronize across tabs of the same origin. They change display only, never account routing or quota enforcement.
195
+
196
+ A vertical marker shows elapsed time only for an explicit valid start/end interval or a provider-verified fixed duration. A session/weekly label alone does not establish a fixed window; rolling or unknown intervals have no marker. Claude's recognized five-hour/seven-day buckets and Codex's reported quota durations supply fixed-window metadata. Unknown timing falls back to green below 90%, without a marker or yellow warning.
197
+
198
+ The marker depends on the window's timing alone, not on the reading: it stays at 0% used, for an invalid percentage, and when a refresh failed or the reading is more than five minutes old. Such a reading is not set against the current time, so it gets no yellow pace warning and its tooltip omits the ahead/behind comparison. It retains the last reported green/red color with an accessible stale label; refresh to confirm it. Once the reset time has passed the window has no position and the marker disappears. Invalid percentages show an unavailable state without a fabricated fill. Tooltips explain remaining quota, time progress and reset countdown where available. See the [decision record](.agents/notes/implemented/architecture/2026-10-01-usage-pace-colors.md) and the [cursor note](.agents/notes/implemented/architecture/2026-10-06-usage-cursor-timing-only.md) for rationale and verification limits.
199
+
200
+ ### MiniMax subscription usage
201
+
202
+ Configure `MINIMAX_API_KEY` for the global service or
203
+ `MINIMAX_CN_API_KEY` for China, using DSH's standard model-key configuration.
204
+ The plugin reuses those credential references; no separate subscription ref is
205
+ required. Quota reads require subscription keys, not `sk-api-*` pay-as-you-go keys. The plugin reads the
206
+ region's `/v1/token_plan/remains` endpoint and displays finite per-model quotas
207
+ with API-provided start/end times. It does not add model routing or OAuth login.
208
+ Rows are labelled with the model name, and the plugin shows the windows the API
209
+ returns for each model. MiniMax documents a five-hour and a weekly window for
210
+ non-video models and only a weekly one for video. Standard (non-video) models
211
+ on time-based plans report an explicit remaining percentage beside zero counts,
212
+ and that percentage is enough for a row. Unlimited, boosted-above-100% and
213
+ unsupported quotas are omitted (a status-3 row also covers a model the plan
214
+ does not include); if none remain, the card reports unavailable rather than
215
+ inventing a percentage. Explicit remaining percentages take priority; older
216
+ responses follow the official CLI's remaining-count compatibility convention. No
217
+ live-account response was used in verification, so a missing model may still
218
+ come from the account or the plan rather than from this plugin.
219
+
109
220
  ## Development
110
221
 
111
222
  ```sh
package/icon.svg ADDED
@@ -0,0 +1,28 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
2
+ <defs>
3
+ <linearGradient id="bg" x1="0" y1="0" x2="256" y2="256" gradientUnits="userSpaceOnUse">
4
+ <stop offset="0" stop-color="#2f6df6"/>
5
+ <stop offset="1" stop-color="#0bb6a8"/>
6
+ </linearGradient>
7
+ </defs>
8
+ <rect width="256" height="256" rx="58" fill="url(#bg)"/>
9
+ <g stroke="#ffffff" stroke-opacity="0.65" stroke-width="7" stroke-linecap="round">
10
+ <line x1="128" y1="128" x2="54" y2="64"/>
11
+ <line x1="128" y1="128" x2="202" y2="64"/>
12
+ <line x1="128" y1="128" x2="42" y2="150"/>
13
+ <line x1="128" y1="128" x2="214" y2="150"/>
14
+ <line x1="128" y1="128" x2="78" y2="208"/>
15
+ <line x1="128" y1="128" x2="178" y2="208"/>
16
+ </g>
17
+ <g fill="#ffffff">
18
+ <circle cx="54" cy="64" r="17"/>
19
+ <circle cx="202" cy="64" r="17"/>
20
+ <circle cx="42" cy="150" r="17"/>
21
+ <circle cx="214" cy="150" r="17"/>
22
+ <circle cx="78" cy="208" r="17"/>
23
+ <circle cx="178" cy="208" r="17"/>
24
+ </g>
25
+ <circle cx="128" cy="128" r="37" fill="#ffffff"/>
26
+ <circle cx="128" cy="128" r="37" fill="none" stroke="#0b3b5e" stroke-opacity="0.18" stroke-width="3"/>
27
+ <circle cx="128" cy="128" r="15" fill="#0e7490"/>
28
+ </svg>
@@ -4,20 +4,17 @@ import { callSubscriptionsAuth } from './subscriptions-rpc.js';
4
4
  import { ProviderAccountManager } from './ProviderAccountManager.js';
5
5
  import { USAGE_BADGE_REFRESH_EVENT } from './usage-badge-preferences.js';
6
6
  import { subscriptionCardStyles as styles } from './subscription-card-styles.js';
7
+ import { UsageMeter } from './UsageMeter.js';
8
+ import { displayUsedPercent } from './usage-pace.js';
7
9
  function message(error) {
8
10
  return error instanceof Error ? error.message : String(error);
9
11
  }
10
- function barColor(percent) {
11
- if (percent >= 95)
12
- return 'var(--dsw-alias-state-error-primary)';
13
- if (percent >= 80)
14
- return 'var(--dsw-alias-state-warn-label)';
15
- return 'var(--dsw-alias-state-success-primary)';
16
- }
17
12
  /** Cursor's single connected account, rendered with the same card and Manage dialog as the other providers. */
18
13
  export function CursorCard({ rpc, t }) {
19
14
  const [status, setStatus] = useState();
20
15
  const [usage, setUsage] = useState();
16
+ const [observedAt, setObservedAt] = useState();
17
+ const [usageStale, setUsageStale] = useState(false);
21
18
  const [authorizeUrl, setAuthorizeUrl] = useState();
22
19
  const [error, setError] = useState();
23
20
  const [loading, setLoading] = useState(false);
@@ -43,12 +40,17 @@ export function CursorCard({ rpc, t }) {
43
40
  const refreshUsage = useCallback(async () => {
44
41
  setLoading(true);
45
42
  try {
46
- setUsage(await callSubscriptionsAuth(rpc, 'cursorUsage', {}));
43
+ const value = await callSubscriptionsAuth(rpc, 'cursorUsage', {});
44
+ setUsage(value);
45
+ // Prefer the server's observation time; a cached reading must not look fresh.
46
+ setObservedAt(value.observedAt ?? Date.now());
47
+ setUsageStale(value.stale === true);
47
48
  setError(undefined);
48
49
  window.dispatchEvent(new Event(USAGE_BADGE_REFRESH_EVENT));
49
50
  }
50
51
  catch (failure) {
51
52
  setError(message(failure));
53
+ setUsageStale(true);
52
54
  }
53
55
  finally {
54
56
  setLoading(false);
@@ -91,6 +93,8 @@ export function CursorCard({ rpc, t }) {
91
93
  try {
92
94
  await callSubscriptionsAuth(rpc, 'cursorLogout', {});
93
95
  setUsage(undefined);
96
+ setObservedAt(undefined);
97
+ setUsageStale(false);
94
98
  await refreshStatus();
95
99
  }
96
100
  catch (failure) {
@@ -101,13 +105,13 @@ export function CursorCard({ rpc, t }) {
101
105
  return _jsxs("div", { style: styles.card, children: [_jsxs("div", { style: styles.header, children: [_jsx("span", { style: { ...styles.dot, background: status?.busy ? 'var(--dsw-alias-state-warn-label)'
102
106
  : connected ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-label-dimmed)' } }), _jsx("span", { style: styles.name, children: t('cursorTitle') })] }), _jsx("p", { style: styles.status, children: status === undefined ? t('checking') : status.busy ? t('loginInProgress')
103
107
  : connected ? t('loggedInCount', { count: 1 }) : t('notLoggedIn') }), error !== undefined && _jsx("p", { style: styles.error, children: error }), connected && _jsxs("div", { style: styles.account, children: [_jsxs("div", { style: styles.accountHeader, children: [_jsx("span", { style: styles.defaultStar, title: t('defaultBadge'), children: "\u2605" }), _jsx("span", { style: styles.accountName, children: "Cursor" }), status.expiresAt !== undefined && _jsx("span", { style: styles.status, children: t('accountExpires', { date: new Date(status.expiresAt).toLocaleString() }) }), _jsx("button", { type: "button", style: { ...styles.button, marginLeft: 'auto', flexShrink: 0 }, onClick: () => { void logout(); }, children: t('logout') })] }), _jsxs("div", { style: styles.usage, children: [_jsxs("div", { style: styles.usageHeader, children: [_jsx("span", { style: styles.usageTitle, children: t('usageTitle') }), usage?.plan !== undefined && _jsx("span", { style: styles.status, children: t('usagePlan', { plan: usage.plan }) }), _jsx("button", { type: "button", style: styles.usageRefresh, disabled: loading, onClick: () => { void refreshUsage(); }, children: t('usageRefresh') })] }), loading && usage === undefined && _jsx("p", { style: styles.status, children: t('usageLoading') }), usage?.windows?.length === 0 && _jsx("p", { style: styles.status, children: t('usageEmpty') }), usage?.windows?.map((window, index) => {
104
- const percent = Math.min(100, Math.max(0, window.usedPercent));
108
+ const percent = displayUsedPercent(window.usedPercent);
105
109
  const label = window.scope === 'Included' ? t('cursorIncluded')
106
110
  : window.scope === 'Cursor Models' ? t('cursorModels')
107
111
  : window.scope === 'Other Models' ? t('cursorOtherModels')
108
112
  : window.scope === 'Included requests' ? t('cursorIncludedRequests')
109
113
  : window.scope ?? t('usageWindow');
110
- return _jsxs("div", { style: styles.usageRow, children: [_jsxs("div", { style: styles.usageMeta, children: [_jsx("span", { children: label }), _jsxs("span", { children: [Math.round(percent), "%", window.resetsAt === undefined ? ''
111
- : ` · ${t('usageResets', { date: new Date(window.resetsAt).toLocaleString() })}`] })] }), _jsx("div", { style: styles.usageTrack, children: _jsx("div", { style: { width: `${percent}%`, height: '100%', borderRadius: 3, background: barColor(percent) } }) })] }, index);
114
+ return _jsxs("div", { style: styles.usageRow, children: [_jsxs("div", { style: styles.usageMeta, children: [_jsx("span", { children: label }), _jsxs("span", { children: [percent === undefined ? t('usageMeterInvalid') : `${percent}%`, window.resetsAt === undefined ? ''
115
+ : ` · ${t('usageResets', { date: new Date(window.resetsAt).toLocaleString() })}`] })] }), _jsx(UsageMeter, { window: window, t: t, observedAt: observedAt, stale: usageStale })] }, index);
112
116
  })] })] }), _jsxs("div", { style: styles.actions, children: [status?.authenticated === false && !status.busy && _jsx("button", { type: "button", style: styles.button, onClick: () => { void login(); }, children: t('login') }), _jsx("button", { type: "button", style: styles.button, "aria-haspopup": "dialog", onClick: () => setManage(true), children: t('accountsManage') }), status?.busy === true && _jsx("button", { type: "button", style: styles.button, onClick: () => { void cancel(); }, children: t('cancel') })] }), status?.busy === true && authorizeUrl !== undefined && _jsx("a", { href: authorizeUrl, target: "_blank", rel: "noopener noreferrer", children: t('cursorOpenLogin') }), manage && _jsx(ProviderAccountManager, { provider: "cursor-subscription", name: "Cursor", rpc: rpc, t: t, onClose: () => setManage(false) })] });
113
117
  }
@@ -1,6 +1,16 @@
1
1
  import type { ConnectionHandle } from '@deepseek-ai/dsh-api-remotes/client';
2
- import type { SubscriptionsSectionInjected } from './SubscriptionsSection.js';
2
+ import type { SubscriptionsSectionInjected, UsageWindow } from './SubscriptionsSection.js';
3
3
  type Translate = SubscriptionsSectionInjected['t'];
4
+ /**
5
+ * Row label of one window on an external usage card. A session or weekly
6
+ * window appends the model it belongs to when the provider names one (MiniMax
7
+ * reports a window per model), so two models' weekly rows stay distinguishable.
8
+ * The monthly pool is a scope on an `other` window, not a model.
9
+ * @param t - section translate.
10
+ * @param window - the reported window.
11
+ * @returns the localized label, e.g. "Weekly · general".
12
+ */
13
+ export declare function externalUsageWindowLabel(t: Translate, window: UsageWindow): string;
4
14
  /** Quota cards for API-key providers already available through DSH itself. */
5
15
  export declare function ExternalUsageCards({ rpc, t }: {
6
16
  rpc: ConnectionHandle['rpc'];
@@ -3,33 +3,50 @@ import { useCallback, useEffect, useState } from 'react';
3
3
  import { callSubscriptionsAuth } from './subscriptions-rpc.js';
4
4
  import { subscriptionCardStyles as styles } from './subscription-card-styles.js';
5
5
  import { USAGE_BADGE_REFRESH_EVENT } from './usage-badge-preferences.js';
6
+ import { UsageMeter } from './UsageMeter.js';
7
+ import { displayUsedPercent } from './usage-pace.js';
6
8
  const SOURCES = [
7
9
  { id: 'opencode-go', name: 'OpenCode Go', ref: 'OPENCODE_GO_API_KEY' },
8
10
  { id: 'kimi-code', name: 'Kimi Code', ref: 'KIMI_CODING_API_KEY' },
11
+ { id: 'minimax', name: 'MiniMax', ref: 'MINIMAX_API_KEY' },
12
+ { id: 'minimax-cn', name: 'MiniMax CN', ref: 'MINIMAX_CN_API_KEY' },
9
13
  ];
10
14
  function errorText(error) {
11
15
  return error instanceof Error ? error.message : String(error);
12
16
  }
13
- function barColor(percent) {
14
- if (percent >= 95)
15
- return 'var(--dsw-alias-state-error-primary)';
16
- if (percent >= 80)
17
- return 'var(--dsw-alias-state-warn-label)';
18
- return 'var(--dsw-alias-state-success-primary)';
17
+ /**
18
+ * Row label of one window on an external usage card. A session or weekly
19
+ * window appends the model it belongs to when the provider names one (MiniMax
20
+ * reports a window per model), so two models' weekly rows stay distinguishable.
21
+ * The monthly pool is a scope on an `other` window, not a model.
22
+ * @param t - section translate.
23
+ * @param window - the reported window.
24
+ * @returns the localized label, e.g. "Weekly · general".
25
+ */
26
+ export function externalUsageWindowLabel(t, window) {
27
+ const named = window.scope !== undefined && window.scope !== '';
28
+ if (window.kind === 'other') {
29
+ return window.scope === 'Monthly' ? t('usageMonthly') : named ? window.scope : t('usageWindow');
30
+ }
31
+ const base = window.kind === 'session' ? t('usageSession') : t('usageWeekly');
32
+ return named ? `${base} · ${window.scope}` : base;
19
33
  }
20
34
  /** Quota cards for API-key providers already available through DSH itself. */
21
35
  export function ExternalUsageCards({ rpc, t }) {
22
36
  const [status, setStatus] = useState();
23
37
  const [statusError, setStatusError] = useState();
24
38
  const [usage, setUsage] = useState({});
39
+ const [observedAt, setObservedAt] = useState({});
25
40
  const [errors, setErrors] = useState({});
26
41
  const [loading, setLoading] = useState({});
27
42
  const refresh = useCallback(async (source) => {
28
43
  setLoading(prev => ({ ...prev, [source]: true }));
29
- setErrors(prev => ({ ...prev, [source]: undefined }));
30
44
  try {
31
45
  const value = await callSubscriptionsAuth(rpc, 'externalUsage', { source });
32
46
  setUsage(prev => ({ ...prev, [source]: value }));
47
+ // Prefer the server's observation time; a cached reading must not look fresh.
48
+ setObservedAt(prev => ({ ...prev, [source]: value.observedAt ?? Date.now() }));
49
+ setErrors(prev => ({ ...prev, [source]: undefined }));
33
50
  window.dispatchEvent(new Event(USAGE_BADGE_REFRESH_EVENT));
34
51
  }
35
52
  catch (error) {
@@ -61,12 +78,10 @@ export function ExternalUsageCards({ rpc, t }) {
61
78
  return _jsxs("div", { style: styles.card, children: [_jsxs("div", { style: styles.header, children: [_jsx("span", { style: { ...styles.dot, background: configured
62
79
  ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-label-dimmed)' } }), _jsx("span", { style: styles.name, children: name })] }), !(status === undefined && statusError !== undefined) && _jsx("p", { style: styles.status, children: status === undefined ? t('checking')
63
80
  : configured ? t('externalUsageConnected') : t('externalUsageNotConfigured') }), statusError !== undefined && _jsx("p", { style: styles.error, children: t('externalUsageUnavailable', { message: statusError }) }), status !== undefined && !configured && _jsx("p", { style: styles.status, children: t('externalUsageConfigure', { ref }) }), configured && _jsxs("div", { style: styles.account, children: [_jsxs("div", { style: styles.accountHeader, children: [_jsx("span", { style: styles.defaultStar, title: t('defaultBadge'), children: "\u2605" }), _jsx("span", { style: styles.accountName, children: t('externalUsageApiKey') })] }), _jsxs("div", { style: styles.usage, children: [_jsxs("div", { style: styles.usageHeader, children: [_jsx("span", { style: styles.usageTitle, children: t('usageTitle') }), snapshot?.plan !== undefined && _jsx("span", { style: styles.usagePlan, children: t('usagePlan', { plan: snapshot.plan }) }), _jsx("button", { type: "button", style: styles.usageRefresh, disabled: loading[id] === true, onClick: () => { void refresh(id); }, children: t('usageRefresh') })] }), loading[id] === true && snapshot === undefined && _jsx("p", { style: styles.status, children: t('usageLoading') }), errors[id] !== undefined && _jsx("p", { style: styles.error, children: t('usageError', { message: errors[id] }) }), snapshot?.windows?.length === 0 && _jsx("p", { style: styles.status, children: t('usageEmpty') }), snapshot?.windows?.map((window, index) => {
64
- const percent = Math.min(100, Math.max(0, window.usedPercent));
65
- const label = window.kind === 'session' ? t('usageSession')
66
- : window.kind === 'weekly' ? t('usageWeekly')
67
- : window.scope === 'Monthly' ? t('usageMonthly') : window.scope ?? t('usageWindow');
68
- return _jsxs("div", { style: styles.usageRow, children: [_jsxs("div", { style: styles.usageMeta, children: [_jsx("span", { children: label }), _jsxs("span", { children: [Math.round(percent), "%", window.resetsAt === undefined ? ''
69
- : ` · ${t('usageResets', { date: new Date(window.resetsAt).toLocaleString() })}`] })] }), _jsx("div", { style: styles.usageTrack, children: _jsx("div", { style: { ...styles.usageFill, width: `${percent}%`, background: barColor(percent) } }) })] }, index);
81
+ const percent = displayUsedPercent(window.usedPercent);
82
+ const label = externalUsageWindowLabel(t, window);
83
+ return _jsxs("div", { style: styles.usageRow, children: [_jsxs("div", { style: styles.usageMeta, children: [_jsx("span", { children: label }), _jsxs("span", { children: [percent === undefined ? t('usageMeterInvalid') : `${percent}%`, window.resetsAt === undefined ? ''
84
+ : ` · ${t('usageResets', { date: new Date(window.resetsAt).toLocaleString() })}`] })] }), _jsx(UsageMeter, { window: window, t: t, observedAt: observedAt[id], stale: errors[id] !== undefined || snapshot?.stale === true })] }, index);
70
85
  })] })] }), !configured && _jsx("div", { style: styles.actions, children: _jsx("button", { type: "button", style: styles.button, onClick: () => { void refreshStatus(); }, children: t('usageRefresh') }) })] }, id);
71
86
  }) });
72
87
  }
@@ -20,7 +20,11 @@ export interface SubscriptionUsageBadgeInjected {
20
20
  } | undefined>;
21
21
  }
22
22
  /** Props delivered by the slot outlet + inject + the locale seat. */
23
- export type SubscriptionUsageBadgeProps = PropsRuntime<'conversation.composer.dock'> & Partial<SubscriptionUsageBadgeInjected> & Partial<PropsLocale<'settings.subscriptions'>>;
23
+ export type SubscriptionUsageBadgeProps = PropsRuntime<'conversation.composer.dock'> & Partial<SubscriptionUsageBadgeInjected> & Partial<PropsLocale<'settings.subscriptions'>>
24
+ /** Override the rotation cadence (tests only; the slot never passes it). */
25
+ & {
26
+ rotationMs?: number;
27
+ };
24
28
  /** One logged-in account's usage windows, as listed in the expanded dialog. */
25
29
  export interface AccountUsageDisplay {
26
30
  /** Account key (the `usage` endpoint's `account` argument). */
@@ -31,6 +35,8 @@ export interface AccountUsageDisplay {
31
35
  /** The account direct routes serve; the collapsed pill reads this one. */
32
36
  isDefault: boolean;
33
37
  windows: UsageWindow[];
38
+ observedAt?: number | undefined;
39
+ stale?: boolean | undefined;
34
40
  }
35
41
  /** One provider's usage snapshot: every logged-in account that reports windows. */
36
42
  export interface ProviderUsageDisplay {
@@ -42,7 +48,7 @@ export interface ProviderUsageDisplay {
42
48
  /** The account the collapsed pill reads: the default one, else the first listed. */
43
49
  export declare function pillAccountOf(d: ProviderUsageDisplay): AccountUsageDisplay;
44
50
  /** Brand display names (short form for the compact badge). */
45
- export type BadgeProvider = SubscriptionProvider | 'cursor-subscription' | 'opencode-go' | 'kimi-coding';
51
+ export type BadgeProvider = SubscriptionProvider | 'cursor-subscription' | 'opencode-go' | 'kimi-coding' | 'minimax' | 'minimax-cn';
46
52
  export interface UsageRosterEntry {
47
53
  provider: BadgeProvider;
48
54
  account: AccountStatus;
@@ -96,6 +102,19 @@ type ModelSelection = {
96
102
  export declare function retainSubscriptionSelection(previous: ModelSelection | undefined, current: ModelSelection | undefined): ModelSelection | undefined;
97
103
  /** At most one provider; no known selection or no usage means no badge. */
98
104
  export declare function collapsedDisplays(displays: readonly ProviderUsageDisplay[], current: string | undefined): readonly ProviderUsageDisplay[];
105
+ /**
106
+ * `always` mode: the current model's provider while it reports usage,
107
+ * otherwise the rotation steps through the providers that do, in display
108
+ * order, wrapping around. The index is taken modulo the current provider
109
+ * count, so providers appearing or dropping out stay inside range; the
110
+ * sequence itself may shift when the roster changes. Unlike `recent`, the
111
+ * retained subscription does not pin the pill — that retention is exactly
112
+ * what this mode exists to escape.
113
+ * @param displays - providers with usage, in display order.
114
+ * @param current - the provider of the session's current model.
115
+ * @param step - rotation tick; consecutive ticks advance one provider.
116
+ */
117
+ export declare function rotatingDisplay(displays: readonly ProviderUsageDisplay[], current: string | undefined, step: number): readonly ProviderUsageDisplay[];
99
118
  /** Order for the expanded dialog: the current provider first, the rest in poll order. */
100
119
  export declare function expandedDisplays(displays: readonly ProviderUsageDisplay[], current: string | undefined): readonly ProviderUsageDisplay[];
101
120
  type Translate = (key: SubscriptionsKey, params?: Record<string, unknown>) => string;
@@ -105,12 +124,14 @@ type Translate = (key: SubscriptionsKey, params?: Record<string, unknown>) => st
105
124
  * every provider's accounts and their windows. Returns null when no data is
106
125
  * available.
107
126
  */
108
- export declare function SubscriptionUsageBadge({ rpc, currentModel, t }: SubscriptionUsageBadgeProps): import("react").JSX.Element;
127
+ export declare function SubscriptionUsageBadge({ rpc, currentModel, t, rotationMs }: SubscriptionUsageBadgeProps): import("react").JSX.Element;
109
128
  /** Preview each account independently; all remaining quotas stay accessible. */
110
- export declare function AccountWindows({ windows, model, provider, translate }: {
129
+ export declare function AccountWindows({ windows, model, provider, translate, observedAt, stale }: {
111
130
  windows: readonly UsageWindow[];
112
131
  model: string | undefined;
113
132
  provider?: BadgeProvider;
114
133
  translate: Translate;
134
+ observedAt?: number | undefined;
135
+ stale?: boolean | undefined;
115
136
  }): import("react").JSX.Element;
116
137
  export {};