dsh-plugin-subscription-oauth 0.11.0 → 0.12.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.
@@ -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,12 @@ 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 'setAutoOn': {
159
+ const enabled = booleanField(payload, 'enabled');
160
+ if (enabled === undefined)
161
+ throw new ProviderError('setAutoOn requires an enabled flag', BAD_REQUEST);
162
+ return registry.setAutoOn(enabled);
163
+ }
158
164
  case 'deepseek-balance': {
159
165
  // The DeepSeek account's balance, read on the node half through the host's own
160
166
  // 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,8 @@ 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 },
237
247
  modelRow: {
238
248
  display: 'flex', alignItems: 'flex-start', gap: 8,
239
249
  cursor: 'pointer', fontSize: 13, lineHeight: '20px',
@@ -275,13 +285,13 @@ const styles = {
275
285
  modalActions: { display: 'flex', gap: 8, justifyContent: 'flex-end', marginTop: 2 },
276
286
  divider: { borderTop: '0.5px solid var(--dsw-alias-border-l2)' },
277
287
  };
278
- /** Status dot color for one provider's connection state. */
279
- function dotColor(status) {
288
+ /** Status dot style for one provider's connection state. */
289
+ function dotStyle(status) {
280
290
  if (status.busy)
281
- return 'var(--dsw-alias-state-warn-label)';
291
+ return styles.dotBusy;
282
292
  if (status.loggedIn)
283
- return 'var(--dsw-alias-state-success-primary)';
284
- return 'var(--dsw-alias-label-dimmed)';
293
+ return styles.dotLive;
294
+ return {};
285
295
  }
286
296
  /** Localized label for one provider-owned detail row. */
287
297
  function detailLabel(t, row) {
@@ -301,7 +311,14 @@ function windowAmount(window) {
301
311
  return `${String(window.used)}/${String(window.limit)}`;
302
312
  return '—';
303
313
  }
304
- /** One account's plan and metered allowances. */
314
+ /** One account's metered windows, rendered under its account row. */
315
+ function AccountUsage(props) {
316
+ const { entry, t } = props;
317
+ if (entry === undefined)
318
+ return null;
319
+ 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') }))] }));
320
+ }
321
+ /** The plan-and-usage block for the whole route. */
305
322
  function UsageBlock(props) {
306
323
  const { usage, error, t, busyAction, onRefresh } = props;
307
324
  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 +517,12 @@ function ProviderCard(props) {
500
517
  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
518
  event.preventDefault();
502
519
  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) => {
520
+ } }, 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 }), accounts.map((account) => {
521
+ const usageEntry = (status.usage ?? []).find((entry) => entry.account === account.account);
522
+ 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
523
+ ? t('accountExpired')
524
+ : 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));
525
+ }), 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
526
  event.preventDefault();
507
527
  const id = draft.trim();
508
528
  if (id.length === 0)
@@ -525,7 +545,7 @@ function ProviderCard(props) {
525
545
  ? { ...styles.modelRowGrow, cursor: 'pointer' }
526
546
  : { ...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
547
  ? ''
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}`,
548
+ : ` · ${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
549
  // A closed `<dialog>` is hidden by the user-agent stylesheet, and an
530
550
  // inline `display` would override that rule — so the open state has to
531
551
  // be restated here rather than left to `showModal()` alone.