dsh-subscriptions 0.1.1 → 0.1.2
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 +97 -4
- package/icon.svg +28 -0
- package/lib/client/CursorCard.js +15 -11
- package/lib/client/ExternalUsageCards.js +11 -11
- package/lib/client/SubscriptionUsageBadge.d.ts +6 -2
- package/lib/client/SubscriptionUsageBadge.js +56 -24
- package/lib/client/SubscriptionsSection.d.ts +7 -2
- package/lib/client/SubscriptionsSection.js +9 -13
- package/lib/client/UsageBadgeDisplaySetting.js +18 -1
- package/lib/client/UsageMeter.d.ts +8 -0
- package/lib/client/UsageMeter.js +51 -0
- package/lib/client/host-contract.d.ts +1 -1
- package/lib/client/host-contract.js +1 -0
- package/lib/client/locales.d.ts +48 -0
- package/lib/client/locales.js +48 -0
- package/lib/client/subscription-card-styles.js +0 -5
- 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 +29 -0
- package/lib/client/usage-pace.js +58 -0
- package/lib/client.js +396 -114
- package/lib/client.js.map +1 -1
- package/lib/index.js +225 -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 +3 -0
- package/lib/providers/minimax-usage.js +55 -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,74 @@ 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 **subscription 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.
|
|
76
|
+
|
|
77
|
+

|
|
78
|
+
|
|
79
|
+
Click the pill to expand every provider and account that reports usage —
|
|
80
|
+
the default account is starred, and the current provider is listed first.
|
|
81
|
+
A provider with no usage endpoint, such as GitHub Copilot, has no row, and
|
|
82
|
+
neither do accounts without a usage window. Antigravity previews only the
|
|
83
|
+
current model's windows; the other model windows stay in a closed
|
|
84
|
+
disclosure.
|
|
85
|
+
|
|
86
|
+

