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
package/lib/client.js CHANGED
@@ -70,6 +70,20 @@ const en = {
70
70
  usageWindow: "Window",
71
71
  usageResets: "resets {date}",
72
72
  usagePlan: "Plan: {plan}",
73
+ modelDefaultsTitle: "Default reasoning effort",
74
+ modelDefaultsHint: "The model picker preselects this level when you switch to the model; models without levels always follow the provider.",
75
+ modelDefaultsFollowProvider: "Follow provider",
76
+ modelDefaultsNoLevels: "{count} model(s) advertise no reasoning levels and always follow the provider.",
77
+ modelDefaultsLoading: "Loading models…",
78
+ modelDefaultsLoadFailed: "Failed to load models: {message}",
79
+ modelDefaultsRetry: "Retry",
80
+ modelDefaultsSaving: "Saving…",
81
+ modelDefaultsSaveFailedNamed: "Save failed for {model}: {message}",
82
+ modelDefaultsSummary: "{total} model(s) · {configured} overridden",
83
+ modelDefaultsSummaryNone: "{total} model(s) · following the provider",
84
+ modelDefaultsSummaryEmpty: "No model advertises reasoning levels",
85
+ modelDefaultsFilterPlaceholder: "Filter models",
86
+ modelDefaultsFilterEmpty: "No model matches “{query}”.",
73
87
  generating: "Generating image…",
74
88
  image: "image",
75
89
  viewImage: "View image",
@@ -159,6 +173,20 @@ const zh = {
159
173
  usageWindow: "窗口",
160
174
  usageResets: "{date} 重置",
161
175
  usagePlan: "计划:{plan}",
176
+ modelDefaultsTitle: "默认推理档",
177
+ modelDefaultsHint: "切换到该模型时,模型选择器会预选此档位;没有推理档的模型始终跟随服务商默认。",
178
+ modelDefaultsFollowProvider: "跟随服务商",
179
+ modelDefaultsNoLevels: "{count} 个模型未声明推理档,始终跟随服务商默认。",
180
+ modelDefaultsLoading: "加载模型中…",
181
+ modelDefaultsLoadFailed: "模型加载失败:{message}",
182
+ modelDefaultsRetry: "重试",
183
+ modelDefaultsSaving: "保存中…",
184
+ modelDefaultsSaveFailedNamed: "{model} 保存失败:{message}",
185
+ modelDefaultsSummary: "{total} 个模型 · {configured} 个已覆盖",
186
+ modelDefaultsSummaryNone: "{total} 个模型 · 全部跟随服务商",
187
+ modelDefaultsSummaryEmpty: "没有模型声明推理档",
188
+ modelDefaultsFilterPlaceholder: "筛选模型",
189
+ modelDefaultsFilterEmpty: "没有匹配「{query}」的模型。",
162
190
  generating: "正在生成图片…",
163
191
  image: "图片",
164
192
  viewImage: "查看图片",
@@ -217,6 +245,17 @@ const zh = {
217
245
  const SUBSCRIPTIONS_AUTH_CHANNEL$2 = "/subscriptions-auth";
218
246
  /** Poll cadence while a provider login attempt is busy. */
219
247
  const POLL_INTERVAL_MS$1 = 2e3;
248
+ /**
249
+ * Model count above which the expanded default-effort list also offers a name
250
+ * filter; below it the list is short enough to scan.
251
+ */
252
+ const MODEL_FILTER_THRESHOLD = 8;
253
+ /**
254
+ * Height cap of the expanded default-effort list, in px. Past it the list
255
+ * scrolls internally so a provider with dozens of models cannot stretch the
256
+ * card (roughly 7 rows, which keeps the next card's header on screen).
257
+ */
258
+ const MODEL_LIST_MAX_HEIGHT = 260;
220
259
  /** Card display metadata, in page order (names are brand names, not translated). */
221
260
  const PROVIDERS = [
222
261
  {
@@ -456,6 +495,90 @@ const styles$4 = {
456
495
  height: "100%",
457
496
  borderRadius: 3
458
497
  },
498
+ defaultEffort: {
499
+ display: "flex",
500
+ flexDirection: "column",
501
+ gap: 6,
502
+ marginTop: 4,
503
+ borderTop: "1px solid var(--dsw-alias-border-l2)",
504
+ paddingTop: 8
505
+ },
506
+ defaultEffortToggle: {
507
+ boxSizing: "border-box",
508
+ display: "flex",
509
+ alignItems: "center",
510
+ gap: 8,
511
+ width: "100%",
512
+ padding: 0,
513
+ border: "none",
514
+ background: "transparent",
515
+ font: "inherit",
516
+ textAlign: "left",
517
+ cursor: "pointer"
518
+ },
519
+ defaultEffortChevron: {
520
+ marginLeft: "auto",
521
+ flexShrink: 0,
522
+ fontSize: 10,
523
+ lineHeight: "18px",
524
+ color: "var(--dsw-alias-label-tertiary)"
525
+ },
526
+ defaultEffortList: {
527
+ display: "flex",
528
+ flexDirection: "column",
529
+ gap: 6,
530
+ maxHeight: MODEL_LIST_MAX_HEIGHT,
531
+ overflowY: "auto",
532
+ paddingRight: 2
533
+ },
534
+ defaultEffortRow: {
535
+ display: "flex",
536
+ alignItems: "center",
537
+ justifyContent: "space-between",
538
+ gap: 8
539
+ },
540
+ defaultEffortName: {
541
+ fontSize: 12,
542
+ lineHeight: "18px",
543
+ color: "var(--dsw-alias-label-primary)",
544
+ overflow: "hidden",
545
+ textOverflow: "ellipsis",
546
+ whiteSpace: "nowrap"
547
+ },
548
+ defaultEffortSaving: {
549
+ marginLeft: "auto",
550
+ flexShrink: 0,
551
+ fontSize: 12,
552
+ lineHeight: "18px",
553
+ color: "var(--dsw-alias-label-tertiary)"
554
+ },
555
+ defaultEffortSelect: {
556
+ maxWidth: 220,
557
+ flexShrink: 0,
558
+ height: 28,
559
+ boxSizing: "border-box",
560
+ border: "1px solid var(--dsw-alias-border-l2)",
561
+ borderRadius: 8,
562
+ padding: "0 8px",
563
+ font: "inherit",
564
+ fontSize: 12,
565
+ lineHeight: "18px",
566
+ background: "var(--dsw-alias-bg-layer-1)",
567
+ color: "var(--dsw-alias-label-primary)"
568
+ },
569
+ defaultEffortFilter: {
570
+ height: 28,
571
+ width: "100%",
572
+ boxSizing: "border-box",
573
+ border: "1px solid var(--dsw-alias-border-l2)",
574
+ borderRadius: 8,
575
+ padding: "0 8px",
576
+ font: "inherit",
577
+ fontSize: 12,
578
+ lineHeight: "18px",
579
+ background: "var(--dsw-alias-bg-layer-1)",
580
+ color: "var(--dsw-alias-label-primary)"
581
+ },
459
582
  manual: {
460
583
  marginTop: 4,
461
584
  fontSize: 12,
@@ -591,6 +714,16 @@ function dotColor(status) {
591
714
  return "var(--dsw-alias-label-dimmed)";
592
715
  }
593
716
  /**
717
+ * Whether a provider has at least one connected account. Multi-account made
718
+ * "logged in" a property of the account list rather than a flag, so every
719
+ * logged-in test goes through this one predicate.
720
+ * @param status - the provider's last reported state, possibly absent.
721
+ * @returns true when the provider serves at least one account.
722
+ */
723
+ function hasAccount(status) {
724
+ return (status?.accounts.length ?? 0) > 0;
725
+ }
726
+ /**
594
727
  * One-line status text for one provider state.
595
728
  * @param t - section translate.
596
729
  * @param status - the provider's last reported state.
@@ -631,6 +764,55 @@ function messageColor(tone) {
631
764
  return tone === "error" ? "var(--dsw-alias-state-error-primary)" : "var(--dsw-alias-state-success-primary)";
632
765
  }
633
766
  /**
767
+ * Derive one provider's default-effort section from its catalog and filter.
768
+ * Pure so the collapsed-header counts and the filter stay testable without a
769
+ * DOM: rows come only from models that advertise levels, the count of the rest
770
+ * rides as one line, and the filter matches display name or model id.
771
+ * @param models - the provider's catalog models, or undefined while loading.
772
+ * @param filter - the raw filter input (trimmed and lowercased here).
773
+ * @returns the section's rows and header counts.
774
+ */
775
+ function deriveModelDefaultsView(models, filter) {
776
+ const all = models ?? [];
777
+ const withEfforts = all.filter((model) => model.efforts.length > 0);
778
+ const query = filter.trim().toLowerCase();
779
+ return {
780
+ shown: query === "" ? withEfforts : withEfforts.filter((model) => model.name.toLowerCase().includes(query) || model.id.toLowerCase().includes(query)),
781
+ total: withEfforts.length,
782
+ overridden: withEfforts.filter((model) => model.configured !== void 0).length,
783
+ withoutEfforts: all.length - withEfforts.length,
784
+ showFilter: withEfforts.length > MODEL_FILTER_THRESHOLD
785
+ };
786
+ }
787
+ /**
788
+ * Whether the default-effort catalog needs (re)fetching.
789
+ *
790
+ * Fetching is gated on an *attempt* signature rather than on the payload
791
+ * being empty: an empty answer is a legitimate result (a narrowed
792
+ * `config.providers`, or a catalog that is momentarily unavailable), and
793
+ * treating it as "not loaded yet" re-ran this effect forever. The signature
794
+ * also covers the accounts, so logging a second provider in refetches
795
+ * instead of leaving that card on the previous answer.
796
+ * @param input - the decision inputs.
797
+ * @returns true when the caller should start a fetch.
798
+ */
799
+ function shouldFetchModelDefaults(input) {
800
+ if (input.failed) return false;
801
+ if (input.loggedIn.length === 0) return false;
802
+ if (!input.open.some((provider) => input.loggedIn.includes(provider))) return false;
803
+ return input.loadedFor !== input.signature;
804
+ }
805
+ /**
806
+ * Stable signature of the accounts a catalog answer depends on. A change
807
+ * means a previous answer is stale (an account arrived or left), so the next
808
+ * open disclosure refetches.
809
+ * @param statuses - the per-provider status snapshot.
810
+ * @returns a signature string, stable across renders with equal accounts.
811
+ */
812
+ function modelDefaultsSignature(statuses) {
813
+ return PROVIDERS.map(({ id }) => `${id}:${(statuses[id]?.accounts ?? []).map((account) => account.key).sort().join(",")}`).join("|");
814
+ }
815
+ /**
634
816
  * The Subscriptions settings page component.
635
817
  * @param props - the slot inject face ({@link SubscriptionsSectionInjected}).
636
818
  * @returns the section body, or a notice while the RPC face is absent.
@@ -672,6 +854,24 @@ function SubscriptionsSection(props) {
672
854
  const [proxyTesting, setProxyTesting] = (0, react.useState)(false);
673
855
  const [proxyMessage, setProxyMessage] = (0, react.useState)(void 0);
674
856
  const [proxyTestResult, setProxyTestResult] = (0, react.useState)(void 0);
857
+ /** Per-model default-effort picker state as answered by `modelDefaults`. */
858
+ const [modelDefaults, setModelDefaults] = (0, react.useState)({});
859
+ const [modelDefaultsLoading, setModelDefaultsLoading] = (0, react.useState)(false);
860
+ const [modelDefaultsLoadError, setModelDefaultsLoadError] = (0, react.useState)(void 0);
861
+ /** One set in flight: the `${provider}/${model}` key. */
862
+ const [modelDefaultsSaving, setModelDefaultsSaving] = (0, react.useState)(void 0);
863
+ /** Per-model save failures, keyed `${provider}/${model}`. */
864
+ const [modelDefaultsSaveErrors, setModelDefaultsSaveErrors] = (0, react.useState)({});
865
+ /** Providers whose default-effort disclosure is open (collapsed by default). */
866
+ const [modelDefaultsOpen, setModelDefaultsOpen] = (0, react.useState)({});
867
+ /** Per-provider name filter of the expanded list. */
868
+ const [modelDefaultsFilters, setModelDefaultsFilters] = (0, react.useState)({});
869
+ /** Account signature the last completed catalog fetch was answered for. */
870
+ const [modelDefaultsLoadedFor, setModelDefaultsLoadedFor] = (0, react.useState)(void 0);
871
+ /** Optimistic in-flight selections, keyed `${provider}/${model}` ('' = follow provider). */
872
+ const [modelDefaultsPending, setModelDefaultsPending] = (0, react.useState)({});
873
+ /** Guard the catalog effect against concurrent loads. */
874
+ const modelDefaultsInflightRef = (0, react.useRef)(false);
675
875
  const setProviderError = (0, react.useCallback)((provider, message) => {
676
876
  if (!mountedRef.current) return;
677
877
  setErrors((prev) => {
@@ -733,7 +933,7 @@ function SubscriptionsSection(props) {
733
933
  pollersRef.current.clear();
734
934
  };
735
935
  }, [refresh, startPolling]);
736
- const loadUsage = (0, react.useCallback)(async (provider, account) => {
936
+ const loadUsage = (0, react.useCallback)(async (provider, account, force = false) => {
737
937
  const key = `${provider}:${account}`;
738
938
  if (rpc === void 0 || usageInflightRef.current.has(key)) return;
739
939
  usageInflightRef.current.add(key);
@@ -744,7 +944,8 @@ function SubscriptionsSection(props) {
744
944
  try {
745
945
  const usage = await callSubscriptionsAuth(rpc, "usage", {
746
946
  provider,
747
- account
947
+ account,
948
+ ...force ? { force: true } : {}
748
949
  });
749
950
  if (!mountedRef.current) return;
750
951
  setUsages((prev) => ({
@@ -784,6 +985,116 @@ function SubscriptionsSection(props) {
784
985
  usageErrors,
785
986
  loadUsage
786
987
  ]);
988
+ const loadModelDefaultsData = (0, react.useCallback)(async (signature) => {
989
+ if (rpc === void 0 || modelDefaultsInflightRef.current) return;
990
+ modelDefaultsInflightRef.current = true;
991
+ setModelDefaultsLoading(true);
992
+ try {
993
+ const catalog = await callSubscriptionsAuth(rpc, "modelDefaults", {});
994
+ if (!mountedRef.current) return;
995
+ const next = {};
996
+ for (const entry of catalog) next[entry.provider] = entry;
997
+ setModelDefaults(next);
998
+ setModelDefaultsLoadError(void 0);
999
+ setModelDefaultsLoadedFor(signature);
1000
+ setModelDefaultsSaveErrors({});
1001
+ } catch (error) {
1002
+ if (mountedRef.current) setModelDefaultsLoadError(messageOf(error));
1003
+ } finally {
1004
+ modelDefaultsInflightRef.current = false;
1005
+ if (mountedRef.current) setModelDefaultsLoading(false);
1006
+ }
1007
+ }, [rpc]);
1008
+ (0, react.useEffect)(() => {
1009
+ const loggedIn = PROVIDERS.filter(({ id }) => hasAccount(statuses[id])).map(({ id }) => id);
1010
+ const signature = modelDefaultsSignature(statuses);
1011
+ if (loggedIn.length > 0) {
1012
+ if (shouldFetchModelDefaults({
1013
+ loggedIn,
1014
+ open: PROVIDERS.filter(({ id }) => modelDefaultsOpen[id] === true).map(({ id }) => id),
1015
+ loadedFor: modelDefaultsLoadedFor,
1016
+ signature,
1017
+ failed: modelDefaultsLoadError !== void 0
1018
+ })) loadModelDefaultsData(signature);
1019
+ } else if (modelDefaultsLoadedFor !== void 0 || Object.keys(modelDefaults).length > 0 || Object.keys(modelDefaultsOpen).length > 0) {
1020
+ setModelDefaults({});
1021
+ setModelDefaultsSaveErrors({});
1022
+ setModelDefaultsLoadError(void 0);
1023
+ setModelDefaultsLoadedFor(void 0);
1024
+ setModelDefaultsLoading(false);
1025
+ setModelDefaultsOpen({});
1026
+ setModelDefaultsFilters({});
1027
+ }
1028
+ }, [
1029
+ statuses,
1030
+ modelDefaults,
1031
+ modelDefaultsOpen,
1032
+ modelDefaultsLoadError,
1033
+ modelDefaultsLoadedFor,
1034
+ loadModelDefaultsData
1035
+ ]);
1036
+ /** Open or close one provider's default-effort disclosure. */
1037
+ const toggleModelDefaults = (0, react.useCallback)((provider) => {
1038
+ setModelDefaultsOpen((prev) => ({
1039
+ ...prev,
1040
+ [provider]: prev[provider] !== true
1041
+ }));
1042
+ }, []);
1043
+ const setModelDefault = (0, react.useCallback)(async (provider, model, effort) => {
1044
+ if (rpc === void 0) return;
1045
+ const key = `${provider}/${model}`;
1046
+ setModelDefaultsSaving(key);
1047
+ setModelDefaultsPending((prev) => ({
1048
+ ...prev,
1049
+ [key]: effort ?? ""
1050
+ }));
1051
+ setModelDefaultsSaveErrors((prev) => {
1052
+ const next = { ...prev };
1053
+ delete next[key];
1054
+ return next;
1055
+ });
1056
+ try {
1057
+ await callSubscriptionsAuth(rpc, "setModelDefault", {
1058
+ provider,
1059
+ model,
1060
+ ...effort === void 0 ? {} : { effort }
1061
+ });
1062
+ if (!mountedRef.current) return;
1063
+ setModelDefaults((prev) => {
1064
+ const section = prev[provider];
1065
+ if (section === void 0) return prev;
1066
+ return {
1067
+ ...prev,
1068
+ [provider]: {
1069
+ ...section,
1070
+ models: section.models.map((entry) => {
1071
+ if (entry.id !== model) return entry;
1072
+ if (effort !== void 0) return {
1073
+ ...entry,
1074
+ configured: effort
1075
+ };
1076
+ const { configured: _cleared,...rest } = entry;
1077
+ return rest;
1078
+ })
1079
+ }
1080
+ };
1081
+ });
1082
+ } catch (error) {
1083
+ if (mountedRef.current) setModelDefaultsSaveErrors((prev) => ({
1084
+ ...prev,
1085
+ [key]: messageOf(error)
1086
+ }));
1087
+ } finally {
1088
+ if (mountedRef.current) {
1089
+ setModelDefaultsSaving((current) => current === key ? void 0 : current);
1090
+ setModelDefaultsPending((prev) => {
1091
+ const next = { ...prev };
1092
+ delete next[key];
1093
+ return next;
1094
+ });
1095
+ }
1096
+ }
1097
+ }, [rpc]);
787
1098
  const login = (0, react.useCallback)(async (provider, method) => {
788
1099
  if (rpc === void 0) return;
789
1100
  setProviderError(provider, void 0);
@@ -1148,7 +1459,7 @@ function SubscriptionsSection(props) {
1148
1459
  },
1149
1460
  disabled: usageLoading[usageKey] === true,
1150
1461
  onClick: () => {
1151
- loadUsage(id, account.key);
1462
+ loadUsage(id, account.key, true);
1152
1463
  },
1153
1464
  children: t("usageRefresh")
1154
1465
  })
@@ -1235,6 +1546,135 @@ function SubscriptionsSection(props) {
1235
1546
  style: styles$4.statusLine,
1236
1547
  children: t("addAccountHint")
1237
1548
  }),
1549
+ accounts.length > 0 && (() => {
1550
+ const open = modelDefaultsOpen[id] === true;
1551
+ const catalog = modelDefaults[id];
1552
+ const filter = modelDefaultsFilters[id] ?? "";
1553
+ const view = deriveModelDefaultsView(catalog?.models, filter);
1554
+ const saveErrors = Object.entries(modelDefaultsSaveErrors).filter(([key]) => key.startsWith(`${id}/`));
1555
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1556
+ style: styles$4.defaultEffort,
1557
+ children: [
1558
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1559
+ type: "button",
1560
+ style: styles$4.defaultEffortToggle,
1561
+ "aria-expanded": open,
1562
+ onClick: () => {
1563
+ toggleModelDefaults(id);
1564
+ },
1565
+ children: [
1566
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1567
+ style: styles$4.usageTitle,
1568
+ children: t("modelDefaultsTitle")
1569
+ }),
1570
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1571
+ style: styles$4.usagePlan,
1572
+ children: catalog === void 0 ? modelDefaultsLoading ? t("modelDefaultsLoading") : "" : view.total === 0 ? t("modelDefaultsSummaryEmpty") : view.overridden === 0 ? t("modelDefaultsSummaryNone", { total: view.total }) : t("modelDefaultsSummary", {
1573
+ total: view.total,
1574
+ configured: view.overridden
1575
+ })
1576
+ }),
1577
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1578
+ style: styles$4.defaultEffortChevron,
1579
+ "aria-hidden": "true",
1580
+ children: open ? "▲" : "▼"
1581
+ })
1582
+ ]
1583
+ }),
1584
+ saveErrors.map(([key, message]) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1585
+ style: styles$4.errorLine,
1586
+ role: "alert",
1587
+ children: t("modelDefaultsSaveFailedNamed", {
1588
+ model: catalog?.models.find((entry) => `${id}/${entry.id}` === key)?.name ?? key.slice(id.length + 1),
1589
+ message
1590
+ })
1591
+ }, key)),
1592
+ open && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1593
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1594
+ style: styles$4.statusLine,
1595
+ children: t("modelDefaultsHint")
1596
+ }),
1597
+ modelDefaultsLoadError !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1598
+ style: styles$4.errorLine,
1599
+ children: t("modelDefaultsLoadFailed", { message: modelDefaultsLoadError })
1600
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1601
+ style: styles$4.actions,
1602
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1603
+ type: "button",
1604
+ style: styles$4.button,
1605
+ onClick: () => {
1606
+ setModelDefaultsLoadError(void 0);
1607
+ },
1608
+ children: t("modelDefaultsRetry")
1609
+ })
1610
+ })] }),
1611
+ modelDefaultsLoadError === void 0 && catalog === void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1612
+ style: styles$4.statusLine,
1613
+ children: t("modelDefaultsLoading")
1614
+ }),
1615
+ view.showFilter && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1616
+ style: styles$4.defaultEffortFilter,
1617
+ value: filter,
1618
+ placeholder: t("modelDefaultsFilterPlaceholder"),
1619
+ "aria-label": t("modelDefaultsFilterPlaceholder"),
1620
+ onChange: (event) => {
1621
+ setModelDefaultsFilters((prev) => ({
1622
+ ...prev,
1623
+ [id]: event.target.value
1624
+ }));
1625
+ }
1626
+ }),
1627
+ view.shown.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1628
+ style: styles$4.defaultEffortList,
1629
+ children: view.shown.map((model) => {
1630
+ const rowKey = `${id}/${model.id}`;
1631
+ const saving = modelDefaultsSaving === rowKey;
1632
+ const labelId = `dsh-model-default-${id}-${model.id}`;
1633
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1634
+ style: styles$4.defaultEffortRow,
1635
+ children: [
1636
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1637
+ id: labelId,
1638
+ style: styles$4.defaultEffortName,
1639
+ title: model.id,
1640
+ children: model.name
1641
+ }),
1642
+ saving && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1643
+ style: styles$4.defaultEffortSaving,
1644
+ children: t("modelDefaultsSaving")
1645
+ }),
1646
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1647
+ style: styles$4.defaultEffortSelect,
1648
+ "aria-labelledby": labelId,
1649
+ value: modelDefaultsPending[rowKey] ?? model.configured ?? "",
1650
+ disabled: saving,
1651
+ onChange: (event) => {
1652
+ setModelDefault(id, model.id, event.target.value === "" ? void 0 : event.target.value);
1653
+ },
1654
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1655
+ value: "",
1656
+ children: t("modelDefaultsFollowProvider")
1657
+ }), model.efforts.map((effort) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1658
+ value: effort.id,
1659
+ children: effort.name
1660
+ }, effort.id))]
1661
+ })
1662
+ ]
1663
+ }, model.id);
1664
+ })
1665
+ }),
1666
+ catalog !== void 0 && view.shown.length === 0 && filter.trim() !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1667
+ style: styles$4.statusLine,
1668
+ children: t("modelDefaultsFilterEmpty", { query: filter.trim() })
1669
+ }),
1670
+ view.withoutEfforts > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1671
+ style: styles$4.statusLine,
1672
+ children: t("modelDefaultsNoLevels", { count: view.withoutEfforts })
1673
+ })
1674
+ ] })
1675
+ ]
1676
+ });
1677
+ })(),
1238
1678
  busy && deviceCode !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1239
1679
  style: styles$4.deviceCode,
1240
1680
  children: [
@@ -2092,14 +2532,21 @@ function VideoGenerateToolview(props) {
2092
2532
  * known state, so a transient failure never locks the toggle away.
2093
2533
  *
2094
2534
  * `sessionId` is a plain string: slot and command contexts brand it through
2095
- * different dsh-session copies, and only the API-client boundary needs one.
2535
+ * different dsh-session copies, and only the service boundary needs one.
2536
+ *
2537
+ * `models` resolves lazily per call: the ui-model-selection service may
2538
+ * register after this plugin applies, and a shell without it (no model seat
2539
+ * at all) simply keeps the toggle hidden.
2096
2540
  */
2097
- function createSpeedLoader(connection, sessionId) {
2541
+ function createSpeedLoader(connection, models, sessionId) {
2098
2542
  return async () => {
2099
2543
  const state = await callSubscriptionsAuth(connection.rpc, "speed", { sessionId });
2100
- const { result } = await connection.api.sessions.models({ sessionId });
2101
- if (!result.ok) throw new Error(`session.models failed: ${result.error.code}: ${result.error.message}`);
2102
- const current = result.value.current;
2544
+ const directories = models();
2545
+ if (directories === void 0) return {
2546
+ visible: false,
2547
+ tier: state.tier
2548
+ };
2549
+ const { current } = await directories.directoryFor(sessionId).load();
2103
2550
  return {
2104
2551
  visible: current !== null && current.provider === "codex" && state.fastModels.includes(current.model),
2105
2552
  tier: state.tier
@@ -2379,13 +2826,14 @@ function apply(ctx) {
2379
2826
  locale: NS,
2380
2827
  inject: videoToolviewInjected
2381
2828
  }, VideoGenerateToolview));
2829
+ const models = () => ctx.get("modelDirectories");
2382
2830
  ctx.slots.inject("conversation.input.right", () => ctx.slots.register({
2383
2831
  name: "conversation.input.right",
2384
2832
  id: "codex-speed",
2385
2833
  order: 0,
2386
2834
  locale: NS,
2387
2835
  inject: (sessionId) => ({
2388
- loadSpeed: createSpeedLoader(connection, sessionId),
2836
+ loadSpeed: createSpeedLoader(connection, models, sessionId),
2389
2837
  setSpeed: createSpeedSetter(connection, sessionId)
2390
2838
  })
2391
2839
  }, SpeedSelect));
@@ -2398,7 +2846,7 @@ function apply(ctx) {
2398
2846
  ui: {
2399
2847
  kind: "popupSelect",
2400
2848
  options: async (session) => {
2401
- const state = await createSpeedLoader(connection, session.sessionId)();
2849
+ const state = await createSpeedLoader(connection, models, session.sessionId)();
2402
2850
  if (!state.visible) throw new Error(t("commandFastUnavailable"));
2403
2851
  return [{
2404
2852
  id: "standard",