dsh-plugin-subscription-oauth 0.11.0 → 0.12.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.
@@ -63,6 +63,8 @@ export interface MimoSessionCookies {
63
63
  /** The Xiaomi sign-in has lapsed and the account must sign in again. */
64
64
  export declare class MimoSignInLapsed extends Error {
65
65
  }
66
+ /** The open platform's origin (the Token Plan lives here, not at a MiMo server). */
67
+ export declare const MIMO_PLATFORM = "https://platform.xiaomimimo.com";
66
68
  /**
67
69
  * Sign the account on at a MiMo server, following the serviceLogin redirects.
68
70
  *
@@ -118,6 +120,8 @@ export declare function refreshMimoSession(creds: MimoCredentials, fetchFn: type
118
120
  export interface MimoUsageWindow {
119
121
  label: string;
120
122
  usedPercent?: number;
123
+ used?: number;
124
+ limit?: number;
121
125
  resetsAt?: number;
122
126
  }
123
127
  /** The account's plan and weekly allowance, for the Settings card and chip. */
@@ -126,4 +130,9 @@ export declare function readMimoUsage(session: MimoSessionCookies, creds: MimoCr
126
130
  renewsAt?: number;
127
131
  windows: MimoUsageWindow[];
128
132
  }>;
133
+ /** The account's Token Plan as a usable API credential, or null when it has none. */
134
+ export declare function readMimoPlanKey(creds: MimoCredentials, fetchFn: typeof fetch): Promise<{
135
+ key: string;
136
+ base: string;
137
+ } | null>;
129
138
  export {};
@@ -144,6 +144,10 @@ export class Jar {
144
144
  /** The Xiaomi sign-in has lapsed and the account must sign in again. */
145
145
  export class MimoSignInLapsed extends Error {
146
146
  }
147
+ /** The open platform's origin (the Token Plan lives here, not at a MiMo server). */
148
+ export const MIMO_PLATFORM = 'https://platform.xiaomimimo.com';
149
+ /** A plan the app refused covers for this long before its endpoint is asked again. */
150
+ const TP_LIFE_MS = 60 * 60 * 1000;
147
151
  const appHeaders = () => ({
148
152
  'User-Agent': USER_AGENT,
149
153
  'X-Client-Version': APP_VERSION,
@@ -457,5 +461,172 @@ export async function readMimoUsage(session, creds, fetchFn) {
457
461
  catch {
458
462
  // The plan alone, when the week's allowance can't be read.
459
463
  }
464
+ // The Token Plan at the open platform, an aside the app's requests don't spend.
465
+ const tokenPlan = await readMimoTokenPlan(creds, fetchFn);
466
+ if (tokenPlan !== null) {
467
+ if (current === undefined) {
468
+ plan = tokenPlan.plan;
469
+ if (tokenPlan.until !== undefined)
470
+ renewsAt = tokenPlan.until;
471
+ }
472
+ if (tokenPlan.window !== undefined)
473
+ windows.push(tokenPlan.window);
474
+ }
460
475
  return { plan, ...renewsAt === undefined ? {} : { renewsAt }, windows };
461
476
  }
477
+ /** A short count of the Token Plan's credits, "6.81M" / "4.1B" / "12K". */
478
+ function mimoCredits(value) {
479
+ for (const [divisor, unit] of [[1e9, 'B'], [1e6, 'M'], [1e3, 'K']]) {
480
+ if (value >= divisor)
481
+ return `${Number((value / divisor).toPrecision(3))}${unit}`;
482
+ }
483
+ return String(value);
484
+ }
485
+ /** Read one of the platform's /api/v1 pages, signing on afresh when the session is gone. */
486
+ async function mimoPlatformPage(creds, path, cookies, fetchFn) {
487
+ const url = `${MIMO_PLATFORM}/api/v1${path}`;
488
+ const ask = async (cookie) => {
489
+ const response = await fetchFn(url, {
490
+ headers: cookie === '' ? {} : { Cookie: cookie },
491
+ redirect: 'manual',
492
+ signal: AbortSignal.timeout(20_000),
493
+ });
494
+ return { status: response.status, text: await response.text() };
495
+ };
496
+ const parse = (text) => {
497
+ try {
498
+ const value = JSON.parse(text);
499
+ return value?.code === 0 ? (isRecord(value.data) ? value.data : null) : undefined;
500
+ }
501
+ catch {
502
+ return undefined;
503
+ }
504
+ };
505
+ if (cookies !== undefined && Object.keys(cookies).length > 0) {
506
+ const { status, text } = await ask(cookieHeaderOf(cookies));
507
+ const data = status === 200 ? parse(text) : undefined;
508
+ if (data !== undefined)
509
+ return { cookies, data };
510
+ }
511
+ let { status, text } = await ask('');
512
+ let login;
513
+ try {
514
+ login = JSON.parse(text).loginUrl;
515
+ }
516
+ catch {
517
+ login = undefined;
518
+ }
519
+ if (status !== 401 || typeof login !== 'string' || !login.startsWith('https://account.xiaomi.com/')) {
520
+ throw new Error(`Xiaomi MiMo platform ${path}: HTTP ${status}`);
521
+ }
522
+ // Follow the serviceLogin redirect with the passToken, collecting the platform's cookies.
523
+ const jar = new Jar();
524
+ for (const [key, value] of [
525
+ ['userId', creds.userId], ['passToken', creds.passToken], ['cUserId', creds.cUserId],
526
+ ['deviceId', creds.deviceId], ['pass_ua', 'pc'], ['uLocale', 'zh_CN'],
527
+ ]) {
528
+ if (value)
529
+ jar.seed('https://account.xiaomi.com', key, value);
530
+ }
531
+ let at = login;
532
+ for (let hop = 0;; hop++) {
533
+ if (hop > 10)
534
+ throw new Error('Xiaomi MiMo platform sign-in: too many redirects');
535
+ const cookie = jar.header(at);
536
+ const response = await fetchFn(at, {
537
+ headers: cookie === '' ? {} : { Cookie: cookie },
538
+ redirect: 'manual',
539
+ signal: AbortSignal.timeout(20_000),
540
+ });
541
+ jar.take(response, at);
542
+ const location = response.headers.get('location');
543
+ if (response.status < 300 || response.status >= 400 || !location) {
544
+ status = response.status;
545
+ text = await response.text();
546
+ break;
547
+ }
548
+ await response.arrayBuffer().catch(() => { });
549
+ const next = new URL(location, at);
550
+ if (next.protocol === 'http:' && next.host === new URL(MIMO_PLATFORM).host)
551
+ next.protocol = 'https:';
552
+ at = next.href;
553
+ }
554
+ const data = status === 200 ? parse(text) : undefined;
555
+ if (data === undefined)
556
+ throw new Error(`Xiaomi MiMo platform ${path}: the Xiaomi account didn't sign on (HTTP ${status})`);
557
+ return { cookies: jar.forOrigin(`${MIMO_PLATFORM}/`), data };
558
+ }
559
+ /** Read the account's Token Plan, or null when it has none or the platform can't be read. */
560
+ async function readMimoTokenPlan(creds, fetchFn) {
561
+ try {
562
+ const detail = await mimoPlatformPage(creds, '/tokenPlan/detail', undefined, fetchFn);
563
+ const p = detail.data;
564
+ const name = p !== null && typeof p.planName === 'string' ? p.planName.trim() : '';
565
+ if (name === '' || p?.expired !== false)
566
+ return null;
567
+ const out = { plan: `Token Plan ${name}` };
568
+ const end = typeof p.currentPeriodEnd === 'string' && /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(p.currentPeriodEnd)
569
+ ? Date.parse(p.currentPeriodEnd.replace(' ', 'T') + 'Z')
570
+ : NaN;
571
+ if (!Number.isNaN(end))
572
+ out.until = end;
573
+ try {
574
+ const usage = await mimoPlatformPage(creds, '/tokenPlan/usage', detail.cookies, fetchFn);
575
+ const u = usage.data;
576
+ const items = [
577
+ ...(isRecord(u) && isRecord(u.monthUsage) && Array.isArray(u.monthUsage.items) ? u.monthUsage.items : []),
578
+ ...(isRecord(u) && isRecord(u.usage) && Array.isArray(u.usage.items) ? u.usage.items : []),
579
+ ].filter(isRecord);
580
+ const it = items.find((x) => x.name === 'month_total_token') ?? items.find((x) => x.name === 'plan_total_token');
581
+ const used = typeof it?.used === 'number' ? it.used : NaN;
582
+ const limit = typeof it?.limit === 'number' ? it.limit : NaN;
583
+ if (Number.isFinite(used) && Number.isFinite(limit) && limit > 0) {
584
+ const window = {
585
+ label: 'Token Plan',
586
+ usedPercent: Math.max(0, Math.min(100, (100 * used) / limit)),
587
+ used,
588
+ limit,
589
+ };
590
+ if (out.until !== undefined)
591
+ window.resetsAt = out.until;
592
+ out.window = window;
593
+ }
594
+ }
595
+ catch {
596
+ // The plan alone, when its usage can't be read.
597
+ }
598
+ return out;
599
+ }
600
+ catch {
601
+ return null;
602
+ }
603
+ }
604
+ /** The account's Token Plan as a usable API credential, or null when it has none. */
605
+ export async function readMimoPlanKey(creds, fetchFn) {
606
+ try {
607
+ let cookies;
608
+ const read = async (path) => {
609
+ const result = await mimoPlatformPage(creds, path, cookies, fetchFn);
610
+ cookies = result.cookies;
611
+ return result.data;
612
+ };
613
+ const detail = await read('/tokenPlan/detail');
614
+ if (detail === null || typeof detail.planName !== 'string' || detail.planName.trim() === '' || detail.expired !== false) {
615
+ return null;
616
+ }
617
+ const keyInfo = await read('/tokenPlan/apiKey');
618
+ const base = typeof keyInfo?.openaiBaseUrl === 'string' && /^https:\/\/[^/]+\.xiaomimimo\.com\//.test(keyInfo.openaiBaseUrl)
619
+ ? keyInfo.openaiBaseUrl.replace(/\/+$/, '')
620
+ : '';
621
+ if (base === '')
622
+ return null;
623
+ const raw = await read('/tokenPlan/apiKey/raw');
624
+ const rawKey = typeof raw === 'string' ? raw : '';
625
+ if (!rawKey.startsWith('tp-'))
626
+ return null;
627
+ return { key: rawKey, base };
628
+ }
629
+ catch {
630
+ return null;
631
+ }
632
+ }
@@ -8,6 +8,7 @@
8
8
  *
9
9
  * @module dsh-plugin-subscription-oauth/auth/registry
10
10
  */
11
+ import type { ModelPreferences } from '../settings.js';
11
12
  import type { LoginStart, ModelVariants, ProviderStatus, RefreshResult, SubscriptionStatus, UsageResult } from '../protocol.js';
12
13
  import type { LlmAdapter } from '@deepseek-ai/dsh-llm';
13
14
  import type { ProviderLoginStart, ProviderRefreshResult, ProviderUsageResult } from './provider-controller.js';
@@ -41,8 +42,9 @@ export interface ProviderRoute {
41
42
  * failure: the message names the routes that do exist.
42
43
  */
43
44
  export declare class SubscriptionRegistry {
45
+ private readonly preferences;
44
46
  private readonly byId;
45
- constructor(routes: readonly ProviderRoute[]);
47
+ constructor(routes: readonly ProviderRoute[], preferences: ModelPreferences);
46
48
  /** Route keys this registry serves, in display order. */
47
49
  ids(): readonly string[];
48
50
  /** Every route, in registration order. */
@@ -92,4 +94,11 @@ export declare class SubscriptionRegistry {
92
94
  reload(): Promise<SubscriptionStatus>;
93
95
  /** The fast and image variants for one model, as the route's discovery reported them. */
94
96
  modelVariants(provider: string, id: string): Promise<ModelVariants | undefined>;
97
+ /** Whether the `auto` provider is offered; flips it. */
98
+ autoState(): Promise<{
99
+ enabled: boolean;
100
+ }>;
101
+ setAutoOn(enabled: boolean): Promise<{
102
+ enabled: boolean;
103
+ }>;
95
104
  }
@@ -29,8 +29,10 @@ function displayRank(id) {
29
29
  * failure: the message names the routes that do exist.
30
30
  */
31
31
  export class SubscriptionRegistry {
32
+ preferences;
32
33
  byId;
33
- constructor(routes) {
34
+ constructor(routes, preferences) {
35
+ this.preferences = preferences;
34
36
  this.byId = new Map(routes.map((route) => [route.id, route]));
35
37
  }
36
38
  /** Route keys this registry serves, in display order. */
@@ -151,4 +153,12 @@ export class SubscriptionRegistry {
151
153
  const route = this.route(provider);
152
154
  return route.modelVariants?.(id);
153
155
  }
156
+ /** Whether the `auto` provider is offered; flips it. */
157
+ async autoState() {
158
+ return { enabled: (await this.preferences.autoPreferences()).enabled };
159
+ }
160
+ async setAutoOn(enabled) {
161
+ await this.preferences.setAutoOn(enabled);
162
+ return { enabled };
163
+ }
154
164
  }
package/lib/auth/rpc.js CHANGED
@@ -155,6 +155,14 @@ async function runEndpoint(registry, endpoint, payload, balance) {
155
155
  }
156
156
  case 'usage':
157
157
  return registry.usage(requiredProvider(payload, endpoint), booleanField(payload, 'force'));
158
+ case 'auto-state':
159
+ return registry.autoState();
160
+ case 'setAutoOn': {
161
+ const enabled = booleanField(payload, 'enabled');
162
+ if (enabled === undefined)
163
+ throw new ProviderError('setAutoOn requires an enabled flag', BAD_REQUEST);
164
+ return registry.setAutoOn(enabled);
165
+ }
158
166
  case 'deepseek-balance': {
159
167
  // The DeepSeek account's balance, read on the node half through the host's own
160
168
  // account service. The browser cannot call that service directly (it sits behind
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
2
  /**
3
3
  * Subscriptions settings page.
4
4
  *
@@ -134,13 +134,21 @@ const styles = {
134
134
  cardBody: { display: 'flex', flexDirection: 'column', gap: 10, marginTop: 2 },
135
135
  /** The one-line state a folded card shows in place of its body. */
136
136
  cardSummary: { fontSize: 12, lineHeight: '18px', color: 'var(--dsw-alias-label-tertiary)' },
137
- dot: { width: 8, height: 8, borderRadius: '50%', flexShrink: 0, boxShadow: '0 0 0 3px var(--dsw-alias-state-success-primary, transparent)' },
138
- name: { fontWeight: 600, fontSize: 14, lineHeight: '22px', color: 'var(--dsw-alias-label-primary)' },
139
- providerId: {
140
- fontSize: 11, lineHeight: '16px', color: 'var(--dsw-alias-label-dimmed)',
141
- padding: '1px 6px', borderRadius: 999, background: 'var(--dsw-alias-bg-layer-3, var(--dsw-alias-bg-layer-1))',
142
- fontFamily: 'monospace',
137
+ /** The status indicator: filled when live, a hollow ring when not. */
138
+ dot: {
139
+ width: 8, height: 8, borderRadius: '50%', flexShrink: 0,
140
+ border: '1.5px solid var(--dsw-alias-label-dimmed)',
141
+ background: 'transparent',
142
+ },
143
+ dotLive: {
144
+ borderColor: 'var(--dsw-alias-state-success-primary, #067647)',
145
+ background: 'var(--dsw-alias-state-success-primary, #067647)',
143
146
  },
147
+ dotBusy: {
148
+ borderColor: 'var(--dsw-alias-state-warn-primary, #f5a524)',
149
+ background: 'var(--dsw-alias-state-warn-primary, #f5a524)',
150
+ },
151
+ name: { fontWeight: 600, fontSize: 14, lineHeight: '22px', color: 'var(--dsw-alias-label-primary)' },
144
152
  statusLine: { margin: 0, fontSize: 12, lineHeight: '18px', color: 'var(--dsw-alias-label-tertiary)' },
145
153
  errorLine: { margin: 0, fontSize: 12, lineHeight: '18px', color: 'var(--dsw-alias-state-error-primary)' },
146
154
  successLine: { margin: 0, fontSize: 12, lineHeight: '18px', color: 'var(--dsw-alias-state-success-primary)' },
@@ -234,6 +242,10 @@ const styles = {
234
242
  padding: '8px 10px', borderRadius: 'var(--dsw-radius-md, 10px)',
235
243
  background: 'var(--dsw-alias-bg-layer-1)', border: '0.5px solid var(--dsw-alias-border-l2)',
236
244
  },
245
+ /** The metered windows under one account row: no border, just the meters. */
246
+ usageInline: { display: 'flex', flexDirection: 'column', gap: 6, marginTop: 4 },
247
+ /** The auto provider's on/off row: a label and its switch. */
248
+ autoSwitchRow: { display: 'flex', alignItems: 'center', gap: 8, justifyContent: 'space-between' },
237
249
  modelRow: {
238
250
  display: 'flex', alignItems: 'flex-start', gap: 8,
239
251
  cursor: 'pointer', fontSize: 13, lineHeight: '20px',
@@ -275,13 +287,13 @@ const styles = {
275
287
  modalActions: { display: 'flex', gap: 8, justifyContent: 'flex-end', marginTop: 2 },
276
288
  divider: { borderTop: '0.5px solid var(--dsw-alias-border-l2)' },
277
289
  };
278
- /** Status dot color for one provider's connection state. */
279
- function dotColor(status) {
290
+ /** Status dot style for one provider's connection state. */
291
+ function dotStyle(status) {
280
292
  if (status.busy)
281
- return 'var(--dsw-alias-state-warn-label)';
293
+ return styles.dotBusy;
282
294
  if (status.loggedIn)
283
- return 'var(--dsw-alias-state-success-primary)';
284
- return 'var(--dsw-alias-label-dimmed)';
295
+ return styles.dotLive;
296
+ return {};
285
297
  }
286
298
  /** Localized label for one provider-owned detail row. */
287
299
  function detailLabel(t, row) {
@@ -301,7 +313,14 @@ function windowAmount(window) {
301
313
  return `${String(window.used)}/${String(window.limit)}`;
302
314
  return '—';
303
315
  }
304
- /** One account's plan and metered allowances. */
316
+ /** One account's metered windows, rendered under its account row. */
317
+ function AccountUsage(props) {
318
+ const { entry, t } = props;
319
+ if (entry === undefined)
320
+ return null;
321
+ return (_jsxs("div", { style: styles.usageInline, children: [entry.windows.map((window) => (_jsxs("div", { style: styles.usageRow, children: [_jsxs("div", { style: styles.usageLine, children: [_jsxs("span", { style: styles.modelName, children: [windowLabel(t, window), ' · ', windowAmount(window)] }), _jsx("span", { style: styles.modelMeta, children: window.resetsAt === undefined ? '' : formatReset(window.resetsAt, t) })] }), window.usedPercent !== undefined && (_jsx("div", { className: "dsh-subscription-meter", children: _jsx("span", { style: { width: `${String(Math.min(100, Math.max(0, window.usedPercent)))}%` }, "data-exhausted": window.usedPercent >= 90 ? 'true' : 'false' }) }))] }, `${entry.account}-${window.label}`))), entry.windows.length === 0 && (_jsx("p", { style: styles.statusLine, children: t('usageNone') }))] }));
322
+ }
323
+ /** The plan-and-usage block for the whole route. */
305
324
  function UsageBlock(props) {
306
325
  const { usage, error, t, busyAction, onRefresh } = props;
307
326
  return (_jsxs("div", { style: styles.usage, children: [_jsxs("div", { style: styles.usageHeader, children: [_jsx("span", { style: styles.catalogTitle, children: t('usageTitle') }), _jsx("button", { type: "button", className: "dsh-subscription-quiet", style: styles.pushRight, disabled: busyAction, onClick: onRefresh, children: busyAction ? t('refreshing') : t('usageRefresh') })] }), error !== undefined && _jsx("p", { className: "dsh-subscription-error", children: t('usageError', { message: error }) }), usage.length === 0 && error === undefined && _jsx("p", { style: styles.statusLine, children: t('usageNone') }), usage.map((entry) => (_jsxs("div", { style: styles.usageAccount, children: [_jsxs("div", { style: styles.usageLine, children: [_jsxs("span", { style: styles.modelMeta, children: [entry.plan === undefined ? entry.account : t('usagePlan', { plan: entry.plan }), entry.renewsAt === undefined ? '' : ` · ${t('usageRenews', { time: formatTime(entry.renewsAt) })}`] }), _jsx("span", { style: styles.modelMeta, children: t('usageFetched', { time: formatTime(entry.fetchedAt) }) })] }), entry.windows.map((window) => (_jsxs("div", { style: styles.usageRow, children: [_jsxs("div", { style: styles.usageLine, children: [_jsxs("span", { style: styles.modelName, children: [windowLabel(t, window), ' · ', windowAmount(window)] }), _jsx("span", { style: styles.modelMeta, children: window.resetsAt === undefined ? '' : formatReset(window.resetsAt, t) })] }), window.usedPercent !== undefined && (_jsx("div", { className: "dsh-subscription-meter", children: _jsx("span", { style: { width: `${String(Math.min(100, Math.max(0, window.usedPercent)))}%` }, "data-exhausted": window.usedPercent >= 90 ? 'true' : 'false' }) }))] }, `${entry.account}-${window.label}`)))] }, entry.account)))] }));
@@ -500,9 +519,12 @@ function ProviderCard(props) {
500
519
  return (_jsxs("div", { style: cardOpen ? { ...styles.card, ...styles.cardOpen } : styles.card, children: [_jsxs("div", { style: styles.cardHeader, role: "button", tabIndex: 0, "aria-expanded": cardOpen, onClick: toggleCardOpen, onKeyDown: (event) => { if (event.key === 'Enter' || event.key === ' ') {
501
520
  event.preventDefault();
502
521
  toggleCardOpen();
503
- } }, children: [_jsx("span", { style: { ...styles.dot, background: dotColor(status) }, "aria-hidden": "true" }), _jsx("span", { style: styles.name, children: status.displayName }), _jsx("span", { style: styles.providerId, children: status.provider }), status.loggedIn && _jsx("span", { style: styles.badgeDefault, children: t('badgeLive') }), _jsx("span", { style: styles.cardSummary, children: summaryLine }), _jsx("button", { type: "button", className: "dsh-subscription-quiet", style: styles.pushRight, "aria-expanded": cardOpen, "aria-controls": `dsh-subscription-card-${status.provider}`, onClick: (event) => { event.stopPropagation(); toggleCardOpen(); }, children: cardOpen ? t('collapse') : t('edit') })] }), _jsxs("div", { id: `dsh-subscription-card-${status.provider}`, style: cardOpen ? styles.cardBody : { ...styles.cardBody, display: 'none' }, children: [connectable && !status.loggedIn && !busy && _jsx("p", { style: styles.statusLine, children: t('statusSignIn') }), status.lastError !== undefined && (_jsx("p", { className: "dsh-subscription-error", children: t('lastError', { message: status.lastError }) })), message?.tone === 'error' && _jsx("p", { className: "dsh-subscription-error", children: message.text }), message?.tone === 'success' && _jsx("p", { className: "dsh-subscription-success", children: message.text }), accounts.map((account) => (_jsx("div", { style: styles.accountRow, children: _jsxs("div", { style: styles.accountHeader, children: [_jsx("button", { type: "button", style: styles.starButton, title: account.isDefault ? t('defaultBadge') : t('makeDefault'), "aria-label": account.isDefault ? t('defaultBadge') : t('makeDefault'), disabled: busyAction || account.isDefault, onClick: () => { props.onSetDefault(account.account); }, children: account.isDefault ? '★' : '☆' }), _jsx("span", { style: styles.accountName, children: account.label }), account.isDefault && _jsx("span", { style: styles.badgeDefault, children: t('defaultBadge') }), account.needsLogin === true && (_jsx("span", { style: styles.badgeWarn, title: t('accountNeedsLoginHint'), children: t('accountNeedsLogin') })), _jsx("span", { style: styles.statusLine, children: account.expired
504
- ? t('accountExpired')
505
- : t('accountExpires', { time: formatTime(account.expiresAt) }) }), _jsx("button", { type: "button", className: "dsh-subscription-quiet", style: styles.pushRight, disabled: busyAction, onClick: () => { props.onLogout(account.account); }, children: t('logout') })] }) }, account.account))), status.loggedIn && (_jsxs("div", { style: styles.catalog, children: [_jsxs("div", { style: styles.catalogHeader, children: [_jsxs("button", { type: "button", className: "dsh-subscription-disclosure", style: styles.disclosure, "aria-expanded": modelsOpen, "aria-controls": `dsh-subscription-models-${status.provider}`, onClick: toggleModelsOpen, children: [_jsx("span", { style: styles.chevron, "aria-hidden": "true", children: modelsOpen ? '▾' : '▸' }), _jsx("span", { style: styles.catalogTitle, children: t('modelsTitle') })] }), _jsxs("span", { style: styles.catalogMeta, children: [t('modelsCount', { count: models.length }), ' · ', liveCatalog ? t('modelsLive') : t('modelsBuiltIn'), hiddenCount === 0 ? '' : ` ${t('modelsHiddenCount', { count: hiddenCount })}`] }), _jsx("button", { type: "button", className: "dsh-subscription-quiet", style: styles.pushRight, disabled: busyAction, onClick: props.onRefreshModels, children: busyAction ? t('refreshing') : t('refresh') }), _jsx("button", { type: "button", className: "dsh-subscription-quiet", disabled: busyAction || !connectable, title: t('modelsAddHint'), onClick: () => { setAdding((open) => !open); }, children: t('modelsAdd') })] }), adding && (_jsxs("form", { style: styles.addRow, onSubmit: (event) => {
522
+ } }, children: [_jsx("span", { style: { ...styles.dot, ...dotStyle(status) }, "aria-hidden": "true" }), _jsx("span", { style: styles.name, children: status.displayName }), status.loggedIn && _jsx("span", { style: styles.badgeDefault, children: t('badgeLive') }), _jsx("span", { style: styles.cardSummary, children: summaryLine }), _jsx("button", { type: "button", className: "dsh-subscription-quiet", style: styles.pushRight, "aria-expanded": cardOpen, "aria-controls": `dsh-subscription-card-${status.provider}`, onClick: (event) => { event.stopPropagation(); toggleCardOpen(); }, children: cardOpen ? t('collapse') : t('edit') })] }), _jsxs("div", { id: `dsh-subscription-card-${status.provider}`, style: cardOpen ? styles.cardBody : { ...styles.cardBody, display: 'none' }, children: [connectable && !status.loggedIn && !busy && _jsx("p", { style: styles.statusLine, children: t('statusSignIn') }), status.lastError !== undefined && (_jsx("p", { className: "dsh-subscription-error", children: t('lastError', { message: status.lastError }) })), message?.tone === 'error' && _jsx("p", { className: "dsh-subscription-error", children: message.text }), message?.tone === 'success' && _jsx("p", { className: "dsh-subscription-success", children: message.text }), status.provider === 'auto' && props.onSetAutoOn !== undefined && (_jsxs("label", { style: styles.autoSwitchRow, children: [_jsx("span", { style: styles.modelName, children: t('autoSwitchLabel') }), _jsx("input", { type: "checkbox", className: "dsh-subscription-switch", checked: props.autoOn ?? true, disabled: busyAction, onChange: (event) => { props.onSetAutoOn?.(event.target.checked); } })] })), accounts.map((account) => {
523
+ const usageEntry = (status.usage ?? []).find((entry) => entry.account === account.account);
524
+ return (_jsxs("div", { style: styles.accountRow, children: [_jsxs("div", { style: styles.accountHeader, children: [_jsx("button", { type: "button", style: styles.starButton, title: account.isDefault ? t('defaultBadge') : t('makeDefault'), "aria-label": account.isDefault ? t('defaultBadge') : t('makeDefault'), disabled: busyAction || account.isDefault, onClick: () => { props.onSetDefault(account.account); }, children: account.isDefault ? '★' : '☆' }), _jsx("span", { style: styles.accountName, children: account.label }), account.isDefault && _jsx("span", { style: styles.badgeDefault, children: t('defaultBadge') }), account.needsLogin === true && (_jsx("span", { style: styles.badgeWarn, title: t('accountNeedsLoginHint'), children: t('accountNeedsLogin') })), usageEntry?.plan !== undefined && (_jsx("span", { style: styles.badge, children: t('usagePlan', { plan: usageEntry.plan }) })), _jsx("span", { style: styles.statusLine, children: account.expired
525
+ ? t('accountExpired')
526
+ : t('accountExpires', { time: formatTime(account.expiresAt) }) }), _jsx("button", { type: "button", className: "dsh-subscription-quiet", style: styles.pushRight, disabled: busyAction, onClick: () => { props.onLogout(account.account); }, children: t('logout') })] }), _jsx(AccountUsage, { entry: usageEntry, t: t })] }, account.account));
527
+ }), status.loggedIn && (_jsxs("div", { style: styles.catalog, children: [_jsxs("div", { style: styles.catalogHeader, children: [_jsxs("button", { type: "button", className: "dsh-subscription-disclosure", style: styles.disclosure, "aria-expanded": modelsOpen, "aria-controls": `dsh-subscription-models-${status.provider}`, onClick: toggleModelsOpen, children: [_jsx("span", { style: styles.chevron, "aria-hidden": "true", children: modelsOpen ? '▾' : '▸' }), _jsx("span", { style: styles.catalogTitle, children: t('modelsTitle') })] }), _jsxs("span", { style: styles.catalogMeta, children: [t('modelsCount', { count: models.length }), ' · ', liveCatalog ? t('modelsLive') : t('modelsBuiltIn'), hiddenCount === 0 ? '' : ` ${t('modelsHiddenCount', { count: hiddenCount })}`] }), _jsx("button", { type: "button", className: "dsh-subscription-quiet", style: styles.pushRight, disabled: busyAction, onClick: props.onRefreshModels, children: busyAction ? t('refreshing') : t('refresh') }), _jsx("button", { type: "button", className: "dsh-subscription-quiet", disabled: busyAction || !connectable, title: t('modelsAddHint'), onClick: () => { setAdding((open) => !open); }, children: t('modelsAdd') })] }), adding && (_jsxs("form", { style: styles.addRow, onSubmit: (event) => {
506
528
  event.preventDefault();
507
529
  const id = draft.trim();
508
530
  if (id.length === 0)
@@ -525,7 +547,7 @@ function ProviderCard(props) {
525
547
  ? { ...styles.modelRowGrow, cursor: 'pointer' }
526
548
  : { ...styles.modelRowGrow, cursor: 'pointer', color: 'var(--dsw-alias-label-dimmed)' }, title: model.visible ? t('modelsHidden') : t('modelsShow'), children: [_jsx("input", { type: "checkbox", className: "dsh-subscription-switch", role: "switch", checked: model.visible, disabled: busyAction, "aria-label": `${t('modelsShow')} · ${model.name}`, onChange: (event) => { props.onSetModelVisible(model.id, event.target.checked); } }), _jsxs("span", { style: styles.modelBody, children: [_jsxs("span", { style: styles.modelName, children: [model.name, model.source === undefined ? null : _jsx("span", { style: styles.badge, children: model.source })] }), _jsxs("span", { style: styles.modelMeta, children: [model.id, ' · ', t('modelsContext', { tokens: formatTokens(model.contextWindow) }), model.defaultEffort === undefined ? '' : ` · ${model.defaultEffort}`, model.inputModalities === undefined || model.inputModalities.length === 0
527
549
  ? ''
528
- : ` · ${model.inputModalities.join('/')}`] })] })] }), model.userAdded === true && (_jsx("button", { type: "button", className: "dsh-subscription-quiet", style: styles.removeButton, disabled: busyAction, title: t('modelsRemove'), "aria-label": `${t('modelsRemove')} · ${model.name}`, onClick: () => { props.onRemoveModel(model.id); }, children: "\u00D7" }))] }, model.id))), models.length === 0 && (_jsx("p", { style: styles.statusLine, children: status.provider === 'auto' ? t('modelsAutoChainEmpty') : t('modelsEmpty') }))] }))] })), _jsx(UsageBlock, { usage: status.usage ?? [], ...status.usageError === undefined ? {} : { error: status.usageError }, t: t, busyAction: busyAction, onRefresh: props.onRefreshUsage }), connectable && (_jsxs("div", { style: styles.actions, children: [!busy && (_jsx("button", { type: "button", disabled: busyAction, onClick: props.onLogin, children: accounts.length === 0 ? t('connect') : t('addAccount') })), !busy && (_jsx("button", { type: "button", disabled: busyAction, onClick: () => { setManageOpen(true); }, children: t('manage') })), busy && (_jsx("button", { type: "button", disabled: busyAction, onClick: props.onCancel, children: t('cancel') }))] })), connectable && accounts.length > 0 && _jsx("p", { style: styles.statusLine, children: t('hint') }), busy && status.pending !== undefined && (_jsx(PendingLogin, { prompt: status.pending, t: t, copied: copied, onCopy: (code) => { void copyCode(code); } }))] }), _jsxs("dialog", { ref: dialog, className: "dsh-subscription-manager", "aria-labelledby": `dsh-subscription-manage-${status.provider}`,
550
+ : ` · ${model.inputModalities.join('/')}`] })] })] }), model.userAdded === true && (_jsx("button", { type: "button", className: "dsh-subscription-quiet", style: styles.removeButton, disabled: busyAction, title: t('modelsRemove'), "aria-label": `${t('modelsRemove')} · ${model.name}`, onClick: () => { props.onRemoveModel(model.id); }, children: "\u00D7" }))] }, model.id))), models.length === 0 && (_jsx("p", { style: styles.statusLine, children: status.provider === 'auto' ? t('modelsAutoChainEmpty') : t('modelsEmpty') }))] }))] })), connectable && (_jsxs("div", { style: styles.actions, children: [!busy && (_jsx("button", { type: "button", disabled: busyAction, onClick: props.onLogin, children: accounts.length === 0 ? t('connect') : t('addAccount') })), !busy && (_jsx("button", { type: "button", disabled: busyAction, onClick: () => { setManageOpen(true); }, children: t('manage') })), busy && (_jsx("button", { type: "button", disabled: busyAction, onClick: props.onCancel, children: t('cancel') }))] })), connectable && accounts.length > 0 && _jsx("p", { style: styles.statusLine, children: t('hint') }), busy && status.pending !== undefined && (_jsx(PendingLogin, { prompt: status.pending, t: t, copied: copied, onCopy: (code) => { void copyCode(code); } }))] }), _jsxs("dialog", { ref: dialog, className: "dsh-subscription-manager", "aria-labelledby": `dsh-subscription-manage-${status.provider}`,
529
551
  // A closed `<dialog>` is hidden by the user-agent stylesheet, and an
530
552
  // inline `display` would override that rule — so the open state has to
531
553
  // be restated here rather than left to `showModal()` alone.
@@ -543,8 +565,20 @@ export function SubscriptionsSection(props) {
543
565
  const [error, setError] = useState(undefined);
544
566
  const [messages, setMessages] = useState({});
545
567
  const [busyAction, setBusyAction] = useState(false);
568
+ const [autoOn, setAutoOn] = useState(true);
546
569
  const mounted = useRef(true);
547
570
  const timer = useRef(undefined);
571
+ /** Read whether the `auto` provider is offered, so its card can render the switch. */
572
+ useEffect(() => {
573
+ if (call === undefined)
574
+ return;
575
+ let live = true;
576
+ void call('auto-state', undefined)
577
+ .then((result) => { if (live)
578
+ setAutoOn(result.enabled); })
579
+ .catch(() => { });
580
+ return () => { live = false; };
581
+ }, [call]);
548
582
  /** Fetch the current state; a failure is shown rather than swallowed. */
549
583
  const refresh = useCallback(async () => {
550
584
  if (call === undefined)
@@ -561,6 +595,27 @@ export function SubscriptionsSection(props) {
561
595
  setError(t('actionFailed', { message: messageOf(caught) }));
562
596
  }
563
597
  }, [call, t]);
598
+ /** Flip the `auto` provider on or off. */
599
+ const setAuto = useCallback(async (enabled) => {
600
+ if (call === undefined)
601
+ return;
602
+ setBusyAction(true);
603
+ try {
604
+ const result = await call('setAutoOn', { enabled });
605
+ if (!mounted.current)
606
+ return;
607
+ setAutoOn(result.enabled);
608
+ await refresh();
609
+ }
610
+ catch (caught) {
611
+ if (mounted.current)
612
+ setError(t('actionFailed', { message: messageOf(caught) }));
613
+ }
614
+ finally {
615
+ if (mounted.current)
616
+ setBusyAction(false);
617
+ }
618
+ }, [call, refresh, t]);
564
619
  const stopPolling = useCallback(() => {
565
620
  if (timer.current !== undefined) {
566
621
  clearInterval(timer.current);
@@ -804,5 +859,5 @@ export function SubscriptionsSection(props) {
804
859
  }, [act, call]);
805
860
  if (call === undefined)
806
861
  return _jsx("p", { style: styles.intro, children: t('unavailable') });
807
- return (_jsxs("div", { className: "dsh-subscriptions-settings", style: styles.section, children: [_jsx("style", { children: subscriptionsCss }), _jsxs("div", { style: styles.sectionHeader, children: [_jsx("h2", { style: styles.heading, children: t('nav') }), _jsx("p", { style: styles.intro, children: t('intro') })] }), error !== undefined && _jsx("p", { className: "dsh-subscription-error", children: error }), status === undefined && _jsx("p", { style: styles.statusLine, children: t('loading') }), orderedProviders.map((provider) => (_jsx(ProviderCard, { status: provider, t: t, busyAction: busyAction, ...messages[provider.provider] === undefined ? {} : { message: messages[provider.provider] }, onLogin: () => { login(provider.provider); }, onCancel: () => { cancel(provider.provider); }, onLogout: (account) => { logout(provider.provider, account); }, onSetDefault: (account) => { setDefault(provider.provider, account); }, onRefreshToken: (account) => { refreshToken(provider.provider, account); }, onRefreshModels: () => { refreshModels(provider.provider); }, onSetModelVisible: (id, visible) => { setModelVisible(provider.provider, id, visible); }, onSetModelOrder: (ids) => { setModelOrder(provider.provider, ids); }, onAddModel: (id) => { addModel(provider.provider, id); }, onRemoveModel: (id) => { removeModel(provider.provider, id); }, onRefreshUsage: () => { refreshUsage(provider.provider); }, onReload: () => { reload(provider.provider); } }, provider.provider)))] }));
862
+ return (_jsxs("div", { className: "dsh-subscriptions-settings", style: styles.section, children: [_jsx("style", { children: subscriptionsCss }), _jsxs("div", { style: styles.sectionHeader, children: [_jsx("h2", { style: styles.heading, children: t('nav') }), _jsx("p", { style: styles.intro, children: t('intro') })] }), error !== undefined && _jsx("p", { className: "dsh-subscription-error", children: error }), status === undefined && _jsx("p", { style: styles.statusLine, children: t('loading') }), orderedProviders.map((provider) => (_jsx(ProviderCard, { status: provider, t: t, busyAction: busyAction, ...messages[provider.provider] === undefined ? {} : { message: messages[provider.provider] }, onLogin: () => { login(provider.provider); }, onCancel: () => { cancel(provider.provider); }, onLogout: (account) => { logout(provider.provider, account); }, onSetDefault: (account) => { setDefault(provider.provider, account); }, onRefreshToken: (account) => { refreshToken(provider.provider, account); }, onRefreshModels: () => { refreshModels(provider.provider); }, onSetModelVisible: (id, visible) => { setModelVisible(provider.provider, id, visible); }, onSetModelOrder: (ids) => { setModelOrder(provider.provider, ids); }, onAddModel: (id) => { addModel(provider.provider, id); }, onRemoveModel: (id) => { removeModel(provider.provider, id); }, onRefreshUsage: () => { refreshUsage(provider.provider); }, onReload: () => { reload(provider.provider); }, ...provider.provider !== 'auto' ? {} : { autoOn, onSetAutoOn: (enabled) => { void setAuto(enabled); } } }, provider.provider)))] }));
808
863
  }
@@ -207,12 +207,18 @@ export function mergeAccountWindows(readings) {
207
207
  if (withPercent.length === 0)
208
208
  continue;
209
209
  const usedPercent = withPercent.reduce((sum, window) => sum + (window.usedPercent ?? 0), 0) / withPercent.length;
210
+ // A credit window carries the raw figures too; keep them so the compact line can read
211
+ // money as money (0/1500) rather than as a spent share (0%), which misreads an empty
212
+ // balance as nothing to meter.
213
+ const used = list.reduce((sum, window) => sum + (window.used ?? 0), 0);
214
+ const limit = list.reduce((sum, window) => sum + (window.limit ?? 0), 0);
210
215
  // The pool's reset is the soonest account reset: the window refills when the first
211
216
  // account's does.
212
217
  const resets = list.map((window) => window.resetsAt).filter((at) => typeof at === 'number');
213
218
  merged.push({
214
219
  label,
215
220
  usedPercent: Math.max(0, Math.min(100, usedPercent)),
221
+ ...limit > 0 ? { used, limit } : {},
216
222
  ...resets.length === 0 ? {} : { resetsAt: Math.min(...resets) },
217
223
  });
218
224
  }
@@ -430,7 +436,9 @@ export function UsageChip({ call, t, isOwnProvider, accountBalance, ccSwitchBala
430
436
  // The document listener would otherwise close what this click just opened.
431
437
  event.stopPropagation();
432
438
  setOpen((was) => !was);
433
- }, children: [_jsx("span", { className: "dsh-subscription-usage-chip-label", children: short }), merged.map((window) => (_jsxs("span", { className: "dsh-subscription-usage-chip-cell", children: [_jsx("span", { className: "dsh-subscription-usage-chip-window", children: shortWindow(window.label) }), _jsx("span", { className: "dsh-subscription-usage-chip-value", children: `${String(Math.round(window.usedPercent ?? 0))}%` })] }, window.label)))] }), open && (_jsxs("span", { className: "dsh-subscription-usage-chip-panel", role: "status", children: [_jsx("span", { className: "dsh-subscription-usage-chip-full", children: t('usageChipFull', { provider: selected ?? '' }) }), accountReadings.map((account) => (_jsxs("span", { className: "dsh-subscription-usage-chip-account", children: [_jsx("span", { className: "dsh-subscription-usage-chip-meterHead", children: _jsx("span", { children: account.plan ?? account.account }) }), account.windows.map((window) => {
439
+ }, children: [_jsx("span", { className: "dsh-subscription-usage-chip-label", children: short }), merged.map((window) => (_jsxs("span", { className: "dsh-subscription-usage-chip-cell", children: [_jsx("span", { className: "dsh-subscription-usage-chip-window", children: shortWindow(window.label) }), _jsx("span", { className: "dsh-subscription-usage-chip-value", children: window.used !== undefined && window.limit !== undefined
440
+ ? `${String(window.used)}/${String(window.limit)}`
441
+ : `${String(Math.round(window.usedPercent ?? 0))}%` })] }, window.label)))] }), open && (_jsxs("span", { className: "dsh-subscription-usage-chip-panel", role: "status", children: [_jsx("span", { className: "dsh-subscription-usage-chip-full", children: t('usageChipFull', { provider: selected ?? '' }) }), accountReadings.map((account) => (_jsxs("span", { className: "dsh-subscription-usage-chip-account", children: [_jsx("span", { className: "dsh-subscription-usage-chip-meterHead", children: _jsx("span", { children: account.plan ?? account.account }) }), account.windows.map((window) => {
434
442
  const windowReset = formatReset(window.resetsAt, now);
435
443
  // A credit balance reads as money (`used`/`limit`), a quota as a percent.
436
444
  const amount = window.usedPercent === undefined && window.used !== undefined && window.limit !== undefined
@@ -60,6 +60,7 @@ export declare const en: {
60
60
  readonly modelsDrag: "Drag to re-order (or press ↑ / ↓ here)";
61
61
  readonly modelsAutoChainOff: "Take out of Auto";
62
62
  readonly modelsAutoChainEmpty: "No model is enabled, so Auto has nothing to try.";
63
+ readonly autoSwitchLabel: "Offer Auto in the model picker";
63
64
  readonly modelsPinned: "pinned";
64
65
  readonly usageTitle: "Plan and usage";
65
66
  readonly usagePlan: "Plan: {plan}";
@@ -157,6 +158,7 @@ export declare const zh: {
157
158
  modelsDrag: string;
158
159
  modelsAutoChainOff: string;
159
160
  modelsAutoChainEmpty: string;
161
+ autoSwitchLabel: string;
160
162
  modelsPinned: string;
161
163
  usageTitle: string;
162
164
  usagePlan: string;
@@ -60,6 +60,7 @@ export const en = {
60
60
  modelsDrag: 'Drag to re-order (or press ↑ / ↓ here)',
61
61
  modelsAutoChainOff: 'Take out of Auto',
62
62
  modelsAutoChainEmpty: 'No model is enabled, so Auto has nothing to try.',
63
+ autoSwitchLabel: 'Offer Auto in the model picker',
63
64
  modelsPinned: 'pinned',
64
65
  usageTitle: 'Plan and usage',
65
66
  usagePlan: 'Plan: {plan}',
@@ -155,6 +156,7 @@ export const zh = {
155
156
  modelsDrag: '拖动调整顺序(也可在此按 ↑ / ↓)',
156
157
  modelsAutoChainOff: '从自动中移除',
157
158
  modelsAutoChainEmpty: '没有启用任何模型,自动没有可尝试的模型。',
159
+ autoSwitchLabel: '在模型选择器中提供 Auto',
158
160
  modelsPinned: '固定',
159
161
  usageTitle: '套餐与额度',
160
162
  usagePlan: '套餐:{plan}',