dsh-subscriptions 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/README.md +212 -180
  2. package/lib/auth/claude-code-creds.js +31 -9
  3. package/lib/auth/device-flow.js +16 -9
  4. package/lib/auth/oauth-flow.js +13 -10
  5. package/lib/auth/rpc.js +74 -23
  6. package/lib/auth/store.js +80 -31
  7. package/lib/client/CursorCard.d.ts +1 -1
  8. package/lib/client/CursorCard.js +50 -16
  9. package/lib/client/ExternalUsageCards.d.ts +1 -1
  10. package/lib/client/ExternalUsageCards.js +37 -16
  11. package/lib/client/ImageGallery.d.ts +2 -2
  12. package/lib/client/ImageGallery.js +92 -33
  13. package/lib/client/ImageGenerateToolview.js +50 -14
  14. package/lib/client/OllamaCard.d.ts +1 -1
  15. package/lib/client/OllamaCard.js +26 -8
  16. package/lib/client/OllamaSettingsManager.js +113 -30
  17. package/lib/client/ProviderAccountManager.d.ts +1 -1
  18. package/lib/client/ProviderAccountManager.js +164 -49
  19. package/lib/client/ProviderModelEditor.js +107 -26
  20. package/lib/client/SpeedSelect.d.ts +62 -14
  21. package/lib/client/SpeedSelect.js +257 -46
  22. package/lib/client/SubscriptionUsageBadge.d.ts +4 -10
  23. package/lib/client/SubscriptionUsageBadge.js +300 -112
  24. package/lib/client/SubscriptionsSection.js +258 -85
  25. package/lib/client/UsageBadgeDisplaySetting.js +35 -12
  26. package/lib/client/UsageMeter.d.ts +1 -1
  27. package/lib/client/UsageMeter.js +62 -24
  28. package/lib/client/VideoGenerateToolview.js +69 -17
  29. package/lib/client/account-preferences.d.ts +8 -0
  30. package/lib/client/account-preferences.js +20 -5
  31. package/lib/client/host-contract.d.ts +11 -2
  32. package/lib/client/host-contract.js +20 -3
  33. package/lib/client/index.d.ts +5 -5
  34. package/lib/client/index.js +34 -12
  35. package/lib/client/locales.d.ts +10 -0
  36. package/lib/client/locales.js +61 -51
  37. package/lib/client/ollama-draft.js +21 -12
  38. package/lib/client/reset-action.js +6 -4
  39. package/lib/client/reset-credits-view.d.ts +1 -1
  40. package/lib/client/reset-credits-view.js +66 -17
  41. package/lib/client/subscription-card-styles.js +94 -24
  42. package/lib/client/usage-color-preferences.js +4 -2
  43. package/lib/client/usage-pace.js +30 -12
  44. package/lib/client.js +287 -47
  45. package/lib/client.js.map +1 -1
  46. package/lib/http.js +1 -1
  47. package/lib/index.d.ts +3 -3
  48. package/lib/index.js +118 -64
  49. package/lib/model-defaults.js +4 -2
  50. package/lib/provider-settings.d.ts +12 -0
  51. package/lib/provider-settings.js +58 -16
  52. package/lib/providers/account-preferences.js +51 -22
  53. package/lib/providers/accounts.js +15 -9
  54. package/lib/providers/antigravity-oauth-client.js +2 -2
  55. package/lib/providers/antigravity.js +141 -68
  56. package/lib/providers/catalog-store.js +120 -41
  57. package/lib/providers/claude-cli-version.js +3 -3
  58. package/lib/providers/claude.d.ts +9 -2
  59. package/lib/providers/claude.js +153 -90
  60. package/lib/providers/codex-client-version.js +3 -3
  61. package/lib/providers/codex-search.js +40 -13
  62. package/lib/providers/codex.js +196 -112
  63. package/lib/providers/common.js +71 -39
  64. package/lib/providers/copilot.js +143 -82
  65. package/lib/providers/cursor-adapter.js +42 -18
  66. package/lib/providers/cursor-auth.d.ts +1 -1
  67. package/lib/providers/cursor-auth.js +69 -24
  68. package/lib/providers/cursor-model-settings.js +14 -5
  69. package/lib/providers/cursor-usage.js +27 -13
  70. package/lib/providers/external-usage-controller.js +14 -5
  71. package/lib/providers/external-usage.js +59 -23
  72. package/lib/providers/grok.js +152 -89
  73. package/lib/providers/image-pool.js +29 -11
  74. package/lib/providers/minimax-usage.js +31 -11
  75. package/lib/providers/npm-cli-version.js +10 -3
  76. package/lib/providers/ollama-settings.js +24 -8
  77. package/lib/providers/ollama-web.js +29 -11
  78. package/lib/providers/ollama.js +129 -66
  79. package/lib/providers/pool-family.js +11 -3
  80. package/lib/providers/pool-health.js +28 -8
  81. package/lib/providers/pool-usage.js +16 -9
  82. package/lib/providers/pool.d.ts +8 -0
  83. package/lib/providers/pool.js +81 -42
  84. package/lib/providers/rate-limit.js +8 -2
  85. package/lib/providers/replay.js +40 -17
  86. package/lib/providers/reset-redemption.js +18 -6
  87. package/lib/tools/image-generate.js +123 -65
  88. package/lib/tools/registration.js +5 -2
  89. package/lib/tools/video-generate.js +51 -31
  90. package/lib/tools/x-search.js +50 -22
  91. package/lib/translate/anthropic.js +85 -37
  92. package/lib/translate/antigravity-schema.js +71 -28
  93. package/lib/translate/antigravity-thinking.js +22 -9
  94. package/lib/translate/antigravity.js +141 -50
  95. package/lib/translate/chat-completions.js +67 -23
  96. package/lib/translate/resolved.js +55 -21
  97. package/lib/translate/responses.js +69 -29
  98. package/lib/translate/sse.js +4 -1
  99. package/package.json +26 -4