|
|
87
|
+
|
|
26
88
|
## Install
|
|
27
89
|
|
|
28
90
|
Current source is tested with DSH `0.1.7-rc.2`, `0.2.0-rc.1`, and
|
|
29
91
|
`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.
|
|
92
|
+
versions inside that range install but are unsupported, and versions
|
|
93
|
+
outside it need an explicit `dsh plugin allow-version` exemption.
|
|
94
|
+
`dsh plugin add` checks those ranges with `includePrerelease` semantics, so
|
|
95
|
+
the admitted set covers every host build from `0.1.7-rc.2` through the
|
|
96
|
+
whole `0.2.x` line, prereleases included, and blocks the `0.3.0` line. See
|
|
32
97
|
[docs/compatibility.md](docs/compatibility.md)
|
|
33
98
|
for the support window and [docs/testing.md](docs/testing.md) for the test
|
|
34
99
|
layers. With `dsh` available, install the plugin into the web profile from
|
|
@@ -74,7 +139,7 @@ spec (`#vX.Y.Z`); see the
|
|
|
74
139
|
|
|
75
140
|
## Verification and limits
|
|
76
141
|
|
|
77
|
-
CI builds, tests, and boots a packed tarball on
|
|
142
|
+
CI builds, tests, and boots a packed tarball on all supported DSH versions;
|
|
78
143
|
it does not run the GitHub source install's `prepare` step. Offline
|
|
79
144
|
provider tests check recorded API responses, so a passing CI run cannot prove
|
|
80
145
|
that a provider still accepts live sign-in or model requests. See
|
|
@@ -106,6 +171,34 @@ usage.
|
|
|
106
171
|
The plugin follows DSH network settings. Its former proxy configuration and
|
|
107
172
|
UI have been retired; an old plugin proxy config file is ignored.
|
|
108
173
|
|
|
174
|
+
## Usage coloring
|
|
175
|
+
|
|
176
|
+
**Settings → Subscriptions → Usage coloring** offers one browser-local dropdown:
|
|
177
|
+
|
|
178
|
+
- **Standard** (default): yellow when used quota leads elapsed time by at least 10 percentage points.
|
|
179
|
+
- **Relaxed**: yellow at a lead of at least 15 points.
|
|
180
|
+
- **Remaining quota only**: no yellow pace warnings.
|
|
181
|
+
|
|
182
|
+
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.
|
|
183
|
+
|
|
184
|
+
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.
|
|
185
|
+
|
|
186
|
+
Expired, failed-refresh or more-than-five-minute-old readings retain the last reported green/red color, without pace warnings or markers. Cached numbers remain visible, with an accessible stale label; refresh to confirm them. 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) for rationale and verification limits.
|
|
187
|
+
|
|
188
|
+
### MiniMax subscription usage
|
|
189
|
+
|
|
190
|
+
Configure `MINIMAX_API_KEY` for the global service or
|
|
191
|
+
`MINIMAX_CN_API_KEY` for China, using DSH's standard model-key configuration.
|
|
192
|
+
The plugin reuses those credential references; no separate subscription ref is
|
|
193
|
+
required. Quota reads require subscription keys, not `sk-api-*` pay-as-you-go keys. The plugin reads the
|
|
194
|
+
region's `/v1/token_plan/remains` endpoint and displays finite per-model quotas
|
|
195
|
+
with API-provided start/end times. It does not add model routing or OAuth login.
|
|
196
|
+
Unlimited, boosted-above-100% and unsupported quotas are omitted; if none remain,
|
|
197
|
+
the card reports unavailable rather than inventing a percentage. Explicit
|
|
198
|
+
remaining percentages take priority; older responses follow the official CLI's
|
|
199
|
+
remaining-count compatibility convention. No live-account response was used in
|
|
200
|
+
verification.
|
|
201
|
+
|
|
109
202
|
## Development
|
|
110
203
|
|
|
111
204
|
```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
|
}
|
|
@@ -3,33 +3,33 @@ 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)';
|
|
19
|
-
}
|
|
20
17
|
/** Quota cards for API-key providers already available through DSH itself. */
|
|
21
18
|
export function ExternalUsageCards({ rpc, t }) {
|
|
22
19
|
const [status, setStatus] = useState();
|
|
23
20
|
const [statusError, setStatusError] = useState();
|
|
24
21
|
const [usage, setUsage] = useState({});
|
|
22
|
+
const [observedAt, setObservedAt] = useState({});
|
|
25
23
|
const [errors, setErrors] = useState({});
|
|
26
24
|
const [loading, setLoading] = useState({});
|
|
27
25
|
const refresh = useCallback(async (source) => {
|
|
28
26
|
setLoading(prev => ({ ...prev, [source]: true }));
|
|
29
|
-
setErrors(prev => ({ ...prev, [source]: undefined }));
|
|
30
27
|
try {
|
|
31
28
|
const value = await callSubscriptionsAuth(rpc, 'externalUsage', { source });
|
|
32
29
|
setUsage(prev => ({ ...prev, [source]: value }));
|
|
30
|
+
// Prefer the server's observation time; a cached reading must not look fresh.
|
|
31
|
+
setObservedAt(prev => ({ ...prev, [source]: value.observedAt ?? Date.now() }));
|
|
32
|
+
setErrors(prev => ({ ...prev, [source]: undefined }));
|
|
33
33
|
window.dispatchEvent(new Event(USAGE_BADGE_REFRESH_EVENT));
|
|
34
34
|
}
|
|
35
35
|
catch (error) {
|
|
@@ -61,12 +61,12 @@ export function ExternalUsageCards({ rpc, t }) {
|
|
|
61
61
|
return _jsxs("div", { style: styles.card, children: [_jsxs("div", { style: styles.header, children: [_jsx("span", { style: { ...styles.dot, background: configured
|
|
62
62
|
? '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
63
|
: 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 =
|
|
64
|
+
const percent = displayUsedPercent(window.usedPercent);
|
|
65
65
|
const label = window.kind === 'session' ? t('usageSession')
|
|
66
66
|
: window.kind === 'weekly' ? t('usageWeekly')
|
|
67
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: [
|
|
69
|
-
: ` · ${t('usageResets', { date: new Date(window.resetsAt).toLocaleString() })}`] })] }), _jsx(
|
|
68
|
+
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 ? ''
|
|
69
|
+
: ` · ${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
70
|
})] })] }), !configured && _jsx("div", { style: styles.actions, children: _jsx("button", { type: "button", style: styles.button, onClick: () => { void refreshStatus(); }, children: t('usageRefresh') }) })] }, id);
|
|
71
71
|
}) });
|
|
72
72
|
}
|
|
@@ -31,6 +31,8 @@ export interface AccountUsageDisplay {
|
|
|
31
31
|
/** The account direct routes serve; the collapsed pill reads this one. */
|
|
32
32
|
isDefault: boolean;
|
|
33
33
|
windows: UsageWindow[];
|
|
34
|
+
observedAt?: number | undefined;
|
|
35
|
+
stale?: boolean | undefined;
|
|
34
36
|
}
|
|
35
37
|
/** One provider's usage snapshot: every logged-in account that reports windows. */
|
|
36
38
|
export interface ProviderUsageDisplay {
|
|
@@ -42,7 +44,7 @@ export interface ProviderUsageDisplay {
|
|
|
42
44
|
/** The account the collapsed pill reads: the default one, else the first listed. */
|
|
43
45
|
export declare function pillAccountOf(d: ProviderUsageDisplay): AccountUsageDisplay;
|
|
44
46
|
/** Brand display names (short form for the compact badge). */
|
|
45
|
-
export type BadgeProvider = SubscriptionProvider | 'cursor-subscription' | 'opencode-go' | 'kimi-coding';
|
|
47
|
+
export type BadgeProvider = SubscriptionProvider | 'cursor-subscription' | 'opencode-go' | 'kimi-coding' | 'minimax' | 'minimax-cn';
|
|
46
48
|
export interface UsageRosterEntry {
|
|
47
49
|
provider: BadgeProvider;
|
|
48
50
|
account: AccountStatus;
|
|
@@ -107,10 +109,12 @@ type Translate = (key: SubscriptionsKey, params?: Record<string, unknown>) => st
|
|
|
107
109
|
*/
|
|
108
110
|
export declare function SubscriptionUsageBadge({ rpc, currentModel, t }: SubscriptionUsageBadgeProps): import("react").JSX.Element;
|
|
109
111
|
/** Preview each account independently; all remaining quotas stay accessible. */
|
|
110
|
-
export declare function AccountWindows({ windows, model, provider, translate }: {
|
|
112
|
+
export declare function AccountWindows({ windows, model, provider, translate, observedAt, stale }: {
|
|
111
113
|
windows: readonly UsageWindow[];
|
|
112
114
|
model: string | undefined;
|
|
113
115
|
provider?: BadgeProvider;
|
|
114
116
|
translate: Translate;
|
|
117
|
+
observedAt?: number | undefined;
|
|
118
|
+
stale?: boolean | undefined;
|
|
115
119
|
}): import("react").JSX.Element;
|
|
116
120
|
export {};
|
|
@@ -18,15 +18,18 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
18
18
|
* The collapsed pill reads only the default account — the same account
|
|
19
19
|
* direct (non-pool) routes serve — so it stays one short segment even for a
|
|
20
20
|
* provider with several accounts connected; the dialog shows them all.
|
|
21
|
-
* Every color resolves through a `--dsw-*` design token
|
|
22
|
-
*
|
|
21
|
+
* Every color resolves through a `--dsw-*` design token, the dialog wears the
|
|
22
|
+
* host's menu material (its translucent fill plus its backdrop blur), and
|
|
23
|
+
* every user-visible string goes through the locale `t` of the
|
|
23
24
|
* 'settings.subscriptions' namespace.
|
|
24
25
|
*/
|
|
25
26
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
26
27
|
import { createPortal } from 'react-dom';
|
|
27
28
|
import * as primitives from '@deepseek-ai/dsh-client-ui-primitives';
|
|
28
29
|
import { useAnchoredPosition, useDismissOnOutsidePointer } from '@deepseek-ai/dsh-client-ui-primitives';
|
|
29
|
-
import { callSubscriptionsAuth
|
|
30
|
+
import { callSubscriptionsAuth } from './SubscriptionsSection.js';
|
|
31
|
+
import { UsageMeter } from './UsageMeter.js';
|
|
32
|
+
import { displayUsedPercent } from './usage-pace.js';
|
|
30
33
|
import { en } from './locales.js';
|
|
31
34
|
import { hostIcon } from './host-icons.js';
|
|
32
35
|
import { USAGE_BADGE_REFRESH_EVENT, useUsageBadgeMode } from './usage-badge-preferences.js';
|
|
@@ -56,6 +59,8 @@ const PROVIDER_NAMES = {
|
|
|
56
59
|
'cursor-subscription': 'Cursor',
|
|
57
60
|
'opencode-go': 'OpenCode Go',
|
|
58
61
|
'kimi-coding': 'Kimi Code',
|
|
62
|
+
'minimax': 'MiniMax',
|
|
63
|
+
'minimax-cn': 'MiniMax CN',
|
|
59
64
|
};
|
|
60
65
|
/** Key of one account's cached windows: `provider:accountKey`. */
|
|
61
66
|
function usageKeyOf(provider, account) {
|
|
@@ -119,7 +124,7 @@ export async function loadBadgeRoster(rpc) {
|
|
|
119
124
|
}
|
|
120
125
|
if (external.status === 'fulfilled') {
|
|
121
126
|
for (const [source, provider] of [
|
|
122
|
-
['opencode-go', 'opencode-go'], ['kimi-code', 'kimi-coding'],
|
|
127
|
+
['opencode-go', 'opencode-go'], ['kimi-code', 'kimi-coding'], ['minimax', 'minimax'], ['minimax-cn', 'minimax-cn'],
|
|
123
128
|
]) {
|
|
124
129
|
refreshed.add(provider);
|
|
125
130
|
if (external.value[source]?.configured)
|
|
@@ -133,7 +138,7 @@ export async function loadBadgeRoster(rpc) {
|
|
|
133
138
|
export async function usageOf(rpc, { provider, account }) {
|
|
134
139
|
if (provider === 'cursor-subscription')
|
|
135
140
|
return callSubscriptionsAuth(rpc, 'cursorUsage', {});
|
|
136
|
-
if (provider === 'opencode-go' || provider === 'kimi-coding') {
|
|
141
|
+
if (provider === 'opencode-go' || provider === 'kimi-coding' || provider === 'minimax' || provider === 'minimax-cn') {
|
|
137
142
|
return callSubscriptionsAuth(rpc, 'externalUsage', { source: provider === 'kimi-coding' ? 'kimi-code' : provider });
|
|
138
143
|
}
|
|
139
144
|
return callSubscriptionsAuth(rpc, 'usage', { provider, account: account.key });
|
|
@@ -160,7 +165,7 @@ export function createCurrentModelReader(models, sessionId) {
|
|
|
160
165
|
* Falls back to the scope/kind abbreviation when no reset time is known.
|
|
161
166
|
*/
|
|
162
167
|
export function windowLabel(w) {
|
|
163
|
-
if (w.resetsAt === undefined) {
|
|
168
|
+
if (w.resetsAt === undefined || !Number.isFinite(w.resetsAt)) {
|
|
164
169
|
if (w.scope !== undefined && w.scope !== '')
|
|
165
170
|
return w.scope;
|
|
166
171
|
switch (w.kind) {
|
|
@@ -179,9 +184,13 @@ export function windowLabel(w) {
|
|
|
179
184
|
return `${hours}h${minutes % 60}m`;
|
|
180
185
|
return `${Math.max(1, minutes)}m`;
|
|
181
186
|
}
|
|
182
|
-
/**
|
|
183
|
-
|
|
184
|
-
|
|
187
|
+
/**
|
|
188
|
+
* One compact segment piece (`6d1h 25%`); an unusable provider percentage
|
|
189
|
+
* renders as the no-data dash, consistent with the meter's unavailable state.
|
|
190
|
+
*/
|
|
191
|
+
function windowSegment(label, w) {
|
|
192
|
+
const percent = displayUsedPercent(w.usedPercent);
|
|
193
|
+
return percent === undefined ? `${label} —` : `${label} ${percent}%`;
|
|
185
194
|
}
|
|
186
195
|
/** Keep model quotas separate: matching percentages do not imply a shared pool. */
|
|
187
196
|
export function prioritizeWindows(windows, model) {
|
|
@@ -210,10 +219,10 @@ export function compactSegment(d, model, t = fallbackTranslate) {
|
|
|
210
219
|
count: new Set(windows.map(w => w.scope).filter(Boolean)).size,
|
|
211
220
|
})}`;
|
|
212
221
|
}
|
|
213
|
-
const parts = matching.slice(0, 2).map(w =>
|
|
222
|
+
const parts = matching.slice(0, 2).map(w => windowSegment(w.kind === 'weekly' ? t('usageWeekly') : t('usageWindow'), w));
|
|
214
223
|
return `${d.name} ${parts.join(' · ')}`;
|
|
215
224
|
}
|
|
216
|
-
const parts = windows.slice(0, 2).map(w =>
|
|
225
|
+
const parts = windows.slice(0, 2).map(w => windowSegment(windowLabel(w), w));
|
|
217
226
|
if (windows.length > 2)
|
|
218
227
|
parts.push(`+${windows.length - 2}`);
|
|
219
228
|
return `${d.name} ${parts.join(' · ')}`;
|
|
@@ -287,8 +296,10 @@ export function SubscriptionUsageBadge({ rpc, currentModel, t }) {
|
|
|
287
296
|
// across a failed poll (e.g. a 429 during the server's own negative-cache
|
|
288
297
|
// cooldown) so a row doesn't flicker away — it only disappears once the
|
|
289
298
|
// account actually logs out or a fetch succeeds but reports the window as
|
|
290
|
-
// unsupported.
|
|
299
|
+
// unsupported. Observation metadata (freshness) shares this lifecycle: an
|
|
300
|
+
// entry is dropped wherever its windows are dropped.
|
|
291
301
|
const lastKnownRef = useRef(new Map());
|
|
302
|
+
const observationsRef = useRef(new Map());
|
|
292
303
|
const refresh = useCallback(async () => {
|
|
293
304
|
if (rpc === undefined || inflightRef.current)
|
|
294
305
|
return;
|
|
@@ -302,8 +313,10 @@ export function SubscriptionUsageBadge({ rpc, currentModel, t }) {
|
|
|
302
313
|
// real signal, unlike a fetch failure.
|
|
303
314
|
const live = new Set(roster.map(({ provider, account }) => usageKeyOf(provider, account)));
|
|
304
315
|
for (const key of lastKnown.keys()) {
|
|
305
|
-
if (refreshed.has(key.split(':', 1)[0]) && !live.has(key))
|
|
316
|
+
if (refreshed.has(key.split(':', 1)[0]) && !live.has(key)) {
|
|
306
317
|
lastKnown.delete(key);
|
|
318
|
+
observationsRef.current.delete(key);
|
|
319
|
+
}
|
|
307
320
|
}
|
|
308
321
|
const results = await Promise.allSettled(roster.map(async ({ provider, account }) => {
|
|
309
322
|
const usage = await usageOf(rpc, { provider, account });
|
|
@@ -311,6 +324,10 @@ export function SubscriptionUsageBadge({ rpc, currentModel, t }) {
|
|
|
311
324
|
}));
|
|
312
325
|
if (!mountedRef.current)
|
|
313
326
|
return;
|
|
327
|
+
for (const { provider, account } of roster) {
|
|
328
|
+
const key = usageKeyOf(provider, account);
|
|
329
|
+
observationsRef.current.set(key, { ...observationsRef.current.get(key), stale: true });
|
|
330
|
+
}
|
|
314
331
|
const plans = new Map();
|
|
315
332
|
for (const r of results) {
|
|
316
333
|
if (r.status !== 'fulfilled')
|
|
@@ -321,17 +338,29 @@ export function SubscriptionUsageBadge({ rpc, currentModel, t }) {
|
|
|
321
338
|
plans.set(key, usage.plan);
|
|
322
339
|
if (!usage.supported || !usage.windows || usage.windows.length === 0) {
|
|
323
340
|
lastKnown.delete(key);
|
|
341
|
+
observationsRef.current.delete(key);
|
|
324
342
|
continue;
|
|
325
343
|
}
|
|
326
344
|
lastKnown.set(key, usage.windows);
|
|
345
|
+
observationsRef.current.set(key, { observedAt: usage.observedAt ?? Date.now(), stale: usage.stale === true });
|
|
327
346
|
}
|
|
328
347
|
setDisplays(previous => [
|
|
329
|
-
...groupUsageDisplays(roster, lastKnown, plans),
|
|
330
|
-
|
|
348
|
+
...groupUsageDisplays(roster, lastKnown, plans).map(display => ({ ...display,
|
|
349
|
+
accounts: display.accounts.map(account => ({ ...account,
|
|
350
|
+
...observationsRef.current.get(`${display.provider}:${account.key}`),
|
|
351
|
+
})),
|
|
352
|
+
})),
|
|
353
|
+
...previous.filter(display => !refreshed.has(display.provider)).map(display => ({ ...display,
|
|
354
|
+
accounts: display.accounts.map(account => ({ ...account, stale: true })),
|
|
355
|
+
})),
|
|
331
356
|
]);
|
|
332
357
|
}
|
|
333
358
|
catch {
|
|
334
|
-
//
|
|
359
|
+
// Keep cached numbers, but never present failed observations as current.
|
|
360
|
+
if (mountedRef.current)
|
|
361
|
+
setDisplays(previous => previous.map(display => ({ ...display,
|
|
362
|
+
accounts: display.accounts.map(account => ({ ...account, stale: true })),
|
|
363
|
+
})));
|
|
335
364
|
}
|
|
336
365
|
finally {
|
|
337
366
|
inflightRef.current = false;
|
|
@@ -429,7 +458,7 @@ export function SubscriptionUsageBadge({ rpc, currentModel, t }) {
|
|
|
429
458
|
if (next)
|
|
430
459
|
void refresh();
|
|
431
460
|
};
|
|
432
|
-
const pill = (_jsxs("span", { ref: rootRef, style: styles.anchor, children: [_jsxs("button", { type: "button", style: { ...styles.pill, ...(hover || open ? styles.pillActive : {}) }, "aria-haspopup": "dialog", "aria-expanded": open, "aria-label": `${title} · ${label}`, title: title, onMouseEnter: () => { setHover(true); }, onMouseLeave: () => { setHover(false); }, onClick: toggle, children: [_jsx(UsageBadgeIcon, {}), _jsx("span", { style: styles.label, children: label })] }), open && createPortal(_jsxs("div", { ref: panelRef, role: "dialog", "aria-label": title, style: { ...styles.panel, ...(pos ?? MEASURE_STYLE) }, children: [_jsx("div", { style: styles.title, children: _jsxs("span", { style: styles.titleLabel, children: [_jsx(UsageBadgeIcon, {}), title] }) }), _jsx("div", { style: styles.titleRule, "aria-hidden": true }), expanded.map((d, index) => (_jsxs("section", { style: index === 0 ? undefined : styles.section, children: [_jsxs("div", { style: styles.providerRow, children: [_jsxs("span", { style: styles.providerName, children: [d.name, d.provider === current && _jsx("span", { style: styles.currentTag, children: translate('usageBadgeCurrent') })] }), d.accounts.length === 1 && _jsx(AccountMeta, { account: d.accounts[0], translate: translate })] }), d.accounts.map((account, accountIndex) => (_jsxs("div", { style: accountIndex === 0 ? undefined : styles.accountBlock, children: [d.accounts.length > 1 && (_jsx("div", { style: styles.accountRow, children: _jsx(AccountMeta, { account: account, translate: translate }) })), _jsx(AccountWindows, { windows: account.windows, model: d.provider === badgeSelection?.provider ? badgeSelection.model : undefined, provider: d.provider, translate: translate }, `${d.provider}:${badgeSelection?.model ?? ''}`)] }, account.key)))] }, d.provider)))] }), document.body)] }));
|
|
461
|
+
const pill = (_jsxs("span", { ref: rootRef, style: styles.anchor, children: [_jsxs("button", { type: "button", style: { ...styles.pill, ...(hover || open ? styles.pillActive : {}) }, "aria-haspopup": "dialog", "aria-expanded": open, "aria-label": `${title} · ${label}`, title: title, onMouseEnter: () => { setHover(true); }, onMouseLeave: () => { setHover(false); }, onClick: toggle, children: [_jsx(UsageBadgeIcon, {}), _jsx("span", { style: styles.label, children: label })] }), open && createPortal(_jsxs("div", { ref: panelRef, role: "dialog", "aria-label": title, style: { ...styles.panel, ...(pos ?? MEASURE_STYLE) }, children: [_jsx("div", { style: styles.title, children: _jsxs("span", { style: styles.titleLabel, children: [_jsx(UsageBadgeIcon, {}), title] }) }), _jsx("div", { style: styles.titleRule, "aria-hidden": true }), expanded.map((d, index) => (_jsxs("section", { style: index === 0 ? undefined : styles.section, children: [_jsxs("div", { style: styles.providerRow, children: [_jsxs("span", { style: styles.providerName, children: [d.name, d.provider === current && _jsx("span", { style: styles.currentTag, children: translate('usageBadgeCurrent') })] }), d.accounts.length === 1 && _jsx(AccountMeta, { account: d.accounts[0], translate: translate })] }), d.accounts.map((account, accountIndex) => (_jsxs("div", { style: accountIndex === 0 ? undefined : styles.accountBlock, children: [d.accounts.length > 1 && (_jsx("div", { style: styles.accountRow, children: _jsx(AccountMeta, { account: account, translate: translate }) })), _jsx(AccountWindows, { windows: account.windows, observedAt: account.observedAt, stale: account.stale, model: d.provider === badgeSelection?.provider ? badgeSelection.model : undefined, provider: d.provider, translate: translate }, `${d.provider}:${badgeSelection?.model ?? ''}`)] }, account.key)))] }, d.provider)))] }), document.body)] }));
|
|
433
462
|
return (_jsxs(_Fragment, { children: [seat, statsRow !== null && statsRow.isConnected ? createPortal(pill, statsRow) : pill] }));
|
|
434
463
|
}
|
|
435
464
|
/**
|
|
@@ -457,15 +486,15 @@ function AccountMeta({ account, translate }) {
|
|
|
457
486
|
return (_jsxs("span", { style: styles.providerMeta, title: account.account, children: [account.isDefault && _jsx("span", { style: styles.defaultStar, "aria-label": "default", children: "\u2605 " }), parts.join(' · ')] }));
|
|
458
487
|
}
|
|
459
488
|
/** Preview each account independently; all remaining quotas stay accessible. */
|
|
460
|
-
export function AccountWindows({ windows, model, provider, translate }) {
|
|
489
|
+
export function AccountWindows({ windows, model, provider, translate, observedAt, stale }) {
|
|
461
490
|
const { shown, hidden } = previewWindows(windows, model, provider);
|
|
462
|
-
const rows = (items) => (_jsx("dl", { style: styles.details, children: items.map((w, i) => (_jsx(WindowRow, { label: `${usageWindowLabel(translate, w)}${model !== undefined && w.scope === model ? ` · ${translate('usageBadgeCurrent')}` : ''}`, window: w }, i))) }));
|
|
491
|
+
const rows = (items) => (_jsx("dl", { style: styles.details, children: items.map((w, i) => (_jsx(WindowRow, { label: `${usageWindowLabel(translate, w)}${model !== undefined && w.scope === model ? ` · ${translate('usageBadgeCurrent')}` : ''}`, window: w, t: translate, observedAt: observedAt, stale: stale }, i))) }));
|
|
463
492
|
return _jsxs(_Fragment, { children: [rows(shown), hidden.length > 0 && _jsxs("details", { style: styles.moreWindows, children: [_jsx("summary", { style: styles.moreSummary, children: translate('usageBadgeMoreWindows', { count: hidden.length }) }), rows(hidden)] })] });
|
|
464
493
|
}
|
|
465
494
|
/** One `dt`/`dd` pair: window name → `25% · 6d1h`, with the bar underneath. */
|
|
466
|
-
function WindowRow({ label, window: w }) {
|
|
467
|
-
const percent =
|
|
468
|
-
return (_jsxs(_Fragment, { children: [_jsx("dt", { style: styles.dt, children: label }), _jsxs("dd", { style: styles.dd, children: [percent
|
|
495
|
+
function WindowRow({ label, window: w, t, observedAt, stale }) {
|
|
496
|
+
const percent = displayUsedPercent(w.usedPercent);
|
|
497
|
+
return (_jsxs(_Fragment, { children: [_jsx("dt", { style: styles.dt, children: label }), _jsxs("dd", { style: styles.dd, children: [percent === undefined ? t('usageMeterInvalid') : `${percent}%`, w.resetsAt !== undefined && Number.isFinite(w.resetsAt) && _jsxs("span", { style: styles.reset, children: [" \u00B7 ", windowLabel(w)] })] }), _jsx(UsageMeter, { window: w, t: t, style: styles.bar, observedAt: observedAt, stale: stale })] }));
|
|
469
498
|
}
|
|
470
499
|
/**
|
|
471
500
|
* Unplaced portal panel: hidden but laid out so the clamp measures real
|
|
@@ -490,10 +519,14 @@ const styles = {
|
|
|
490
519
|
color: 'var(--dsw-alias-label-secondary)',
|
|
491
520
|
},
|
|
492
521
|
label: { textOverflow: 'ellipsis', minWidth: 0, overflow: 'hidden' },
|
|
493
|
-
// Mirrors the host stat-dialog panel.
|
|
522
|
+
// Mirrors the host stat-dialog panel. The menu fill is translucent by
|
|
523
|
+
// design and only the host's backdrop blur keeps it readable as a surface:
|
|
524
|
+
// without the blur the transcript behind the dialog stays sharp and shows
|
|
525
|
+
// through the fill.
|
|
494
526
|
panel: {
|
|
495
527
|
position: 'fixed', zIndex: 1100, boxSizing: 'border-box',
|
|
496
528
|
background: 'var(--dsw-specific-menu)',
|
|
529
|
+
backdropFilter: 'var(--dsw-menu-backdrop-filter)',
|
|
497
530
|
width: 'max-content', minWidth: 'min(300px, 100vw - 24px)', maxWidth: 'min(440px, 100vw - 24px)',
|
|
498
531
|
maxHeight: 'min(560px, 100dvh - 24px)', overflowY: 'auto', overscrollBehavior: 'contain',
|
|
499
532
|
boxShadow: 'var(--dsw-elevation-prominent)',
|
|
@@ -539,5 +572,4 @@ const styles = {
|
|
|
539
572
|
gridColumn: '1 / -1', height: 4, borderRadius: 2, overflow: 'hidden',
|
|
540
573
|
background: 'var(--dsw-alias-border-l2)', marginBottom: 2,
|
|
541
574
|
},
|
|
542
|
-
barFill: { height: '100%', borderRadius: 2 },
|
|
543
575
|
};
|
|
@@ -30,9 +30,14 @@ export interface UsageWindow {
|
|
|
30
30
|
scope?: string;
|
|
31
31
|
usedPercent: number;
|
|
32
32
|
resetsAt?: number;
|
|
33
|
+
startsAt?: number;
|
|
34
|
+
windowDurationMs?: number;
|
|
35
|
+
fixedWindow?: boolean;
|
|
33
36
|
}
|
|
34
37
|
/** `usage` endpoint value: the node half owns this shape. */
|
|
35
38
|
export interface ProviderUsage {
|
|
39
|
+
observedAt?: number;
|
|
40
|
+
stale?: boolean;
|
|
36
41
|
supported: boolean;
|
|
37
42
|
windows?: UsageWindow[];
|
|
38
43
|
plan?: string;
|
|
@@ -73,8 +78,8 @@ export type SubscriptionsSectionProps = Partial<SubscriptionsSectionInjected>;
|
|
|
73
78
|
* fallback, never as a plan limit.
|
|
74
79
|
*/
|
|
75
80
|
export declare function clientVersionText(t: SubscriptionsSectionInjected['t'], clientVersion: ProviderStatus['clientVersion']): string;
|
|
76
|
-
/**
|
|
77
|
-
export
|
|
81
|
+
/** Meter fill color lives with the pace logic: fresh 90%+ is red, pace presets add a yellow lead threshold. */
|
|
82
|
+
export { usageBarColor } from './usage-pace.js';
|
|
78
83
|
/** What one provider's collapsible default-effort section renders. */
|
|
79
84
|
export interface ModelDefaultsView {
|
|
80
85
|
/** Models with reasoning levels, after the name filter — one row each. */
|