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/README.md
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
# DSH
|
|
1
|
+
# DSH Subscriptions
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/dsh-subscriptions)
|
|
4
|
+
[](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
|
+

|
|
42
|
+
|
|
43
|
+
### Model selection
|
|
44
|
+
|
|
45
|
+
Logged-in providers join the session model picker with their live catalogs.
|
|
46
|
+
|
|
47
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
66
|
+
|
|
67
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
|
31
|
-
it need an explicit `dsh plugin allow-version` exemption.
|
|
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
|
|
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>
|
package/lib/client/CursorCard.js
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
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: [
|
|
111
|
-
: ` · ${t('usageResets', { date: new Date(window.resetsAt).toLocaleString() })}`] })] }), _jsx(
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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 =
|
|
65
|
-
const label = window
|
|
66
|
-
|
|
67
|
-
|
|
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 {};
|