@@ -21,7 +21,7 @@ export function resetCountdownParts(ms) {
21
21
  { unit: 'hour', count: Math.floor((total % 1440) / 60) },
22
22
  { unit: 'minute', count: total % 60 },
23
23
  ];
24
- return parts.filter(part => part.count > 0).slice(0, 2);
24
+ return parts.filter((part) => part.count > 0).slice(0, 2);
25
25
  }
26
26
  /**
27
27
  * Whether the reported percentage still describes the window now. Only a fresh
@@ -30,10 +30,14 @@ export function resetCountdownParts(ms) {
30
30
  * drawn: where the window stands in time never depends on the reading.
31
31
  */
32
32
  export function isUsageFresh(window, observation, now = Date.now()) {
33
- return validUsage(window.usedPercent) && observation.stale !== true
34
- && observation.observedAt !== undefined && Number.isFinite(observation.observedAt)
35
- && observation.observedAt <= now && now - observation.observedAt <= USAGE_FRESHNESS_MS
36
- && (window.resetsAt === undefined || Number.isFinite(window.resetsAt) && window.resetsAt > now);
33
+ return (validUsage(window.usedPercent) &&
34
+ observation.stale !== true &&
35
+ observation.observedAt !== undefined &&
36
+ Number.isFinite(observation.observedAt) &&
37
+ observation.observedAt <= now &&
38
+ now - observation.observedAt <= USAGE_FRESHNESS_MS &&
39
+ (window.resetsAt === undefined ||
40
+ (Number.isFinite(window.resetsAt) && window.resetsAt > now)));
37
41
  }
