dsh-plugin-subscriptions 0.5.3 → 0.6.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.
Files changed (44) hide show
  1. package/README.md +45 -6
  2. package/README.zh.md +43 -4
  3. package/lib/auth/rpc.d.ts +36 -2
  4. package/lib/auth/rpc.js +47 -5
  5. package/lib/client/ImageGenerateToolview.d.ts +1 -1
  6. package/lib/client/SpeedSelect.d.ts +25 -2
  7. package/lib/client/SpeedSelect.js +10 -6
  8. package/lib/client/SubscriptionsSection.d.ts +74 -0
  9. package/lib/client/SubscriptionsSection.js +325 -4
  10. package/lib/client/VideoGenerateToolview.d.ts +1 -1
  11. package/lib/client/index.d.ts +1 -9
  12. package/lib/client/index.js +7 -4
  13. package/lib/client/locales.d.ts +28 -0
  14. package/lib/client/locales.js +28 -0
  15. package/lib/client.js +458 -10
  16. package/lib/client.js.map +1 -1
  17. package/lib/compat.d.ts +36 -0
  18. package/lib/compat.js +20 -0
  19. package/lib/index.d.ts +5 -1
  20. package/lib/index.js +865 -111
  21. package/lib/model-defaults.d.ts +23 -0
  22. package/lib/model-defaults.js +237 -0
  23. package/lib/providers/claude.d.ts +24 -3
  24. package/lib/providers/claude.js +35 -24
  25. package/lib/providers/codex.d.ts +21 -0
  26. package/lib/providers/codex.js +37 -10
  27. package/lib/providers/common.d.ts +70 -6
  28. package/lib/providers/common.js +118 -19
  29. package/lib/providers/copilot.d.ts +10 -0
  30. package/lib/providers/copilot.js +21 -8
  31. package/lib/providers/grok.d.ts +21 -0
  32. package/lib/providers/grok.js +37 -7
  33. package/lib/providers/pool-usage.d.ts +23 -2
  34. package/lib/providers/pool-usage.js +70 -15
  35. package/lib/providers/rate-limit.d.ts +192 -0
  36. package/lib/providers/rate-limit.js +338 -0
  37. package/lib/translate/anthropic.js +5 -4
  38. package/lib/translate/chat-completions.js +5 -4
  39. package/lib/translate/responses.js +5 -4
  40. package/package.json +21 -21
  41. package/lib/providers/antigravity.d.ts +0 -90
  42. package/lib/providers/antigravity.js +0 -392
  43. package/lib/translate/antigravity.d.ts +0 -110
  44. package/lib/translate/antigravity.js +0 -303
@@ -19,6 +19,17 @@ import { en } from './locales.js';
19
19
  const SUBSCRIPTIONS_AUTH_CHANNEL = '/subscriptions-auth';
20
20
  /** Poll cadence while a provider login attempt is busy. */
21
21
  const POLL_INTERVAL_MS = 2000;
22
+ /**
23
+ * Model count above which the expanded default-effort list also offers a name
24
+ * filter; below it the list is short enough to scan.
25
+ */
26
+ const MODEL_FILTER_THRESHOLD = 8;
27
+ /**
28
+ * Height cap of the expanded default-effort list, in px. Past it the list
29
+ * scrolls internally so a provider with dozens of models cannot stretch the
30
+ * card (roughly 7 rows, which keeps the next card's header on screen).
31
+ */
32
+ const MODEL_LIST_MAX_HEIGHT = 260;
22
33
  /** Card display metadata, in page order (names are brand names, not translated). */
23
34
  const PROVIDERS = [
24
35
  { id: 'codex', name: 'Codex (ChatGPT)' },
@@ -141,6 +152,46 @@ const styles = {
141
152
  background: 'var(--dsw-alias-bg-layer-1)', border: '1px solid var(--dsw-alias-border-l2)',
142
153
  },
143
154
  usageFill: { height: '100%', borderRadius: 3 },
155
+ defaultEffort: {
156
+ display: 'flex', flexDirection: 'column', gap: 6, marginTop: 4,
157
+ borderTop: '1px solid var(--dsw-alias-border-l2)', paddingTop: 8,
158
+ },
159
+ /** The always-visible disclosure header: title, summary, chevron. */
160
+ defaultEffortToggle: {
161
+ boxSizing: 'border-box', display: 'flex', alignItems: 'center', gap: 8,
162
+ width: '100%', padding: 0, border: 'none', background: 'transparent',
163
+ font: 'inherit', textAlign: 'left', cursor: 'pointer',
164
+ },
165
+ defaultEffortChevron: {
166
+ marginLeft: 'auto', flexShrink: 0, fontSize: 10, lineHeight: '18px',
167
+ color: 'var(--dsw-alias-label-tertiary)',
168
+ },
169
+ /** Body of the expanded disclosure: bounded height so a long catalog scrolls. */
170
+ defaultEffortList: {
171
+ display: 'flex', flexDirection: 'column', gap: 6,
172
+ maxHeight: MODEL_LIST_MAX_HEIGHT, overflowY: 'auto', paddingRight: 2,
173
+ },
174
+ defaultEffortRow: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 },
175
+ defaultEffortName: {
176
+ fontSize: 12, lineHeight: '18px', color: 'var(--dsw-alias-label-primary)',
177
+ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
178
+ },
179
+ defaultEffortSaving: {
180
+ marginLeft: 'auto', flexShrink: 0,
181
+ fontSize: 12, lineHeight: '18px', color: 'var(--dsw-alias-label-tertiary)',
182
+ },
183
+ defaultEffortSelect: {
184
+ maxWidth: 220, flexShrink: 0, height: 28, boxSizing: 'border-box',
185
+ border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 8,
186
+ padding: '0 8px', font: 'inherit', fontSize: 12, lineHeight: '18px',
187
+ background: 'var(--dsw-alias-bg-layer-1)', color: 'var(--dsw-alias-label-primary)',
188
+ },
189
+ defaultEffortFilter: {
190
+ height: 28, width: '100%', boxSizing: 'border-box',
191
+ border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 8,
192
+ padding: '0 8px', font: 'inherit', fontSize: 12, lineHeight: '18px',
193
+ background: 'var(--dsw-alias-bg-layer-1)', color: 'var(--dsw-alias-label-primary)',
194
+ },
144
195
  manual: { marginTop: 4, fontSize: 12, lineHeight: '18px', color: 'var(--dsw-alias-label-secondary)' },
