dsh-plugin-subscriptions 0.1.2 → 0.2.0

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 CHANGED
@@ -28,6 +28,8 @@ The `image_generate` tool renders its result inline in the conversation:
28
28
 
29
29
  Only logged-in providers appear in the session model picker; the lists above refresh on login/logout. Vision-capable models declare `['text', 'image']` input modalities, and image content is translated to each provider's wire format.
30
30
 
31
+ Logged-in cards also show **subscription usage** — per rate-limit window (5-hour session, weekly, and per-model weekly where the plan has one) with the used percentage, a progress bar, and the reset time, plus a Refresh button. Codex usage comes from `chatgpt.com/backend-api/wham/usage` (also reports the plan), Claude usage from `api.anthropic.com/api/oauth/usage`, and Grok usage from the Grok Build CLI proxy's `cli-chat-proxy.grok.com/v1/billing` (the source of the CLI's `/usage` panel; reports the shared weekly pool and the subscription tier).
32
+
31
33
  Also included, registered when the matching provider is enabled:
32
34
 
33
35
  - **`x_search`** tool (Grok) — xAI's hosted X search, returning `{ answer, citations }`.
package/README.zh.md CHANGED
@@ -28,6 +28,8 @@
28
28
 
29
29
  只有已登录的 provider 才会出现在会话模型选择器里;登录/退出后列表自动刷新。支持视觉的模型会声明 `['text', 'image']` 输入模态,图片内容会被翻译成各 provider 的 wire 格式。
30
30
 
31
+ 已登录的卡片还会显示**订阅用量**——按限额窗口(5 小时会话窗、每周窗,以及计划包含的按模型每周窗)展示已用百分比、进度条和重置时间,并带刷新按钮。Codex 用量来自 `chatgpt.com/backend-api/wham/usage`(同时报告计划类型),Claude 用量来自 `api.anthropic.com/api/oauth/usage`,Grok 用量来自 Grok Build CLI 代理的 `cli-chat-proxy.grok.com/v1/billing`(即 CLI `/usage` 面板的数据源,报告共享每周额度和订阅档位)。
32
+
31
33
  随 provider 启用自动注册的工具:
32
34
 
33
35
  - **`x_search`**(Grok)—— xAI 托管的 X 搜索,返回 `{ answer, citations }`。
package/lib/auth/rpc.d.ts CHANGED
@@ -7,6 +7,7 @@
7
7
  import type { Context } from '@deepseek-ai/cordis';
8
8
  import type { ImageAttachmentRef } from '@deepseek-ai/dsh-attachment';
9
9
  import { type ProviderId } from './store.js';
10
+ import type { ProviderUsage } from '../providers/common.js';
10
11
  /** The RPC channel this plugin registers on the host connection. */
11
12
  export declare const SUBSCRIPTIONS_AUTH_CHANNEL = "/subscriptions-auth";
12
13
  /** Decoded image bytes returned by the `image` endpoint. */