38
42
  /**
39
43
  * How far through its window the clock is, from the window's own timing alone.
@@ -48,10 +52,19 @@ export function elapsedPercent(window, now = Date.now()) {
48
52
  if (end === undefined || !Number.isFinite(end) || end <= now)
49
53
  return undefined;
50
54
  const duration = window.windowDurationMs;
51
- const start = window.startsAt ?? (window.fixedWindow === true && duration !== undefined && Number.isFinite(duration) && duration > 0 ? end - duration : undefined);
52
- if (start === undefined || !Number.isFinite(start) || start >= end || start > now)
55
+ const start = window.startsAt ??
56
+ (window.fixedWindow === true &&
57
+ duration !== undefined &&
58
+ Number.isFinite(duration) &&
59
+ duration > 0
60
+ ? end - duration
61
+ : undefined);
62
+ if (start === undefined ||
63
+ !Number.isFinite(start) ||
64
+ start >= end ||
65
+ start > now)
53
66
  return undefined;
54
- return Math.min(100, Math.max(0, (now - start) / (end - start) * 100));
67
+ return Math.min(100, Math.max(0, ((now - start) / (end - start)) * 100));
55
68
  }
56
69
  export function usageColorState(used, elapsed, preset, fresh) {
57
70
  if (!validUsage(used))
@@ -65,8 +78,10 @@ export function usageColorState(used, elapsed, preset, fresh) {
65
78
  }
66
79
  export function usageBarColor(used, elapsed, preset = 'standard', fresh = true) {
67
80
  const state = usageColorState(used, elapsed, preset, fresh);
68
- return state === 'red' ? 'var(--dsw-alias-state-error-primary)'
69
- : state === 'yellow' ? 'var(--dsw-alias-state-warn-label)'
81
+ return state === 'red'
82
+ ? 'var(--dsw-alias-state-error-primary)'
83
+ : state === 'yellow'
84
+ ? 'var(--dsw-alias-state-warn-label)'
70
85
  : 'var(--dsw-alias-state-success-primary)';
71
86
  }
72
87
  /**
@@ -80,7 +95,10 @@ export function displayStartPercent(window, now = Date.now()) {
80
95
  const start = window.displayStartsAt;
81
96
  if (end === undefined || !Number.isFinite(end) || end <= now)
82
97
  return undefined;
83
- if (start === undefined || !Number.isFinite(start) || start >= end || start > now)
98
+ if (start === undefined ||
99
+ !Number.isFinite(start) ||
100
+ start >= end ||
101
+ start > now)
84
102
  return undefined;
85
- return Math.min(100, Math.max(0, (now - start) / (end - start) * 100));
103
+ return Math.min(100, Math.max(0, ((now - start) / (end - start)) * 100));
86
104
  }
package/lib/client.js CHANGED
@@ -72,6 +72,11 @@ const en = {
72
72
  modelsWebSearch: "Web search",
73
73
  modelsSave: "Save changes",
74
74
  modelsUnavailable: "Currently unavailable",
75
+ claudeCacheTtl: "Prompt cache lifetime",
76
+ claudeCacheTtlFollowConfig: "Follow config",
77
+ claudeCacheTtl5m: "5 minutes",
78
+ claudeCacheTtl1h: "1 hour",
79
+ claudeCacheTtlHint: "Overrides the claudePromptCacheTtl config for this Claude route; Follow config uses the configured value. Saved changes apply from the next request. One hour keeps cached prefixes longer but writes them at a higher input price; compaction and session-title requests stay at five minutes.",
75
80
  nav: "Subscriptions",
76
81
  intro: "Log a subscription provider in or out. Login opens the provider’s authorization page in a new tab; headless setups can paste the callback URL or code instead.",
77
82
  unavailable: "Connection unavailable; subscription status cannot be loaded.",
@@ -307,6 +312,11 @@ const zh = {
307
312
  modelsWebSearch: "网页搜索",
308
313
  modelsSave: "保存更改",
309
314
  modelsUnavailable: "当前不可用",
315
+ claudeCacheTtl: "提示词缓存时长",
316
+ claudeCacheTtlFollowConfig: "跟随配置",
317
+ claudeCacheTtl5m: "5 分钟",
318
+ claudeCacheTtl1h: "1 小时",
319
+ claudeCacheTtlHint: "覆盖此 Claude 路由的 claudePromptCacheTtl 配置;选择「跟随配置」时使用配置值。保存后自下一次请求生效。1 小时可更久保留缓存前缀,但写入按更高的输入价格计费;压缩与会话标题请求仍为 5 分钟。",
310
320
  nav: "订阅",
311
321
  intro: "在此登录或退出订阅服务商。点击登录会在新标签页打开服务商的授权页面;无浏览器环境可改为粘贴回调 URL 或授权码。",
312
322
  unavailable: "连接不可用,无法加载订阅状态。",
@@ -576,6 +586,19 @@ function mergeLatestAccounts(draft, latest) {
576
586
  ...latest.accounts === void 0 ? {} : { accounts: latest.accounts }
577
587
  };
578
588
  }
589
+ /**
590
+ * The account manager owns the Claude prompt-cache TTL picker. The model
591
+ * editor's draft carries the TTL it loaded, so the dialog always replaces it
592
+ * with the picker's choice when touched, or the freshly re-read value
593
+ * otherwise.
594
+ */
595
+ function mergePromptCacheTtl(settings, promptCacheTtl) {
596
+ const { promptCacheTtl: _stale,...rest } = settings;
597
+ return promptCacheTtl === void 0 ? rest : {
598
+ ...rest,
599
+ promptCacheTtl
600
+ };
601
+ }
579
602
  /** Only touched accounts replace their latest preferences; other settings survive. */