145
196
  manualRow: { display: 'flex', gap: 8, marginTop: 6 },
146
197
  manualInput: {
@@ -197,6 +248,16 @@ function dotColor(status) {
197
248
  return 'var(--dsw-alias-state-success-primary)';
198
249
  return 'var(--dsw-alias-label-dimmed)';
199
250
  }
251
+ /**
252
+ * Whether a provider has at least one connected account. Multi-account made
253
+ * "logged in" a property of the account list rather than a flag, so every
254
+ * logged-in test goes through this one predicate.
255
+ * @param status - the provider's last reported state, possibly absent.
256
+ * @returns true when the provider serves at least one account.
257
+ */
258
+ function hasAccount(status) {
259
+ return (status?.accounts.length ?? 0) > 0;
260
+ }
200
261
  /**
201
262
  * One-line status text for one provider state.
202
263
  * @param t - section translate.
@@ -250,6 +311,65 @@ function messageColor(tone) {
250
311
  ? 'var(--dsw-alias-state-error-primary)'
251
312
  : 'var(--dsw-alias-state-success-primary)';
252
313
  }
314
+ /**
315
+ * Derive one provider's default-effort section from its catalog and filter.
316
+ * Pure so the collapsed-header counts and the filter stay testable without a
317
+ * DOM: rows come only from models that advertise levels, the count of the rest
318
+ * rides as one line, and the filter matches display name or model id.
319
+ * @param models - the provider's catalog models, or undefined while loading.
320
+ * @param filter - the raw filter input (trimmed and lowercased here).
321
+ * @returns the section's rows and header counts.
322
+ */
323
+ export function deriveModelDefaultsView(models, filter) {
324
+ const all = models ?? [];
325
+ const withEfforts = all.filter(model => model.efforts.length > 0);
326
+ const query = filter.trim().toLowerCase();
327
+ const shown = query === ''
328
+ ? withEfforts
329
+ : withEfforts.filter(model => model.name.toLowerCase().includes(query)
330
+ || model.id.toLowerCase().includes(query));
331
+ return {
332
+ shown,
333
+ total: withEfforts.length,
334
+ overridden: withEfforts.filter(model => model.configured !== undefined).length,
335
+ withoutEfforts: all.length - withEfforts.length,
336
+ showFilter: withEfforts.length > MODEL_FILTER_THRESHOLD,
337
+ };
338
+ }
339
+ /**
340
+ * Whether the default-effort catalog needs (re)fetching.
341
+ *
342
+ * Fetching is gated on an *attempt* signature rather than on the payload
343
+ * being empty: an empty answer is a legitimate result (a narrowed
344
+ * `config.providers`, or a catalog that is momentarily unavailable), and
345
+ * treating it as "not loaded yet" re-ran this effect forever. The signature
346
+ * also covers the accounts, so logging a second provider in refetches
347
+ * instead of leaving that card on the previous answer.
348
+ * @param input - the decision inputs.
349
+ * @returns true when the caller should start a fetch.
350
+ */
351
+ export function shouldFetchModelDefaults(input) {
352
+ if (input.failed)
353
+ return false;
354
+ if (input.loggedIn.length === 0)
355
+ return false;
356
+ // Only an open disclosure pays for the per-model live resolve.
357
+ if (!input.open.some(provider => input.loggedIn.includes(provider)))
358
+ return false;
359
+ return input.loadedFor !== input.signature;
360
+ }
361
+ /**
362
+ * Stable signature of the accounts a catalog answer depends on. A change
363
+ * means a previous answer is stale (an account arrived or left), so the next
364
+ * open disclosure refetches.
365
+ * @param statuses - the per-provider status snapshot.
366
+ * @returns a signature string, stable across renders with equal accounts.
367
+ */
368
+ export function modelDefaultsSignature(statuses) {
369
+ return PROVIDERS
370
+ .map(({ id }) => `${id}:${(statuses[id]?.accounts ?? []).map(account => account.key).sort().join(',')}`)
371
+ .join('|');
372
+ }
253
373
  /**
254
374
  * The Subscriptions settings page component.
255
375
  * @param props - the slot inject face ({@link SubscriptionsSectionInjected}).
@@ -289,6 +409,24 @@ export function SubscriptionsSection(props) {
289
409
  const [proxyTesting, setProxyTesting] = useState(false);
290
410
  const [proxyMessage, setProxyMessage] = useState(undefined);
291
411
  const [proxyTestResult, setProxyTestResult] = useState(undefined);
412
+ /** Per-model default-effort picker state as answered by `modelDefaults`. */
413
+ const [modelDefaults, setModelDefaults] = useState({});
414
+ const [modelDefaultsLoading, setModelDefaultsLoading] = useState(false);
415
+ const [modelDefaultsLoadError, setModelDefaultsLoadError] = useState(undefined);
416
+ /** One set in flight: the `${provider}/${model}` key. */
417
+ const [modelDefaultsSaving, setModelDefaultsSaving] = useState(undefined);
418
+ /** Per-model save failures, keyed `${provider}/${model}`. */
419
+ const [modelDefaultsSaveErrors, setModelDefaultsSaveErrors] = useState({});
420
+ /** Providers whose default-effort disclosure is open (collapsed by default). */
421
+ const [modelDefaultsOpen, setModelDefaultsOpen] = useState({});
422
+ /** Per-provider name filter of the expanded list. */
423
+ const [modelDefaultsFilters, setModelDefaultsFilters] = useState({});
424
+ /** Account signature the last completed catalog fetch was answered for. */
425
+ const [modelDefaultsLoadedFor, setModelDefaultsLoadedFor] = useState(undefined);
426
+ /** Optimistic in-flight selections, keyed `${provider}/${model}` ('' = follow provider). */
427
+ const [modelDefaultsPending, setModelDefaultsPending] = useState({});
428
+ /** Guard the catalog effect against concurrent loads. */
429
+ const modelDefaultsInflightRef = useRef(false);
292
430
  const setProviderError = useCallback((provider, message) => {
293
431
  if (!mountedRef.current)
294
432
  return;
@@ -366,14 +504,14 @@ export function SubscriptionsSection(props) {
366
504
  pollersRef.current.clear();
367
505
  };
368
506
  }, [refresh, startPolling]);
369
- const loadUsage = useCallback(async (provider, account) => {
507
+ const loadUsage = useCallback(async (provider, account, force = false) => {
370
508
  const key = `${provider}:${account}`;
371
509
  if (rpc === undefined || usageInflightRef.current.has(key))
372
510
  return;
373
511
  usageInflightRef.current.add(key);
374
512
  setUsageLoading(prev => ({ ...prev, [key]: true }));
375
513
  try {
376
- const usage = await callSubscriptionsAuth(rpc, 'usage', { provider, account });
514
+ const usage = await callSubscriptionsAuth(rpc, 'usage', { provider, account, ...force ? { force: true } : {} });
377
515
  if (!mountedRef.current)
378
516
  return;
379
517
  setUsages(prev => ({ ...prev, [key]: usage }));
@@ -409,6 +547,146 @@ export function SubscriptionsSection(props) {
409
547
  setUsages(prev => dropStale(prev, live));
410
548
  setUsageErrors(prev => dropStale(prev, live));
411
549
  }, [statuses, usages, usageErrors, loadUsage]);
550
+ const loadModelDefaultsData = useCallback(async (signature) => {
551
+ if (rpc === undefined || modelDefaultsInflightRef.current)
552
+ return;
553
+ modelDefaultsInflightRef.current = true;
554
+ setModelDefaultsLoading(true);
555
+ try {
556
+ const catalog = await callSubscriptionsAuth(rpc, 'modelDefaults', {});
557
+ if (!mountedRef.current)
558
+ return;
559
+ const next = {};
560
+ for (const entry of catalog)
561
+ next[entry.provider] = entry;
562
+ setModelDefaults(next);
563
+ setModelDefaultsLoadError(undefined);
564
+ // Latch the answered signature, empty answer included: an empty catalog
565
+ // is a result, not a missing load, and re-deriving "loaded" from the
566
+ // payload re-triggered this fetch on every render.
567
+ setModelDefaultsLoadedFor(signature);
568
+ // The shown state is now authoritative; stale per-row failures would
569
+ // otherwise linger next to rows that are correct again.
570
+ setModelDefaultsSaveErrors({});
571
+ }
572
+ catch (error) {
573
+ if (mountedRef.current)
574
+ setModelDefaultsLoadError(messageOf(error));
575
+ }
576
+ finally {
577
+ modelDefaultsInflightRef.current = false;
578
+ if (mountedRef.current)
579
+ setModelDefaultsLoading(false);
580
+ }
581
+ }, [rpc]);
582
+ // Fetch the default-effort catalogs only once a card's list is expanded: the
583
+ // node half resolves live model info per model, so a collapsed page must not
584
+ // pay for it. One fetch covers every logged-in provider (the node half
585
+ // answers them together), and the levels follow the picker's catalog union
586
+ // across that provider's accounts. The account signature drives refetching,
587
+ // so connecting another provider or account does not leave an open card on
588
+ // the previous answer. Everything resets once the last account logs out.
589
+ useEffect(() => {
590
+ const loggedIn = PROVIDERS.filter(({ id }) => hasAccount(statuses[id])).map(({ id }) => id);
591
+ const signature = modelDefaultsSignature(statuses);
592
+ if (loggedIn.length > 0) {
593
+ const open = PROVIDERS.filter(({ id }) => modelDefaultsOpen[id] === true).map(({ id }) => id);
594
+ if (shouldFetchModelDefaults({
595
+ loggedIn,
596
+ open,
597
+ loadedFor: modelDefaultsLoadedFor,
598
+ signature,
599
+ failed: modelDefaultsLoadError !== undefined,
600
+ })) {
601
+ void loadModelDefaultsData(signature);
602
+ }
603
+ }
604
+ else if (modelDefaultsLoadedFor !== undefined
605
+ || Object.keys(modelDefaults).length > 0
606
+ || Object.keys(modelDefaultsOpen).length > 0) {
607
+ setModelDefaults({});
608
+ setModelDefaultsSaveErrors({});
609
+ setModelDefaultsLoadError(undefined);
610
+ setModelDefaultsLoadedFor(undefined);
611
+ setModelDefaultsLoading(false);
612
+ setModelDefaultsOpen({});
613
+ setModelDefaultsFilters({});
614
+ }
615
+ }, [
616
+ statuses,
617
+ modelDefaults,
618
+ modelDefaultsOpen,
619
+ modelDefaultsLoadError,
620
+ modelDefaultsLoadedFor,
621
+ loadModelDefaultsData,
622
+ ]);
623
+ /** Open or close one provider's default-effort disclosure. */
624
+ const toggleModelDefaults = useCallback((provider) => {
625
+ setModelDefaultsOpen(prev => ({ ...prev, [provider]: prev[provider] !== true }));
626
+ }, []);
627
+ const setModelDefault = useCallback(async (provider, model, effort) => {
628
+ if (rpc === undefined)
629
+ return;
630
+ const key = `${provider}/${model}`;
631
+ setModelDefaultsSaving(key);
632
+ // Hold the picked level locally for the duration of the save: the select
633
+ // is controlled by server state, which only updates after the round trip,
634
+ // so without this the row visibly snaps back to "Follow provider" (greyed
635
+ // out) mid-save and reads as a rejected change.
636
+ setModelDefaultsPending(prev => ({ ...prev, [key]: effort ?? '' }));
637
+ setModelDefaultsSaveErrors((prev) => {
638
+ const next = { ...prev };
639
+ delete next[key];
640
+ return next;
641
+ });
642
+ try {
643
+ await callSubscriptionsAuth(rpc, 'setModelDefault', {
644
+ provider,
645
+ model,
646
+ ...(effort === undefined ? {} : { effort }),
647
+ });
648
+ if (!mountedRef.current)
649
+ return;
650
+ setModelDefaults((prev) => {
651
+ const section = prev[provider];
652
+ if (section === undefined)
653
+ return prev;
654
+ return {
655
+ ...prev,
656
+ [provider]: {
657
+ ...section,
658
+ models: section.models.map((entry) => {
659
+ if (entry.id !== model)
660
+ return entry;
661
+ if (effort !== undefined)
662
+ return { ...entry, configured: effort };
663
+ // Cleared: drop the key rather than keep the stale level, or the
664
+ // select would snap back and the header would keep counting it.
665
+ const { configured: _cleared, ...rest } = entry;
666
+ return rest;
667
+ }),
668
+ },
669
+ };
670
+ });
671
+ }
672
+ catch (error) {
673
+ if (mountedRef.current)
674
+ setModelDefaultsSaveErrors((prev) => ({ ...prev, [key]: messageOf(error) }));
675
+ }
676
+ finally {
677
+ if (mountedRef.current) {
678
+ setModelDefaultsSaving(current => current === key ? undefined : current);
679
+ // Drop the optimistic value: on success the server state now carries
680
+ // it, on failure the row must fall back to the real stored level
681
+ // rather than keep showing a change that did not land.
682
+ setModelDefaultsPending((prev) => {
683
+ const next = { ...prev };
684
+ delete next[key];
685
+ return next;
686
+ });
687
+ }
688
+ }
689
+ }, [rpc]);
412
690
  const login = useCallback(async (provider, method) => {
413
691
  if (rpc === undefined)
414
692
  return;
@@ -625,12 +903,55 @@ export function SubscriptionsSection(props) {
625
903
  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('setDefault'), onClick: () => {
626
904
  if (!account.isDefault)
627
905
  void setDefault(id, account.key);
628
- }, children: account.isDefault ? '★' : '☆' }), _jsx("span", { style: styles.accountName, children: display }), account.plan !== undefined && (_jsx("span", { style: styles.usagePlan, children: account.plan })), account.expiresAt !== undefined && (_jsx("span", { style: styles.statusLine, children: t('accountExpires', { date: new Date(account.expiresAt).toLocaleString() }) })), _jsx("button", { type: "button", style: { ...styles.button, marginLeft: 'auto', flexShrink: 0 }, onClick: () => { void logout(id, account.key, display, 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[usageKey] === true ? { opacity: 0.5, cursor: 'default' } : {} }, disabled: usageLoading[usageKey] === true, onClick: () => { void loadUsage(id, account.key); }, 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) => {
906
+ }, children: account.isDefault ? '★' : '☆' }), _jsx("span", { style: styles.accountName, children: display }), account.plan !== undefined && (_jsx("span", { style: styles.usagePlan, children: account.plan })), account.expiresAt !== undefined && (_jsx("span", { style: styles.statusLine, children: t('accountExpires', { date: new Date(account.expiresAt).toLocaleString() }) })), _jsx("button", { type: "button", style: { ...styles.button, marginLeft: 'auto', flexShrink: 0 }, onClick: () => { void logout(id, account.key, display, 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[usageKey] === true ? { opacity: 0.5, cursor: 'default' } : {} }, disabled: usageLoading[usageKey] === true, onClick: () => { void loadUsage(id, account.key, true); }, 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) => {
629
907
  const percent = Math.min(100, Math.max(0, window.usedPercent));
630
908
  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
631
909
  && ` · ${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));
632
910
  })] }))] }, account.key));
633
- }), _jsxs("div", { style: styles.actions, children: [!busy && accounts.length === 0 && (_jsx("button", { type: "button", style: styles.button, onClick: () => { void login(id); }, children: t('login') })), !busy && accounts.length > 0 && id === 'claude' && (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", style: styles.button, onClick: () => { void login(id, 'oauth'); }, children: t('addAccountOAuth') }), _jsx("button", { type: "button", style: styles.button, onClick: () => { void login(id, 'keychain'); }, children: t('addAccountKeychain') })] })), !busy && accounts.length > 0 && id !== 'claude' && (_jsx("button", { type: "button", style: styles.button, onClick: () => { void login(id); }, children: t('addAccount') })), busy && (_jsx("button", { type: "button", style: styles.button, onClick: () => { void cancel(id); }, children: t('cancel') }))] }), !busy && accounts.length > 0 && (_jsx("p", { style: styles.statusLine, children: t('addAccountHint') })), busy && deviceCode !== undefined && (_jsxs("div", { style: styles.deviceCode, children: [_jsx("span", { style: styles.statusLine, children: t('deviceCodePrompt') }), _jsx("span", { style: styles.deviceCodeText, children: deviceCode.userCode }), _jsxs("div", { style: styles.actions, children: [_jsx("button", { type: "button", style: styles.button, onClick: () => { copyDeviceCode(id, deviceCode.userCode); }, children: copiedCode === id ? t('deviceCodeCopied') : t('deviceCodeCopy') }), _jsx("button", { type: "button", style: styles.button, onClick: () => { window.open(deviceCode.verificationUrl, '_blank', 'noopener'); }, children: t('deviceCodeOpenPage') })] })] })), busy && deviceCode === undefined && (_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));
911
+ }), _jsxs("div", { style: styles.actions, children: [!busy && accounts.length === 0 && (_jsx("button", { type: "button", style: styles.button, onClick: () => { void login(id); }, children: t('login') })), !busy && accounts.length > 0 && id === 'claude' && (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", style: styles.button, onClick: () => { void login(id, 'oauth'); }, children: t('addAccountOAuth') }), _jsx("button", { type: "button", style: styles.button, onClick: () => { void login(id, 'keychain'); }, children: t('addAccountKeychain') })] })), !busy && accounts.length > 0 && id !== 'claude' && (_jsx("button", { type: "button", style: styles.button, onClick: () => { void login(id); }, children: t('addAccount') })), busy && (_jsx("button", { type: "button", style: styles.button, onClick: () => { void cancel(id); }, children: t('cancel') }))] }), !busy && accounts.length > 0 && (_jsx("p", { style: styles.statusLine, children: t('addAccountHint') })), accounts.length > 0 && (() => {
912
+ // Collapsed by default: providers with a large catalog (Copilot
913
+ // lists dozens of models) must not push the page down. The
914
+ // header carries the summary so the collapsed state still says
915
+ // how many models are overridden. The section is per provider,
916
+ // not per account: the override keys off the model id, which the
917
+ // pool shares across a provider's accounts.
918
+ const open = modelDefaultsOpen[id] === true;
919
+ const catalog = modelDefaults[id];
920
+ const filter = modelDefaultsFilters[id] ?? '';
921
+ const view = deriveModelDefaultsView(catalog?.models, filter);
922
+ const saveErrors = Object.entries(modelDefaultsSaveErrors).filter(([key]) => key.startsWith(`${id}/`));
923
+ return (_jsxs("div", { style: styles.defaultEffort, children: [_jsxs("button", { type: "button", style: styles.defaultEffortToggle, "aria-expanded": open, onClick: () => { toggleModelDefaults(id); }, children: [_jsx("span", { style: styles.usageTitle, children: t('modelDefaultsTitle') }), _jsx("span", { style: styles.usagePlan, children: catalog === undefined
924
+ ? (modelDefaultsLoading ? t('modelDefaultsLoading') : '')
925
+ : view.total === 0
926
+ ? t('modelDefaultsSummaryEmpty')
927
+ : view.overridden === 0
928
+ ? t('modelDefaultsSummaryNone', { total: view.total })
929
+ : t('modelDefaultsSummary', { total: view.total, configured: view.overridden }) }), _jsx("span", { style: styles.defaultEffortChevron, "aria-hidden": "true", children: open ? '▲' : '▼' })] }), saveErrors.map(([key, message]) => (
930
+ // Named: the failing row may be scrolled out of the
931
+ // bounded list, and several anonymous "Save failed" lines
932
+ // cannot be told apart.
933
+ _jsx("p", { style: styles.errorLine, role: "alert", children: t('modelDefaultsSaveFailedNamed', {
934
+ model: catalog?.models.find(entry => `${id}/${entry.id}` === key)?.name
935
+ ?? key.slice(id.length + 1),
936
+ message,
937
+ }) }, key))), open && (_jsxs(_Fragment, { children: [_jsx("p", { style: styles.statusLine, children: t('modelDefaultsHint') }), modelDefaultsLoadError !== undefined && (_jsxs(_Fragment, { children: [_jsx("p", { style: styles.errorLine, children: t('modelDefaultsLoadFailed', { message: modelDefaultsLoadError }) }), _jsx("div", { style: styles.actions, children: _jsx("button", { type: "button", style: styles.button, onClick: () => {
938
+ // Clearing the latch lets the effect pick the
939
+ // fetch back up on the next render.
940
+ setModelDefaultsLoadError(undefined);
941
+ }, children: t('modelDefaultsRetry') }) })] })), modelDefaultsLoadError === undefined && catalog === undefined && (_jsx("p", { style: styles.statusLine, children: t('modelDefaultsLoading') })), view.showFilter && (_jsx("input", { style: styles.defaultEffortFilter, value: filter, placeholder: t('modelDefaultsFilterPlaceholder'), "aria-label": t('modelDefaultsFilterPlaceholder'), onChange: (event) => {
942
+ setModelDefaultsFilters(prev => ({ ...prev, [id]: event.target.value }));
943
+ } })), view.shown.length > 0 && (_jsx("div", { style: styles.defaultEffortList, children: view.shown.map((model) => {
944
+ const rowKey = `${id}/${model.id}`;
945
+ const saving = modelDefaultsSaving === rowKey;
946
+ // Ids carry the provider: a model id alone repeats
947
+ // across cards, and duplicate ids break the label
948
+ // association the select relies on.
949
+ const labelId = `dsh-model-default-${id}-${model.id}`;
950
+ return (_jsxs("div", { style: styles.defaultEffortRow, children: [_jsx("span", { id: labelId, style: styles.defaultEffortName, title: model.id, children: model.name }), saving && (_jsx("span", { style: styles.defaultEffortSaving, children: t('modelDefaultsSaving') })), _jsxs("select", { style: styles.defaultEffortSelect, "aria-labelledby": labelId, value: modelDefaultsPending[rowKey] ?? model.configured ?? '', disabled: saving, onChange: (event) => {
951
+ void setModelDefault(id, model.id, event.target.value === '' ? undefined : event.target.value);
952
+ }, children: [_jsx("option", { value: "", children: t('modelDefaultsFollowProvider') }), model.efforts.map(effort => (_jsx("option", { value: effort.id, children: effort.name }, effort.id)))] })] }, model.id));
953
+ }) })), catalog !== undefined && view.shown.length === 0 && filter.trim() !== '' && (_jsx("p", { style: styles.statusLine, children: t('modelDefaultsFilterEmpty', { query: filter.trim() }) })), view.withoutEfforts > 0 && (_jsx("p", { style: styles.statusLine, children: t('modelDefaultsNoLevels', { count: view.withoutEfforts }) }))] }))] }));
954
+ })(), busy && deviceCode !== undefined && (_jsxs("div", { style: styles.deviceCode, children: [_jsx("span", { style: styles.statusLine, children: t('deviceCodePrompt') }), _jsx("span", { style: styles.deviceCodeText, children: deviceCode.userCode }), _jsxs("div", { style: styles.actions, children: [_jsx("button", { type: "button", style: styles.button, onClick: () => { copyDeviceCode(id, deviceCode.userCode); }, children: copiedCode === id ? t('deviceCodeCopied') : t('deviceCodeCopy') }), _jsx("button", { type: "button", style: styles.button, onClick: () => { window.open(deviceCode.verificationUrl, '_blank', 'noopener'); }, children: t('deviceCodeOpenPage') })] })] })), busy && deviceCode === undefined && (_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));
634
955
  }), proxyOpen && (_jsx("div", { style: styles.modalOverlay, onClick: () => setProxyOpen(false), children: _jsxs("div", { style: styles.modal, onClick: event => event.stopPropagation(), children: [_jsxs("div", { style: styles.modalHeader, children: [_jsx("span", { style: styles.modalTitle, children: t('proxyDialogTitle') }), _jsx("button", { type: "button", style: { ...styles.button, marginLeft: 'auto' }, onClick: () => setProxyOpen(false), children: t('proxyDialogClose') })] }), _jsxs("label", { style: styles.proxyCheck, children: [_jsx("input", { type: "checkbox", checked: proxyEnabled, onChange: event => setProxyEnabled(event.target.checked) }), _jsx("span", { children: t('proxyEnabled') })] }), _jsxs("label", { style: styles.proxyField, children: [_jsx("span", { style: styles.proxyLabel, children: t('proxyUrl') }), _jsx("input", { style: styles.proxyInput, value: proxyUrl, placeholder: t('proxyUrlPlaceholder'), onChange: event => setProxyUrl(event.target.value) }), _jsx("p", { style: styles.proxyHint, children: t('proxyUrlHint') })] }), _jsxs("label", { style: styles.proxyField, children: [_jsx("span", { style: styles.proxyLabel, children: t('proxyUsername') }), _jsx("input", { style: styles.proxyInput, value: proxyUsername, placeholder: t('proxyUsernamePlaceholder'), onChange: event => setProxyUsername(event.target.value) })] }), _jsxs("div", { style: styles.proxyField, children: [_jsx("span", { style: styles.proxyLabel, children: t('proxyPassword') }), _jsx("input", { type: "password", style: styles.proxyInput, value: proxyPassword, placeholder: t('proxyPasswordPlaceholder'), onChange: event => setProxyPassword(event.target.value) }), _jsxs("label", { style: styles.proxyCheck, children: [_jsx("input", { type: "checkbox", checked: proxyClearPassword, onChange: event => setProxyClearPassword(event.target.checked) }), _jsx("span", { children: t('proxyClearPassword') })] })] }), _jsxs("label", { style: styles.proxyField, children: [_jsx("span", { style: styles.proxyLabel, children: t('proxyBypass') }), _jsx("input", { style: styles.proxyInput, value: proxyBypass, placeholder: t('proxyBypassPlaceholder'), onChange: event => setProxyBypass(event.target.value) }), _jsx("p", { style: styles.proxyHint, children: t('proxyBypassHint') })] }), _jsx("p", { style: styles.proxyHint, children: t('proxyNote') }), proxyMessage !== undefined && (_jsx("p", { style: { ...styles.proxyMessage, color: messageColor(proxyMessage.tone) }, children: proxyMessage.text })), proxyTestResult !== undefined && (_jsx("p", { style: {
635
956
  ...styles.proxyMessage,
636
957
  color: proxyTestResult.ok
@@ -1,5 +1,5 @@
1
1
  import type { ConnectionHandle } from '@deepseek-ai/dsh-api-remotes/client';
2
- import type { ToolCallBlock } from '@deepseek-ai/dsh-client-runtime/client';
2
+ import type { ToolCallBlock } from '@deepseek-ai/dsh-client-ui-conversation/client';
3
3
  import type { SubscriptionsKey } from './locales.js';
4
4
  /** Mirror of ui-tool's ToolCallOwnerProps (see ImageGenerateToolview). */
5
5
  interface ToolCallOwnerProps {
@@ -1,12 +1,4 @@
1
- /**
2
- * Subscription OAuth login page, browser half. Registers the Subscriptions
3
- * settings section; every login state fact arrives through the node half's
4
- * `/subscriptions-auth` RPC channel — this plugin holds no credential state of its
5
- * own. Section copy rides the client locale service: one 'settings.subscriptions'
6
- * namespace with zh/en dictionaries, rebound per read so the nav label and
7
- * page text follow the active locale.
8
- */
9
- import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client';
1
+ import type { Context as ClientContext } from '@deepseek-ai/cordis';
10
2
  import type { SubscriptionsKey } from './locales.js';
11
3
  export type { SubscriptionsSectionInjected, SubscriptionsSectionProps } from './SubscriptionsSection.js';
12
4
  export type { ImageGenerateToolviewInjected, ImageGenerateToolviewProps } from './ImageGenerateToolview.js';
@@ -33,8 +33,8 @@ export function apply(ctx) {
33
33
  document.head.appendChild(style);
34
34
  return () => style.remove();
35
35
  }, 'dsh-plugin-subscriptions: settings panel breathing room');
36
- // The client-runtime Context merge types `connection` as the host handle;
37
- // in the browser shell the same key holds the full client ConnectionHandle.
36
+ // The shell's Context merge types `connection` as the host handle; in the
37
+ // browser shell the same key holds the full client ConnectionHandle.
38
38
  const connection = ctx.get('connection');
39
39
  const t = ctx.locale.bind(NS);
40
40
  const injected = () => ({ rpc: connection.rpc, t });
@@ -68,13 +68,16 @@ export function apply(ctx) {
68
68
  // The composer Speed toggle (codex fast tier) sits in the right tool row,
69
69
  // just left of the model selector; the framework synthesizes its `t` seat
70
70
  // from `locale: NS`, and the inject face binds each session's RPC calls.
71
+ // The current-model read rides ui-model-selection's `modelDirectories`
72
+ // service, resolved lazily so registration order never matters.
73
+ const models = () => ctx.get('modelDirectories');
71
74
  ctx.slots.inject('conversation.input.right', () => ctx.slots.register({
72
75
  name: 'conversation.input.right',
73
76
  id: 'codex-speed',
74
77
  order: 0,
75
78
  locale: NS,
76
79
  inject: (sessionId) => ({
77
- loadSpeed: createSpeedLoader(connection, sessionId),
80
+ loadSpeed: createSpeedLoader(connection, models, sessionId),
78
81
  setSpeed: createSpeedSetter(connection, sessionId),
79
82
  }),
80
83
  }, SpeedSelect));
@@ -92,7 +95,7 @@ export function apply(ctx) {
92
95
  ui: {
93
96
  kind: 'popupSelect',
94
97
  options: async (session) => {
95
- const state = await createSpeedLoader(connection, session.sessionId)();
98
+ const state = await createSpeedLoader(connection, models, session.sessionId)();
96
99
  if (!state.visible)
97
100
  throw new Error(t('commandFastUnavailable'));
98
101
  return [
@@ -37,6 +37,20 @@ export declare const en: {
37
37
  usageWindow: string;
38
38
  usageResets: string;
39
39
  usagePlan: string;
40
+ modelDefaultsTitle: string;
41
+ modelDefaultsHint: string;
42
+ modelDefaultsFollowProvider: string;
43
+ modelDefaultsNoLevels: string;
44
+ modelDefaultsLoading: string;
45
+ modelDefaultsLoadFailed: string;
46
+ modelDefaultsRetry: string;
47
+ modelDefaultsSaving: string;
48
+ modelDefaultsSaveFailedNamed: string;
49
+ modelDefaultsSummary: string;
50
+ modelDefaultsSummaryNone: string;
51
+ modelDefaultsSummaryEmpty: string;
52
+ modelDefaultsFilterPlaceholder: string;
53
+ modelDefaultsFilterEmpty: string;
40
54
  generating: string;
41
55
  image: string;
42
56
  viewImage: string;
@@ -126,6 +140,20 @@ export declare const zh: {
126
140
  usageWindow: string;
127
141
  usageResets: string;
128
142
  usagePlan: string;
143
+ modelDefaultsTitle: string;
144
+ modelDefaultsHint: string;
145
+ modelDefaultsFollowProvider: string;
146
+ modelDefaultsNoLevels: string;
147
+ modelDefaultsLoading: string;
148
+ modelDefaultsLoadFailed: string;
149
+ modelDefaultsRetry: string;
150
+ modelDefaultsSaving: string;
151
+ modelDefaultsSaveFailedNamed: string;
152
+ modelDefaultsSummary: string;
153
+ modelDefaultsSummaryNone: string;
154
+ modelDefaultsSummaryEmpty: string;
155
+ modelDefaultsFilterPlaceholder: string;
156
+ modelDefaultsFilterEmpty: string;
129
157
  generating: string;
130
158
  image: string;
131
159
  viewImage: string;
@@ -37,6 +37,20 @@ export const en = {
37
37
  usageWindow: 'Window',
38
38
  usageResets: 'resets {date}',
39
39
  usagePlan: 'Plan: {plan}',
40
+ modelDefaultsTitle: 'Default reasoning effort',
41
+ modelDefaultsHint: 'The model picker preselects this level when you switch to the model; models without levels always follow the provider.',
42
+ modelDefaultsFollowProvider: 'Follow provider',
43
+ modelDefaultsNoLevels: '{count} model(s) advertise no reasoning levels and always follow the provider.',
44
+ modelDefaultsLoading: 'Loading models…',
45
+ modelDefaultsLoadFailed: 'Failed to load models: {message}',
46
+ modelDefaultsRetry: 'Retry',
47
+ modelDefaultsSaving: 'Saving…',
48
+ modelDefaultsSaveFailedNamed: 'Save failed for {model}: {message}',
49
+ modelDefaultsSummary: '{total} model(s) · {configured} overridden',
50
+ modelDefaultsSummaryNone: '{total} model(s) · following the provider',
51
+ modelDefaultsSummaryEmpty: 'No model advertises reasoning levels',
52
+ modelDefaultsFilterPlaceholder: 'Filter models',
53
+ modelDefaultsFilterEmpty: 'No model matches “{query}”.',
40
54
  generating: 'Generating image…',
41
55
  image: 'image',
42
56
  viewImage: 'View image',
@@ -126,6 +140,20 @@ export const zh = {
126
140
  usageWindow: '窗口',
127
141
  usageResets: '{date} 重置',
128
142
  usagePlan: '计划:{plan}',
143
+ modelDefaultsTitle: '默认推理档',
144
+ modelDefaultsHint: '切换到该模型时,模型选择器会预选此档位;没有推理档的模型始终跟随服务商默认。',
145
+ modelDefaultsFollowProvider: '跟随服务商',
146
+ modelDefaultsNoLevels: '{count} 个模型未声明推理档,始终跟随服务商默认。',
147
+ modelDefaultsLoading: '加载模型中…',
148
+ modelDefaultsLoadFailed: '模型加载失败:{message}',
149
+ modelDefaultsRetry: '重试',
150
+ modelDefaultsSaving: '保存中…',
151
+ modelDefaultsSaveFailedNamed: '{model} 保存失败:{message}',
152
+ modelDefaultsSummary: '{total} 个模型 · {configured} 个已覆盖',
153
+ modelDefaultsSummaryNone: '{total} 个模型 · 全部跟随服务商',
154
+ modelDefaultsSummaryEmpty: '没有模型声明推理档',
155
+ modelDefaultsFilterPlaceholder: '筛选模型',
156
+ modelDefaultsFilterEmpty: '没有匹配「{query}」的模型。',
129
157
  generating: '正在生成图片…',
130
158
  image: '图片',
131
159
  viewImage: '查看图片',