@@ -48,6 +49,13 @@ export interface AuthController {
48
49
  cancel(provider: ProviderId): Promise<void>;
49
50
  /** Delete the stored session. */
50
51
  logout(provider: ProviderId): Promise<void>;
52
+ /**
53
+ * Current subscription usage of one provider.
54
+ * @param signal - caller cancellation from the RPC transport.
55
+ * @returns `{ supported: false }` when the provider has no usage endpoint.
56
+ * @throws when logged out or the usage lookup fails.
57
+ */
58
+ usage(provider: ProviderId, signal: AbortSignal): Promise<ProviderUsage>;
51
59
  /**
52
60
  * Read one image attachment's bytes for inline display.
53
61
  * @param ref - the full durable reference (`readImage` verifies against it).
package/lib/auth/rpc.js CHANGED
@@ -91,6 +91,8 @@ async function dispatch(controller, endpoint, payload, signal) {
91
91
  case 'logout':
92
92
  await controller.logout(readProvider(payload));
93
93
  return ok({ ok: true });
94
+ case 'usage':
95
+ return ok(await controller.usage(readProvider(payload), signal));
94
96
  case 'image':
95
97
  return ok(await controller.readImage(readImageRef(payload), signal));
96
98
  default:
@@ -10,6 +10,19 @@ export interface ProviderStatus {
10
10
  account?: string;
11
11
  detail?: string;
12
12
  }
13
+ /** One rate-limit window as answered by the `usage` endpoint. */
14
+ export interface UsageWindow {
15
+ kind: 'session' | 'weekly' | 'other';
16
+ scope?: string;
17
+ usedPercent: number;
18
+ resetsAt?: number;
19
+ }
20
+ /** `usage` endpoint value: the node half owns this shape. */
21
+ export interface ProviderUsage {
22
+ supported: boolean;
23
+ windows?: UsageWindow[];
24
+ plan?: string;
25
+ }
13
26
  /** Injected dependencies of {@link SubscriptionsSection} (slot `inject`). */
14
27
  export interface SubscriptionsSectionInjected {
15
28
  /** Generic logical-RPC caller over the Connection transport. */
@@ -89,6 +89,30 @@ const styles = {
89
89
  color: 'var(--dsw-alias-label-primary)', font: 'inherit', fontSize: 12, lineHeight: '18px',
90
90
  cursor: 'pointer',
91
91
  },
92
+ usage: {
93
+ display: 'flex', flexDirection: 'column', gap: 6, marginTop: 4,
94
+ borderTop: '1px solid var(--dsw-alias-border-l2)', paddingTop: 8,
95
+ },
96
+ usageHeader: { display: 'flex', alignItems: 'center', gap: 8 },
97
+ usageTitle: { fontSize: 12, lineHeight: '18px', fontWeight: 500, color: 'var(--dsw-alias-label-secondary)' },
98
+ usagePlan: { fontSize: 12, lineHeight: '18px', color: 'var(--dsw-alias-label-tertiary)' },
99
+ usageRefresh: {
100
+ boxSizing: 'border-box', display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
101
+ height: 22, padding: '0 8px', borderRadius: 11, marginLeft: 'auto',
102
+ border: '1px solid var(--dsw-alias-border-l2)', background: 'transparent',
103
+ color: 'var(--dsw-alias-label-secondary)', font: 'inherit', fontSize: 12, lineHeight: '18px',
104
+ cursor: 'pointer',
105
+ },
106
+ usageRow: { display: 'flex', flexDirection: 'column', gap: 3 },
107
+ usageMeta: {
108
+ display: 'flex', justifyContent: 'space-between', gap: 8,
109
+ fontSize: 12, lineHeight: '18px', color: 'var(--dsw-alias-label-tertiary)',
110
+ },
111
+ usageTrack: {
112
+ height: 6, borderRadius: 3, overflow: 'hidden',
113
+ background: 'var(--dsw-alias-bg-layer-1)', border: '1px solid var(--dsw-alias-border-l2)',
114
+ },
115
+ usageFill: { height: '100%', borderRadius: 3 },
92
116
  manual: { marginTop: 4, fontSize: 12, lineHeight: '18px', color: 'var(--dsw-alias-label-secondary)' },
93
117
  manualRow: { display: 'flex', gap: 8, marginTop: 6 },
94
118
  manualInput: {
@@ -133,6 +157,26 @@ function statusText(t, status) {
133
157
  }
134
158
  return t('notLoggedIn');
135
159
  }
160
+ /**
161
+ * Localized label of one usage window (kind, plus the model scope when named).
162
+ * @param t - section translate.
163
+ * @param window - the reported window.
164
+ * @returns e.g. "5-hour window" or "Weekly · Opus".
165
+ */
166
+ function usageWindowLabel(t, window) {
167
+ const base = window.kind === 'session'
168
+ ? t('usageSession')
169
+ : window.kind === 'weekly' ? t('usageWeekly') : t('usageWindow');
170
+ return window.scope !== undefined && window.scope !== '' ? `${base} · ${window.scope}` : base;
171
+ }
172
+ /** Bar fill color: success normally, warn from 80%, error from 95%. */
173
+ function usageBarColor(usedPercent) {
174
+ if (usedPercent >= 95)
175
+ return 'var(--dsw-alias-state-error-primary)';
176
+ if (usedPercent >= 80)
177
+ return 'var(--dsw-alias-state-warn-label)';
178
+ return 'var(--dsw-alias-state-success-primary)';
179
+ }
136
180
  /**
137
181
  * The Subscriptions settings page component.
138
182
  * @param props - the slot inject face ({@link SubscriptionsSectionInjected}).
@@ -146,8 +190,13 @@ export function SubscriptionsSection(props) {
146
190
  const [manualDrafts, setManualDrafts] = useState({
147
191
  codex: '', claude: '', grok: '',
148
192
  });
193
+ const [usages, setUsages] = useState({});
194
+ const [usageErrors, setUsageErrors] = useState({});
195
+ const [usageLoading, setUsageLoading] = useState({});
149
196
  const mountedRef = useRef(true);
150
197
  const pollersRef = useRef(new Map());
198
+ /** Providers with a `usage` call in flight; guards the auto-fetch effect against re-entry. */
199
+ const usageInflightRef = useRef(new Set());
151
200
  const setProviderError = useCallback((provider, message) => {
152
201
  if (!mountedRef.current)
153
202
  return;
@@ -216,6 +265,58 @@ export function SubscriptionsSection(props) {
216
265
  pollersRef.current.clear();
217
266
  };
218
267
  }, [refresh, startPolling]);
268
+ const loadUsage = useCallback(async (provider) => {
269
+ if (rpc === undefined || usageInflightRef.current.has(provider))
270
+ return;
271
+ usageInflightRef.current.add(provider);
272
+ setUsageLoading(prev => ({ ...prev, [provider]: true }));
273
+ try {
274
+ const usage = await callSubscriptionsAuth(rpc, 'usage', { provider });
275
+ if (!mountedRef.current)
276
+ return;
277
+ setUsages(prev => ({ ...prev, [provider]: usage }));
278
+ setUsageErrors((prev) => {
279
+ const next = { ...prev };
280
+ delete next[provider];
281
+ return next;
282
+ });
283
+ }
284
+ catch (error) {
285
+ if (mountedRef.current)
286
+ setUsageErrors(prev => ({ ...prev, [provider]: messageOf(error) }));
287
+ }
288
+ finally {
289
+ usageInflightRef.current.delete(provider);
290
+ if (mountedRef.current)
291
+ setUsageLoading(prev => ({ ...prev, [provider]: false }));
292
+ }
293
+ }, [rpc]);
294
+ // Fetch usage once a provider is logged in; drop the cached snapshot on
295
+ // logout so a re-login refetches. A failed lookup does not auto-retry — the
296
+ // per-card Refresh button is the retry path.
297
+ useEffect(() => {
298
+ for (const { id } of PROVIDERS) {
299
+ const status = statuses[id];
300
+ if (status === undefined)
301
+ continue;
302
+ if (status.loggedIn) {
303
+ if (usages[id] === undefined && usageErrors[id] === undefined)
304
+ void loadUsage(id);
305
+ }
306
+ else if (usages[id] !== undefined || usageErrors[id] !== undefined) {
307
+ setUsages((prev) => {
308
+ const next = { ...prev };
309
+ delete next[id];
310
+ return next;
311
+ });
312
+ setUsageErrors((prev) => {
313
+ const next = { ...prev };
314
+ delete next[id];
315
+ return next;
316
+ });
317
+ }
318
+ }
319
+ }, [statuses, usages, usageErrors, loadUsage]);
219
320
  const login = useCallback(async (provider) => {
220
321
  if (rpc === undefined)
221
322
  return;
@@ -285,6 +386,15 @@ export function SubscriptionsSection(props) {
285
386
  return (_jsxs("div", { style: styles.section, children: [_jsx("p", { style: styles.intro, children: t('intro') }), PROVIDERS.map(({ id, name }) => {
286
387
  const status = statuses[id];
287
388
  const busy = status?.busy === true;
288
- return (_jsxs("div", { style: styles.card, children: [_jsxs("div", { style: styles.cardHeader, children: [_jsx("span", { style: { ...styles.dot, background: dotColor(status) } }), _jsx("span", { style: styles.name, children: name })] }), _jsx("p", { style: styles.statusLine, children: statusText(t, status) }), status?.detail !== undefined && status.detail !== '' && (_jsx("p", { style: styles.statusLine, children: status.detail })), errors[id] !== undefined && _jsx("p", { style: styles.errorLine, children: errors[id] }), _jsxs("div", { style: styles.actions, children: [!busy && status?.loggedIn !== true && (_jsx("button", { type: "button", style: styles.button, onClick: () => { void login(id); }, children: t('login') })), busy && (_jsx("button", { type: "button", style: styles.button, onClick: () => { void cancel(id); }, children: t('cancel') })), status?.loggedIn === true && (_jsx("button", { type: "button", style: styles.button, onClick: () => { void logout(id, name); }, children: t('logout') }))] }), busy && (_jsxs("details", { style: styles.manual, children: [_jsx("summary", { children: t('manualSummary') }), _jsxs("div", { style: styles.manualRow, children: [_jsx("input", { style: styles.manualInput, value: manualDrafts[id], placeholder: t('manualPlaceholder'), onChange: event => setManualDrafts(prev => ({ ...prev, [id]: event.target.value })) }), _jsx("button", { type: "button", style: styles.button, onClick: () => { void submitManual(id); }, children: t('submit') })] })] }))] }, id));
389
+ const usage = usages[id];
390
+ const usageError = usageErrors[id];
391
+ // Providers without a usage endpoint answer supported:false — no block.
392
+ const showUsage = status?.loggedIn === true && usage?.supported !== false
393
+ && (usage !== undefined || usageError !== undefined || usageLoading[id] === true);
394
+ return (_jsxs("div", { style: styles.card, children: [_jsxs("div", { style: styles.cardHeader, children: [_jsx("span", { style: { ...styles.dot, background: dotColor(status) } }), _jsx("span", { style: styles.name, children: name })] }), _jsx("p", { style: styles.statusLine, children: statusText(t, status) }), status?.detail !== undefined && status.detail !== '' && (_jsx("p", { style: styles.statusLine, children: status.detail })), errors[id] !== undefined && _jsx("p", { style: styles.errorLine, children: errors[id] }), _jsxs("div", { style: styles.actions, children: [!busy && status?.loggedIn !== true && (_jsx("button", { type: "button", style: styles.button, onClick: () => { void login(id); }, children: t('login') })), busy && (_jsx("button", { type: "button", style: styles.button, onClick: () => { void cancel(id); }, children: t('cancel') })), status?.loggedIn === true && (_jsx("button", { type: "button", style: styles.button, onClick: () => { void logout(id, name); }, children: t('logout') }))] }), showUsage && (_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.usagePlan, children: t('usagePlan', { plan: usage.plan }) })), _jsx("button", { type: "button", style: { ...styles.usageRefresh, ...usageLoading[id] === true ? { opacity: 0.5, cursor: 'default' } : {} }, disabled: usageLoading[id] === true, onClick: () => { void loadUsage(id); }, children: t('usageRefresh') })] }), usage === undefined && usageError === undefined && (_jsx("p", { style: styles.statusLine, children: t('usageLoading') })), usageError !== undefined && (_jsx("p", { style: styles.errorLine, children: t('usageError', { message: usageError }) })), usage?.windows !== undefined && usage.windows.length === 0 && (_jsx("p", { style: styles.statusLine, children: t('usageEmpty') })), (usage?.windows ?? []).map((window, index) => {
395
+ const percent = Math.min(100, Math.max(0, window.usedPercent));
396
+ return (_jsxs("div", { style: styles.usageRow, children: [_jsxs("div", { style: styles.usageMeta, children: [_jsx("span", { children: usageWindowLabel(t, window) }), _jsxs("span", { children: [`${String(Math.round(percent))}%`, window.resetsAt !== undefined
397
+ && ` · ${t('usageResets', { date: new Date(window.resetsAt).toLocaleString() })}`] })] }), _jsx("div", { style: styles.usageTrack, children: _jsx("div", { style: { ...styles.usageFill, width: `${String(percent)}%`, background: usageBarColor(percent) } }) })] }, index));
398
+ })] })), busy && (_jsxs("details", { style: styles.manual, children: [_jsx("summary", { children: t('manualSummary') }), _jsxs("div", { style: styles.manualRow, children: [_jsx("input", { style: styles.manualInput, value: manualDrafts[id], placeholder: t('manualPlaceholder'), onChange: event => setManualDrafts(prev => ({ ...prev, [id]: event.target.value })) }), _jsx("button", { type: "button", style: styles.button, onClick: () => { void submitManual(id); }, children: t('submit') })] })] }))] }, id));
289
399
  })] }));