580
603
  function mergeAccountChanges(latest, changes) {
581
604
  return {
@@ -683,7 +706,8 @@ const ProviderModelEditor = (0, react.forwardRef)(function ProviderModelEditor$1
683
706
  draft,
684
707
  contexts,
685
708
  efforts,
686
- provider
709
+ provider,
710
+ t
687
711
  ]);
688
712
  const allModels = catalog?.models ?? [];
689
713
  const known = new Set(allModels.map((model) => model.id));
@@ -1149,6 +1173,8 @@ function ProviderAccountManager({ provider, name, rpc, t, onClose }) {
1149
1173
  const [saveError, setSaveError] = (0, react.useState)("");
1150
1174
  const [attempt, setAttempt] = (0, react.useState)(0);
1151
1175
  const [modelsDirty, setModelsDirty] = (0, react.useState)(false);
1176
+ const [ttlDraft, setTtlDraft] = (0, react.useState)();
1177
+ const [ttlDirty, setTtlDirty] = (0, react.useState)(false);
1152
1178
  const alive = (0, react.useRef)(true);
1153
1179
  const saveLock = (0, react.useRef)(false);
1154
1180
  const editor = (0, react.useRef)(null);
@@ -1168,7 +1194,11 @@ function ProviderAccountManager({ provider, name, rpc, t, onClose }) {
1168
1194
  setLoading(true);
1169
1195
  setError("");
1170
1196
  callSubscriptionsAuth(rpc, provider === "cursor-subscription" ? "cursorSettings" : "providerSettings", { provider }).then((data) => {
1171
- if (current) setCatalog(data);
1197
+ if (current) {
1198
+ setCatalog(data);
1199
+ setTtlDraft(data.settings.promptCacheTtl);
1200
+ setTtlDirty(false);
1201
+ }
1172
1202
  }).catch((error$1) => {
1173
1203
  if (current) setError(t("accountsLoadFailed", { message: error$1 instanceof Error ? error$1.message : String(error$1) }));
1174
1204
  }).finally(() => {
@@ -1216,7 +1246,7 @@ function ProviderAccountManager({ provider, name, rpc, t, onClose }) {
1216
1246
  if (provider === "cursor-subscription") await callSubscriptionsAuth(rpc, "cursorSetSettings", { settings: models.settings ?? latest.settings });
1217
1247
  else await callSubscriptionsAuth(rpc, "setProviderSettings", {
1218
1248
  provider,
1219
- settings: mergeAccountChanges(models.settings === void 0 ? latest.settings : mergeLatestAccounts(models.settings, latest.settings), changes)
1249
+ settings: mergeAccountChanges(mergePromptCacheTtl(models.settings === void 0 ? latest.settings : mergeLatestAccounts(models.settings, latest.settings), ttlDirty ? ttlDraft : latest.settings.promptCacheTtl), changes)
1220
1250
  });
1221
1251
  if (alive.current) onClose();
1222
1252
  } catch (error$1) {
@@ -1392,7 +1422,8 @@ function ProviderAccountManager({ provider, name, rpc, t, onClose }) {
1392
1422
  },
1393
1423
  children: [
1394
1424
  t("accountsModels"),
1395
- " · ",
1425
+ " ·",
1426
+ " ",
1396
1427
  preferences.poolModels === void 0 ? t("accountsModelsAll") : t("accountsModelsCount", { count: preferences.poolModels.length })
1397
1428
  ]
1398
1429
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -1484,6 +1515,52 @@ function ProviderAccountManager({ provider, name, rpc, t, onClose }) {
1484
1515
  disabled: saving,
1485
1516
  onDirtyChange: setModelsDirty
1486
1517
  }),
1518
+ provider === "claude" && catalog && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1519
+ style: {
1520
+ ...stack$1,
1521
+ gap: 6,
1522
+ borderTop: border$1,
1523
+ marginTop: 12,
1524
+ paddingTop: 12
1525
+ },
1526
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1527
+ style: {
1528
+ ...stack$1,
1529
+ gap: 6
1530
+ },
1531
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1532
+ className: "dsh-subscription-label",
1533
+ children: t("claudeCacheTtl")
1534
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1535
+ "aria-label": t("claudeCacheTtl"),
1536
+ style: { width: 220 },
1537
+ value: ttlDraft ?? "",
1538
+ disabled: saving,
1539
+ onChange: (event) => {
1540
+ const value = event.target.value;
1541
+ setTtlDraft(value === "" ? void 0 : value);
1542
+ setTtlDirty(true);
1543
+ },
1544
+ children: [
1545
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1546
+ value: "",
1547
+ children: t("claudeCacheTtlFollowConfig")
1548
+ }),
1549
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1550
+ value: "5m",
1551
+ children: t("claudeCacheTtl5m")
1552
+ }),
1553
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1554
+ value: "1h",
1555
+ children: t("claudeCacheTtl1h")
1556
+ })
1557
+ ]
1558
+ })]
1559
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1560
+ className: "dsh-subscription-hint",
1561
+ children: t("claudeCacheTtlHint")
1562
+ })]
1563
+ }),
1487
1564
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("footer", {
1488
1565
  style: {
1489
1566
  ...stack$1,
@@ -1510,7 +1587,7 @@ function ProviderAccountManager({ provider, name, rpc, t, onClose }) {
1510
1587
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1511
1588
  type: "button",
1512
1589
  className: "dsh-subscription-primary",
1513
- disabled: loading || saving || !Object.keys(changes).length && !modelsDirty,
1590
+ disabled: loading || saving || !Object.keys(changes).length && !modelsDirty && !ttlDirty,
1514
1591
  onClick: () => {
1515
1592
  save();
1516
1593
  },
@@ -3588,7 +3665,12 @@ function ResetCreditsDisclosure({ mode, credits, t, now = Date.now(), nextCredit
3588
3665
  }),
3589
3666
  mode === "grants" && state === "notUsable" && credit.cooldownUntil !== void 0 && credit.cooldownUntil > now && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3590
3667
  "aria-hidden": "true",
3591
- children: [t("resetCreditCooldownUntil", { date: new Date(credit.cooldownUntil).toLocaleString() }), " · "]
3668
+ children: [
3669
+ t("resetCreditCooldownUntil", { date: new Date(credit.cooldownUntil).toLocaleString() }),
3670
+ " ",
3671
+ "·",
3672
+ " "
3673
+ ]
3592
3674
  }),
3593
3675
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3594
3676
  "aria-hidden": mode === "grants" ? true : void 0,
@@ -3734,7 +3816,7 @@ const styles$6 = {
3734
3816
  //#endregion
3735
3817
  //#region src/client/SubscriptionsSection.tsx
3736
3818
  /** Poll cadence while a provider login attempt is busy. */
3737
- const POLL_INTERVAL_MS$1 = 2e3;
3819
+ const POLL_INTERVAL_MS = 2e3;
3738
3820
  const CLIENT_VERSION_SOURCES = {
3739
3821
  npm: "clientVersionNpm",
3740
3822
  local: "clientVersionLocal",
@@ -4063,7 +4145,7 @@ function SubscriptionsSection(props) {
4063
4145
  if (pollersRef.current.has(provider)) return;
4064
4146
  pollersRef.current.set(provider, setInterval(() => {
4065
4147
  refresh();
4066
- }, POLL_INTERVAL_MS$1));
4148
+ }, POLL_INTERVAL_MS));
4067
4149
  }, [refresh]);
4068
4150
  (0, react.useEffect)(() => {
4069
4151
  mountedRef.current = true;
@@ -4225,7 +4307,8 @@ function SubscriptionsSection(props) {
4225
4307
  rpc,
4226
4308
  t,
4227
4309
  setProviderError,
4228
- startPolling
4310
+ startPolling,
4311
+ refresh
4229
4312
  ]);
4230
4313
  const cancel = (0, react.useCallback)(async (provider) => {
4231
4314
  if (rpc === void 0) return;
@@ -4972,7 +5055,8 @@ const HOST_CONTRACT = {
4972
5055
  slots: "@deepseek-ai/dsh-client-ui-renderer"
4973
5056
  },
4974
5057
  markers: { composerStats: {
4975
- attribute: "data-composer-stats",
5058
+ row: "data-composer-stats",
5059
+ dock: "data-composer-stat",
4976
5060
  package: "@deepseek-ai/dsh-client-ui-chat"
4977
5061
  } },
4978
5062
  diagnostics: {
@@ -5025,6 +5109,10 @@ const HOST_CONTRACT = {
5025
5109
  ]
5026
5110
  }
5027
5111
  };
5112
+ /** Attribute selector for a host DOM marker (`data-composer-stats` → `[data-composer-stats]`). */
5113
+ function markerSelector(attribute) {
5114
+ return `[${attribute}]`;
5115
+ }
5028
5116
 
5029
5117
  //#endregion
5030
5118
  //#region src/client/host-icons.ts
@@ -5435,9 +5523,10 @@ function VideoGenerateToolview(props) {
5435
5523
  //#region src/client/SpeedSelect.tsx
5436
5524
  /**
5437
5525
  * The `loadSpeed` half of the inject face: the plugin's own speed state plus
5438
- * the host's current model selection (the visibility gate). A model-RPC
5439
- * failure throws rather than answering "hidden" — the caller keeps its last
5440
- * known state, so a transient failure never locks the toggle away.
5526
+ * the host's current model selection (the visibility gate). A session whose
5527
+ * model selection is unavailable (an addressed subagent) answers hidden.
5528
+ * Any other model-directory failure throws, so the caller keeps its last
5529
+ * known state instead of locking the toggle away.
5441
5530
  *
5442
5531
  * `sessionId` is a plain string: slot and command contexts brand it through
5443
5532
  * different dsh-session copies, and only the service boundary needs one.
@@ -5454,13 +5543,126 @@ function createSpeedLoader(connection, models, sessionId) {
5454
5543
  visible: false,
5455
5544
  tier: state.tier
5456
5545
  };
5457
- const { current } = await directories.directoryFor(sessionId).load();
5546
+ let current;
5547
+ try {
5548
+ current = (await directories.directoryFor(sessionId).load()).current;
5549
+ } catch (error) {
5550
+ if (!modelSelectionUnavailable(error)) throw error;
5551
+ return {
5552
+ visible: false,
5553
+ tier: state.tier
5554
+ };
5555
+ }
5458
5556
  return {
5459
5557
  visible: current !== null && current.provider === "codex" && state.fastModels.includes(current.model),
5460
5558
  tier: state.tier
5461
5559
  };
5462
5560
  };
5463
5561
  }
5562
+ /** Host copy for a session that has no model seat (an addressed subagent). */
5563
+ const MODEL_SELECTION_UNAVAILABLE = "model selection is unavailable";
5564
+ function modelSelectionUnavailable(error) {
5565
+ return (error instanceof Error ? error.message : String(error)).includes(MODEL_SELECTION_UNAVAILABLE);
5566
+ }
5567
+ /** Stable identity of a model selection. Empty when nothing is selected. */
5568
+ function modelSelectionKey(current) {
5569
+ if (current == null) return "";
5570
+ return JSON.stringify([current.provider, current.model]);
5571
+ }
5572
+ /**
5573
+ * How often a host without a directory store is re-read, and how often a
5574
+ * missing directory service is retried. Neither interval calls
5575
+ * `subscriptions-auth.speed`.
5576
+ */
5577
+ const MODEL_DIRECTORY_POLL_INTERVAL_MS = 3e3;
5578
+ /**
5579
+ * Mount reads of `speed`, including the first attempt. Further failures stop
5580
+ * instead of polling for the life of the composer.
5581
+ */
5582
+ const SPEED_MOUNT_READ_ATTEMPTS = 4;
5583
+ /** Fired after `/fast` writes a tier, so an open composer re-reads `speed`. */
5584
+ const SPEED_CHANGED_EVENT = "dsh:subscriptions:speed-changed";
5585
+ /** Tell mounted Speed controls that this page changed a session tier. */
5586
+ function notifySpeedChanged() {
5587
+ if (typeof window === "undefined") return;
5588
+ window.dispatchEvent(new Event(SPEED_CHANGED_EVENT));
5589
+ }
5590
+ /**
5591
+ * Subscribe to the session's selected model. A directory store notifies the
5592
+ * listener only when `provider` or `model` changes. A directory with no store
5593
+ * is re-read on {@link MODEL_DIRECTORY_POLL_INTERVAL_MS}; a service that is
5594
+ * not registered yet, or whose `directoryFor` throws, is retried on that same
5595
+ * interval. The first successful attach after a miss notifies once, so a
5596
+ * mount that ran before the service existed reads `speed` again.
5597
+ */
5598
+ function createModelSelectionWatch(models, sessionId) {
5599
+ return (listener) => {
5600
+ let stopped = false;
5601
+ let unsubscribeStore;
5602
+ let timer;
5603
+ let mode = "seek";
5604
+ let lastKey;
5605
+ const stopTimer = () => {
5606
+ if (timer === void 0) return;
5607
+ clearInterval(timer);
5608
+ timer = void 0;
5609
+ };
5610
+ const changed = (current) => {
5611
+ const next = modelSelectionKey(current);
5612
+ if (lastKey === void 0) {
5613
+ lastKey = next;
5614
+ return false;
5615
+ }
5616
+ if (next === lastKey) return false;
5617
+ lastKey = next;
5618
+ return true;
5619
+ };
5620
+ const attach = (late) => {
5621
+ if (stopped || mode !== "seek") return;
5622
+ let directory;
5623
+ try {
5624
+ directory = models()?.directoryFor(sessionId);
5625
+ } catch {
5626
+ return;
5627
+ }
5628
+ if (directory === void 0) return;
5629
+ const store = directory.store;
5630
+ if (store !== void 0) {
5631
+ mode = "store";
5632
+ stopTimer();
5633
+ lastKey = modelSelectionKey(store.getSnapshot().current);
5634
+ unsubscribeStore = store.subscribe(() => {
5635
+ if (!stopped && changed(store.getSnapshot().current)) listener();
5636
+ });
5637
+ if (late) listener();
5638
+ return;
5639
+ }
5640
+ mode = "poll";
5641
+ stopTimer();
5642
+ let force = late;
5643
+ const seed = () => {
5644
+ directory.load().then((state) => {
5645
+ if (stopped || mode !== "poll") return;
5646
+ const identityChanged = changed(state.current);
5647
+ const forced = force;
5648
+ force = false;
5649
+ if (identityChanged || forced) listener();
5650
+ }, () => {});
5651
+ };
5652
+ seed();
5653
+ timer = setInterval(seed, MODEL_DIRECTORY_POLL_INTERVAL_MS);
5654
+ };
5655
+ attach(false);
5656
+ if (mode === "seek") timer = setInterval(() => {
5657
+ attach(true);
5658
+ }, MODEL_DIRECTORY_POLL_INTERVAL_MS);
5659
+ return () => {
5660
+ stopped = true;
5661
+ unsubscribeStore?.();
5662
+ stopTimer();
5663
+ };
5664
+ };
5665
+ }
5464
5666
  /** The `setSpeed` half of the inject face: boolean outcome for the component's busy state. */
5465
5667
  function createSpeedSetter(connection, sessionId) {
5466
5668
  return (tier) => callSubscriptionsAuth(connection.rpc, "setSpeed", {
@@ -5476,20 +5678,15 @@ const TIERS = ["standard", "fast"];
5476
5678
  /**
5477
5679
  * The composer Speed control: a trigger reading `速度 · 快速`/`速度 · 标准`
5478
5680
  * that opens a two-row menu (standard/fast with descriptions, check mark on
5479
- * the current tier). Mount and every open reload the host state so a model
5480
- * switch made since the last open self-corrects.
5681
+ * the current tier). It reads `subscriptions-auth.speed` on mount, when the
5682
+ * menu opens, when {@link SpeedSelectInjected.watchModel} reports a new
5683
+ * selected model, and when {@link notifySpeedChanged} fires. Until the first
5684
+ * read succeeds, a failure is retried on
5685
+ * {@link MODEL_DIRECTORY_POLL_INTERVAL_MS}, at most
5686
+ * {@link SPEED_MOUNT_READ_ATTEMPTS} times; after that, a failed read keeps
5687
+ * the last known state.
5481
5688
  */
5482
- /** How often the control re-reads the host state (model switches arrive only by asking). */
5483
- const POLL_INTERVAL_MS = 3e3;
5484
- /**
5485
- * The composer Speed control: a trigger reading `速度 · 快速`/`速度 · 标准`
5486
- * that opens a two-row menu (standard/fast with descriptions, check mark on
5487
- * the current tier). The host pushes nothing on a model switch, so the
5488
- * control re-reads on a slow poll with a single-flight guard; a failed read
5489
- * keeps the last known state, so a transient RPC failure can never lock the
5490
- * toggle away (the earlier mount-only load had no recovery path).
5491
- */
5492
- function SpeedSelect({ loadSpeed, setSpeed, t }) {
5689
+ function SpeedSelect({ loadSpeed, setSpeed, watchModel, t }) {
5493
5690
  const translate = t ?? fallbackTranslate$1;
5494
5691
  const [state, setState] = (0, react.useState)(null);
5495
5692
  const [open, setOpen] = (0, react.useState)(false);
@@ -5497,25 +5694,58 @@ function SpeedSelect({ loadSpeed, setSpeed, t }) {
5497
5694
  const rootRef = (0, react.useRef)(null);
5498
5695
  const loadRef = (0, react.useRef)(loadSpeed);
5499
5696
  loadRef.current = loadSpeed;
5697
+ const watchRef = (0, react.useRef)(watchModel);
5698
+ watchRef.current = watchModel;
5500
5699
  (0, react.useEffect)(() => {
5501
5700
  if (loadRef.current === void 0) return;
5502
5701
  let cancelled = false;
5503
5702
  let inflight = false;
5703
+ let again = false;
5704
+ let loadedOnce = false;
5705
+ let failures = 0;
5706
+ let retry;
5707
+ const stopRetry = () => {
5708
+ if (retry === void 0) return;
5709
+ clearInterval(retry);
5710
+ retry = void 0;
5711
+ };
5504
5712
  const reload = () => {
5505
5713
  const load = loadRef.current;
5506
- if (load === void 0 || inflight) return;
5714
+ if (load === void 0) return;
5715
+ if (inflight) {
5716
+ again = true;
5717
+ return;
5718
+ }
5507
5719
  inflight = true;
5508
5720
  load().then((loaded) => {
5509
- if (!cancelled) setState(loaded);
5510
- }, () => {}).finally(() => {
5721
+ if (cancelled) return;
5722
+ loadedOnce = true;
5723
+ stopRetry();
5724
+ setState(loaded);
5725
+ }, () => {
5726
+ failures += 1;
5727
+ if (failures >= SPEED_MOUNT_READ_ATTEMPTS) stopRetry();
5728
+ }).finally(() => {
5511
5729
  inflight = false;
5730
+ if (cancelled || !again) return;
5731
+ again = false;
5732
+ reload();
5512
5733
  });
5513
5734
  };
5514
5735
  reload();
5515
- const timer = setInterval(reload, POLL_INTERVAL_MS);
5736
+ retry = setInterval(() => {
5737
+ if (!loadedOnce) reload();
5738
+ }, MODEL_DIRECTORY_POLL_INTERVAL_MS);
5739
+ const unsubscribe = watchRef.current?.(reload);
5740
+ const onSpeedChanged = () => {
5741
+ reload();
5742
+ };
5743
+ window.addEventListener(SPEED_CHANGED_EVENT, onSpeedChanged);
5516
5744
  return () => {
5517
5745
  cancelled = true;
5518
- clearInterval(timer);
5746
+ unsubscribe?.();
5747
+ stopRetry();
5748
+ window.removeEventListener(SPEED_CHANGED_EVENT, onSpeedChanged);
5519
5749
  };
5520
5750
  }, []);
5521
5751
  (0, react.useEffect)(() => {
@@ -5711,7 +5941,7 @@ const PROVIDER_NAMES = {
5711
5941
  "cursor-subscription": "Cursor",
5712
5942
  "opencode-go": "OpenCode Go",
5713
5943
  "kimi-coding": "Kimi Code",
5714
- "minimax": "MiniMax",
5944
+ minimax: "MiniMax",
5715
5945
  "minimax-cn": "MiniMax CN",
5716
5946
  "ollama-cloud": "Ollama Cloud"
5717
5947
  };
@@ -5946,6 +6176,8 @@ function usageWindowLabel(t, window$1) {
5946
6176
  * every provider's accounts and their windows. Returns null when no data is
5947
6177
  * available.
5948
6178
  */
6179
+ const STATS_ROW_SELECTOR = markerSelector(HOST_CONTRACT.markers.composerStats.row);
6180
+ const STATS_DOCK_SELECTOR = markerSelector(HOST_CONTRACT.markers.composerStats.dock);
5949
6181
  function SubscriptionUsageBadge({ rpc, currentModel, t, rotationMs }) {
5950
6182
  const translate = t ?? fallbackTranslate;
5951
6183
  const [displays, setDisplays] = (0, react.useState)([]);
@@ -6105,15 +6337,18 @@ function SubscriptionUsageBadge({ rpc, currentModel, t, rotationMs }) {
6105
6337
  if (seat$1 === null) return;
6106
6338
  const scope = statsScopeOf(seat$1);
6107
6339
  if (scope === null) return;
6108
- const find = () => scope.querySelector("[data-composer-stats]");
6109
- setStatsRow(find());
6110
- const observer = new MutationObserver(() => {
6111
- setStatsRow(find());
6112
- });
6340
+ const find = () => scope.querySelector(STATS_ROW_SELECTOR);
6341
+ const settle = () => {
6342
+ const row = find();
6343
+ setStatsRow(row);
6344
+ if (row === null && scope.querySelector(STATS_DOCK_SELECTOR) !== null) observer.disconnect();
6345
+ };
6346
+ const observer = new MutationObserver(settle);
6113
6347
  observer.observe(scope, {
6114
6348
  childList: true,
6115
6349
  subtree: true
6116
6350
  });
6351
+ settle();
6117
6352
  return () => {
6118
6353
  observer.disconnect();
6119
6354
  };
@@ -6237,15 +6472,19 @@ function SubscriptionUsageBadge({ rpc, currentModel, t, rotationMs }) {
6237
6472
  });
6238
6473
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [seat, statsRow !== null && statsRow.isConnected ? (0, react_dom.createPortal)(pill, statsRow) : pill] });
6239
6474
  }
6475
+ /** How far statsScopeOf walks looking for `composerStats.row`. */
6476
+ const LEGACY_STATS_SCOPE_DEPTH = 4;
6240
6477
  /**
6241
- * Nearest ancestor of the dock seat that can contain the host's stats row:
6242
- * the composer bar. Bounded so a badge in an unfamiliar layout never adopts
6243
- * some other composer's pills.
6478
+ * Scope for the legacy stats row. The nearest ancestor within
6479
+ * LEGACY_STATS_SCOPE_DEPTH that contains `composerStats.row`, so a badge
6480
+ * does not adopt another composer's pills. When none does, the seat's
6481
+ * parent: the slot outlet, which holds sibling `composerStats.dock` anchors
6482
+ * on hosts whose pills are dock entries.
6244
6483
  */
6245
6484
  function statsScopeOf(seat) {
6246
6485
  let node = seat.parentElement;
6247
- for (let depth = 0; node !== null && depth < 4; depth++) {
6248
- if (node.querySelector("[data-composer-stats]") !== null) return node;
6486
+ for (let depth = 0; node !== null && depth < LEGACY_STATS_SCOPE_DEPTH; depth++) {
6487
+ if (node.querySelector(STATS_ROW_SELECTOR) !== null) return node;
6249
6488
  node = node.parentElement;
6250
6489
  }
6251
6490
  return seat.parentElement;
@@ -6260,10 +6499,10 @@ function AccountMeta({ account, translate }) {
6260
6499
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
6261
6500
  style: styles.providerMeta,
6262
6501
  title: account.account,
6263
- children: [account.isDefault && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6502
+ children: [account.isDefault && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
6264
6503
  style: styles.defaultStar,
6265
6504
  "aria-label": "default",
6266
- children: "★ "
6505
+ children: ["★", " "]
6267
6506
  }), parts.join(" · ")]
6268
6507
  });
6269
6508
  }
@@ -6545,7 +6784,8 @@ function apply(ctx) {
6545
6784
  locale: NS,
6546
6785
  inject: (sessionId) => ({
6547
6786
  loadSpeed: createSpeedLoader(connection, models, sessionId),
6548
- setSpeed: createSpeedSetter(connection, sessionId)
6787
+ setSpeed: createSpeedSetter(connection, sessionId),
6788
+ watchModel: createModelSelectionWatch(models, sessionId)
6549
6789
  })
6550
6790
  }, SpeedSelect));
6551
6791
  ctx.slots.inject("conversation.composer.dock", () => ctx.slots.register({
@@ -6583,7 +6823,7 @@ function apply(ctx) {
6583
6823
  }));
6584
6824
  },
6585
6825
  onSelect: async (option, session) => {
6586
- await createSpeedSetter(connection, session.sessionId)(option.id);
6826
+ if (await createSpeedSetter(connection, session.sessionId)(option.id)) notifySpeedChanged();
6587
6827
  }
6588
6828
  }
6589
6829
  }), "dsh-subscriptions: /fast contribution");