290
400
  }
@@ -19,6 +19,16 @@ export declare const en: {
19
19
  manualPlaceholder: string;
20
20
  submit: string;
21
21
  loginMissingUrl: string;
22
+ usageTitle: string;
23
+ usageRefresh: string;
24
+ usageLoading: string;
25
+ usageEmpty: string;
26
+ usageError: string;
27
+ usageSession: string;
28
+ usageWeekly: string;
29
+ usageWindow: string;
30
+ usageResets: string;
31
+ usagePlan: string;
22
32
  generating: string;
23
33
  image: string;
24
34
  viewImage: string;
@@ -48,6 +58,16 @@ export declare const zh: {
48
58
  manualPlaceholder: string;
49
59
  submit: string;
50
60
  loginMissingUrl: string;
61
+ usageTitle: string;
62
+ usageRefresh: string;
63
+ usageLoading: string;
64
+ usageEmpty: string;
65
+ usageError: string;
66
+ usageSession: string;
67
+ usageWeekly: string;
68
+ usageWindow: string;
69
+ usageResets: string;
70
+ usagePlan: string;
51
71
  generating: string;
52
72
  image: string;
53
73
  viewImage: string;
@@ -19,6 +19,16 @@ export const en = {
19
19
  manualPlaceholder: 'Paste the callback URL or code',
20
20
  submit: 'Submit',
21
21
  loginMissingUrl: 'login answered without an authorizeUrl',
22
+ usageTitle: 'Usage',
23
+ usageRefresh: 'Refresh',
24
+ usageLoading: 'Loading usage…',
25
+ usageEmpty: 'No usage windows reported.',
26
+ usageError: 'Usage lookup failed: {message}',
27
+ usageSession: '5-hour window',
28
+ usageWeekly: 'Weekly',
29
+ usageWindow: 'Window',
30
+ usageResets: 'resets {date}',
31
+ usagePlan: 'Plan: {plan}',
22
32
  generating: 'Generating image…',
23
33
  image: 'image',
24
34
  viewImage: 'View image',
@@ -48,6 +58,16 @@ export const zh = {
48
58
  manualPlaceholder: '粘贴回调 URL 或授权码',
49
59
  submit: '提交',
50
60
  loginMissingUrl: 'login 响应缺少 authorizeUrl',
61
+ usageTitle: '用量',
62
+ usageRefresh: '刷新',
63
+ usageLoading: '用量加载中…',
64
+ usageEmpty: '服务商未返回任何用量窗口。',
65
+ usageError: '用量查询失败:{message}',
66
+ usageSession: '5 小时窗口',
67
+ usageWeekly: '每周',
68
+ usageWindow: '窗口',
69
+ usageResets: '{date} 重置',
70
+ usagePlan: '计划:{plan}',
51
71
  generating: '正在生成图片…',
52
72
  image: '图片',
53
73
  viewImage: '查看图片',
package/lib/client.js CHANGED
@@ -54,6 +54,16 @@ const en = {
54
54
  manualPlaceholder: "Paste the callback URL or code",
55
55
  submit: "Submit",
56
56
  loginMissingUrl: "login answered without an authorizeUrl",
57
+ usageTitle: "Usage",
58
+ usageRefresh: "Refresh",
59
+ usageLoading: "Loading usage…",
60
+ usageEmpty: "No usage windows reported.",
61
+ usageError: "Usage lookup failed: {message}",
62
+ usageSession: "5-hour window",
63
+ usageWeekly: "Weekly",
64
+ usageWindow: "Window",
65
+ usageResets: "resets {date}",
66
+ usagePlan: "Plan: {plan}",
57
67
  generating: "Generating image…",
58
68
  image: "image",
59
69
  viewImage: "View image",
@@ -83,6 +93,16 @@ const zh = {
83
93
  manualPlaceholder: "粘贴回调 URL 或授权码",
84
94
  submit: "提交",
85
95
  loginMissingUrl: "login 响应缺少 authorizeUrl",
96
+ usageTitle: "用量",
97
+ usageRefresh: "刷新",
98
+ usageLoading: "用量加载中…",
99
+ usageEmpty: "服务商未返回任何用量窗口。",
100
+ usageError: "用量查询失败:{message}",
101
+ usageSession: "5 小时窗口",
102
+ usageWeekly: "每周",
103
+ usageWindow: "窗口",
104
+ usageResets: "{date} 重置",
105
+ usagePlan: "计划:{plan}",
86
106
  generating: "正在生成图片…",
87
107
  image: "图片",
88
108
  viewImage: "查看图片",
@@ -223,6 +243,71 @@ const styles$1 = {
223
243
  lineHeight: "18px",
224
244
  cursor: "pointer"
225
245
  },
246
+ usage: {
247
+ display: "flex",
248
+ flexDirection: "column",
249
+ gap: 6,
250
+ marginTop: 4,
251
+ borderTop: "1px solid var(--dsw-alias-border-l2)",
252
+ paddingTop: 8
253
+ },
254
+ usageHeader: {
255
+ display: "flex",
256
+ alignItems: "center",
257
+ gap: 8
258
+ },
259
+ usageTitle: {
260
+ fontSize: 12,
261
+ lineHeight: "18px",
262
+ fontWeight: 500,
263
+ color: "var(--dsw-alias-label-secondary)"
264
+ },
265
+ usagePlan: {
266
+ fontSize: 12,
267
+ lineHeight: "18px",
268
+ color: "var(--dsw-alias-label-tertiary)"
269
+ },
270
+ usageRefresh: {
271
+ boxSizing: "border-box",
272
+ display: "inline-flex",
273
+ alignItems: "center",
274
+ justifyContent: "center",
275
+ height: 22,
276
+ padding: "0 8px",
277
+ borderRadius: 11,
278
+ marginLeft: "auto",
279
+ border: "1px solid var(--dsw-alias-border-l2)",
280
+ background: "transparent",
281
+ color: "var(--dsw-alias-label-secondary)",
282
+ font: "inherit",
283
+ fontSize: 12,
284
+ lineHeight: "18px",
285
+ cursor: "pointer"
286
+ },
287
+ usageRow: {
288
+ display: "flex",
289
+ flexDirection: "column",
290
+ gap: 3
291
+ },
292
+ usageMeta: {
293
+ display: "flex",
294
+ justifyContent: "space-between",
295
+ gap: 8,
296
+ fontSize: 12,
297
+ lineHeight: "18px",
298
+ color: "var(--dsw-alias-label-tertiary)"
299
+ },
300
+ usageTrack: {
301
+ height: 6,
302
+ borderRadius: 3,
303
+ overflow: "hidden",
304
+ background: "var(--dsw-alias-bg-layer-1)",
305
+ border: "1px solid var(--dsw-alias-border-l2)"
306
+ },
307
+ usageFill: {
308
+ height: "100%",
309
+ borderRadius: 3
310
+ },
226
311
  manual: {
227
312
  marginTop: 4,
228
313
  fontSize: 12,
@@ -275,6 +360,22 @@ function statusText(t, status) {
275
360
  return t("notLoggedIn");
276
361
  }
277
362
  /**
363
+ * Localized label of one usage window (kind, plus the model scope when named).
364
+ * @param t - section translate.
365
+ * @param window - the reported window.
366
+ * @returns e.g. "5-hour window" or "Weekly · Opus".
367
+ */
368
+ function usageWindowLabel(t, window$1) {
369
+ const base = window$1.kind === "session" ? t("usageSession") : window$1.kind === "weekly" ? t("usageWeekly") : t("usageWindow");
370
+ return window$1.scope !== void 0 && window$1.scope !== "" ? `${base} · ${window$1.scope}` : base;
371
+ }
372
+ /** Bar fill color: success normally, warn from 80%, error from 95%. */
373
+ function usageBarColor(usedPercent) {
374
+ if (usedPercent >= 95) return "var(--dsw-alias-state-error-primary)";
375
+ if (usedPercent >= 80) return "var(--dsw-alias-state-warn-label)";
376
+ return "var(--dsw-alias-state-success-primary)";
377
+ }
378
+ /**
278
379
  * The Subscriptions settings page component.
279
380
  * @param props - the slot inject face ({@link SubscriptionsSectionInjected}).
280
381
  * @returns the section body, or a notice while the RPC face is absent.
@@ -289,8 +390,13 @@ function SubscriptionsSection(props) {
289
390
  claude: "",
290
391
  grok: ""
291
392
  });
393
+ const [usages, setUsages] = (0, react.useState)({});
394
+ const [usageErrors, setUsageErrors] = (0, react.useState)({});
395
+ const [usageLoading, setUsageLoading] = (0, react.useState)({});
292
396
  const mountedRef = (0, react.useRef)(true);
293
397
  const pollersRef = (0, react.useRef)(/* @__PURE__ */ new Map());
398
+ /** Providers with a `usage` call in flight; guards the auto-fetch effect against re-entry. */
399
+ const usageInflightRef = (0, react.useRef)(/* @__PURE__ */ new Set());
294
400
  const setProviderError = (0, react.useCallback)((provider, message) => {
295
401
  if (!mountedRef.current) return;
296
402
  setErrors((prev) => {
@@ -344,6 +450,63 @@ function SubscriptionsSection(props) {
344
450
  pollersRef.current.clear();
345
451
  };
346
452
  }, [refresh, startPolling]);
453
+ const loadUsage = (0, react.useCallback)(async (provider) => {
454
+ if (rpc === void 0 || usageInflightRef.current.has(provider)) return;
455
+ usageInflightRef.current.add(provider);
456
+ setUsageLoading((prev) => ({
457
+ ...prev,
458
+ [provider]: true
459
+ }));
460
+ try {
461
+ const usage = await callSubscriptionsAuth$1(rpc, "usage", { provider });
462
+ if (!mountedRef.current) return;
463
+ setUsages((prev) => ({
464
+ ...prev,
465
+ [provider]: usage
466
+ }));
467
+ setUsageErrors((prev) => {
468
+ const next = { ...prev };
469
+ delete next[provider];
470
+ return next;
471
+ });
472
+ } catch (error) {
473
+ if (mountedRef.current) setUsageErrors((prev) => ({
474
+ ...prev,
475
+ [provider]: messageOf(error)
476
+ }));
477
+ } finally {
478
+ usageInflightRef.current.delete(provider);
479
+ if (mountedRef.current) setUsageLoading((prev) => ({
480
+ ...prev,
481
+ [provider]: false
482
+ }));
483
+ }
484
+ }, [rpc]);
485
+ (0, react.useEffect)(() => {
486
+ for (const { id } of PROVIDERS) {
487
+ const status = statuses[id];
488
+ if (status === void 0) continue;
489
+ if (status.loggedIn) {
490
+ if (usages[id] === void 0 && usageErrors[id] === void 0) loadUsage(id);
491
+ } else if (usages[id] !== void 0 || usageErrors[id] !== void 0) {
492
+ setUsages((prev) => {
493
+ const next = { ...prev };
494
+ delete next[id];
495
+ return next;
496
+ });
497
+ setUsageErrors((prev) => {
498
+ const next = { ...prev };
499
+ delete next[id];
500
+ return next;
501
+ });
502
+ }
503
+ }
504
+ }, [
505
+ statuses,
506
+ usages,
507
+ usageErrors,
508
+ loadUsage
509
+ ]);
347
510
  const login = (0, react.useCallback)(async (provider) => {
348
511
  if (rpc === void 0) return;
349
512
  setProviderError(provider, void 0);
@@ -437,6 +600,9 @@ function SubscriptionsSection(props) {
437
600
  }), PROVIDERS.map(({ id, name }) => {
438
601
  const status = statuses[id];
439
602
  const busy = status?.busy === true;
603
+ const usage = usages[id];
604
+ const usageError = usageErrors[id];
605
+ const showUsage = status?.loggedIn === true && usage?.supported !== false && (usage !== void 0 || usageError !== void 0 || usageLoading[id] === true);
440
606
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
441
607
  style: styles$1.card,
442
608
  children: [
@@ -491,6 +657,68 @@ function SubscriptionsSection(props) {
491
657
  })
492
658
  ]
493
659
  }),
660
+ showUsage && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
661
+ style: styles$1.usage,
662
+ children: [
663
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
664
+ style: styles$1.usageHeader,
665
+ children: [
666
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
667
+ style: styles$1.usageTitle,
668
+ children: t("usageTitle")
669
+ }),
670
+ usage?.plan !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
671
+ style: styles$1.usagePlan,
672
+ children: t("usagePlan", { plan: usage.plan })
673
+ }),
674
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
675
+ type: "button",
676
+ style: {
677
+ ...styles$1.usageRefresh,
678
+ ...usageLoading[id] === true ? {
679
+ opacity: .5,
680
+ cursor: "default"
681
+ } : {}
682
+ },
683
+ disabled: usageLoading[id] === true,
684
+ onClick: () => {
685
+ loadUsage(id);
686
+ },
687
+ children: t("usageRefresh")
688
+ })
689
+ ]
690
+ }),
691
+ usage === void 0 && usageError === void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
692
+ style: styles$1.statusLine,
693
+ children: t("usageLoading")
694
+ }),
695
+ usageError !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
696
+ style: styles$1.errorLine,
697
+ children: t("usageError", { message: usageError })
698
+ }),
699
+ usage?.windows !== void 0 && usage.windows.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
700
+ style: styles$1.statusLine,
701
+ children: t("usageEmpty")
702
+ }),
703
+ (usage?.windows ?? []).map((window$1, index) => {
704
+ const percent = Math.min(100, Math.max(0, window$1.usedPercent));
705
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
706
+ style: styles$1.usageRow,
707
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
708
+ style: styles$1.usageMeta,
709
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: usageWindowLabel(t, window$1) }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [`${String(Math.round(percent))}%`, window$1.resetsAt !== void 0 && ` · ${t("usageResets", { date: new Date(window$1.resetsAt).toLocaleString() })}`] })]
710
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
711
+ style: styles$1.usageTrack,
712
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { style: {
713
+ ...styles$1.usageFill,
714
+ width: `${String(percent)}%`,
715
+ background: usageBarColor(percent)
716
+ } })
717
+ })]
718
+ }, index);
719
+ })
720
+ ]
721
+ }),
494
722
  busy && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
495
723
  style: styles$1.manual,
496
724
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("summary", { children: t("manualSummary") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {