dsh-subscriptions 0.2.0 → 0.2.3

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 (105) hide show
  1. package/README.md +220 -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.d.ts +8 -4
  6. package/lib/auth/rpc.js +89 -25
  7. package/lib/auth/store.js +80 -31
  8. package/lib/client/CursorCard.d.ts +1 -1
  9. package/lib/client/CursorCard.js +50 -16
  10. package/lib/client/ExternalUsageCards.d.ts +1 -1
  11. package/lib/client/ExternalUsageCards.js +37 -16
  12. package/lib/client/ImageGallery.d.ts +2 -2
  13. package/lib/client/ImageGallery.js +92 -33
  14. package/lib/client/ImageGenerateToolview.js +50 -14
  15. package/lib/client/OllamaCard.d.ts +1 -1
  16. package/lib/client/OllamaCard.js +26 -8
  17. package/lib/client/OllamaSettingsManager.js +113 -30
  18. package/lib/client/ProviderAccountManager.d.ts +1 -1
  19. package/lib/client/ProviderAccountManager.js +164 -49
  20. package/lib/client/ProviderModelEditor.js +137 -34
  21. package/lib/client/SpeedSelect.d.ts +62 -14
  22. package/lib/client/SpeedSelect.js +261 -49
  23. package/lib/client/SubscriptionUsageBadge.d.ts +4 -10
  24. package/lib/client/SubscriptionUsageBadge.js +300 -112
  25. package/lib/client/SubscriptionsSection.d.ts +2 -0
  26. package/lib/client/SubscriptionsSection.js +265 -85
  27. package/lib/client/UsageBadgeDisplaySetting.js +35 -12
  28. package/lib/client/UsageMeter.d.ts +1 -1
  29. package/lib/client/UsageMeter.js +62 -24
  30. package/lib/client/VideoGenerateToolview.js +69 -17
  31. package/lib/client/account-preferences.d.ts +8 -0
  32. package/lib/client/account-preferences.js +20 -5
  33. package/lib/client/host-contract.d.ts +11 -2
  34. package/lib/client/host-contract.js +20 -3
  35. package/lib/client/index.d.ts +5 -5
  36. package/lib/client/index.js +34 -12
  37. package/lib/client/locales.d.ts +22 -0
  38. package/lib/client/locales.js +73 -51
  39. package/lib/client/model-filter.d.ts +37 -0
  40. package/lib/client/model-filter.js +45 -0
  41. package/lib/client/ollama-draft.js +21 -12
  42. package/lib/client/reset-action.js +6 -4
  43. package/lib/client/reset-credits-view.d.ts +1 -1
  44. package/lib/client/reset-credits-view.js +66 -17
  45. package/lib/client/subscription-card-styles.js +94 -24
  46. package/lib/client/usage-color-preferences.js +4 -2
  47. package/lib/client/usage-pace.js +30 -12
  48. package/lib/client/usage-window-filter.d.ts +7 -0
  49. package/lib/client/usage-window-filter.js +12 -0
  50. package/lib/client.js +480 -136
  51. package/lib/client.js.map +1 -1
  52. package/lib/http.js +1 -1
  53. package/lib/index.d.ts +3 -3
  54. package/lib/index.js +167 -76
  55. package/lib/model-defaults.js +4 -2
  56. package/lib/provider-settings.d.ts +17 -0
  57. package/lib/provider-settings.js +79 -16
  58. package/lib/providers/account-preferences.js +51 -22
  59. package/lib/providers/accounts.js +15 -9
  60. package/lib/providers/antigravity-oauth-client.js +2 -2
  61. package/lib/providers/antigravity.js +141 -68
  62. package/lib/providers/catalog-store.js +120 -41
  63. package/lib/providers/claude-cli-version.js +3 -3
  64. package/lib/providers/claude.d.ts +9 -2
  65. package/lib/providers/claude.js +153 -90
  66. package/lib/providers/codex-client-version.js +3 -3
  67. package/lib/providers/codex-search.js +40 -13
  68. package/lib/providers/codex.js +196 -112
  69. package/lib/providers/common.js +71 -39
  70. package/lib/providers/copilot.js +143 -82
  71. package/lib/providers/cursor-adapter.js +42 -18
  72. package/lib/providers/cursor-auth.d.ts +1 -1
  73. package/lib/providers/cursor-auth.js +69 -24
  74. package/lib/providers/cursor-model-settings.js +14 -5
  75. package/lib/providers/cursor-usage.js +27 -13
  76. package/lib/providers/external-usage-controller.js +14 -5
  77. package/lib/providers/external-usage.js +59 -23
  78. package/lib/providers/grok.js +152 -89
  79. package/lib/providers/image-pool.js +29 -11
  80. package/lib/providers/minimax-usage.js +31 -11
  81. package/lib/providers/npm-cli-version.js +10 -3
  82. package/lib/providers/ollama-settings.js +24 -8
  83. package/lib/providers/ollama-web.js +29 -11
  84. package/lib/providers/ollama.js +129 -66
  85. package/lib/providers/pool-family.js +11 -3
  86. package/lib/providers/pool-health.js +28 -8
  87. package/lib/providers/pool-usage.js +16 -9
  88. package/lib/providers/pool.d.ts +8 -0
  89. package/lib/providers/pool.js +81 -42
  90. package/lib/providers/rate-limit.js +8 -2
  91. package/lib/providers/replay.js +40 -17
  92. package/lib/providers/reset-redemption.js +18 -6
  93. package/lib/tools/image-generate.js +123 -65
  94. package/lib/tools/registration.js +5 -2
  95. package/lib/tools/video-generate.js +51 -31
  96. package/lib/tools/x-search.js +50 -22
  97. package/lib/translate/anthropic.js +85 -37
  98. package/lib/translate/antigravity-schema.js +71 -28
  99. package/lib/translate/antigravity-thinking.js +22 -9
  100. package/lib/translate/antigravity.js +151 -52
  101. package/lib/translate/chat-completions.js +67 -23
  102. package/lib/translate/resolved.js +55 -21
  103. package/lib/translate/responses.js +69 -29
  104. package/lib/translate/sse.js +4 -1
  105. package/package.json +27 -4
package/lib/client.js CHANGED
@@ -56,7 +56,7 @@ const en = {
56
56
  accountsCatalogUnavailable: "Model discovery is currently unavailable. Saved model selections are preserved.",
57
57
  modelsPartialSave: "Some reasoning defaults were saved. Remaining changes are still in the draft; retry to finish.",
58
58
  modelsEdit: "Edit model list",
59
- modelsHint: "Configure model visibility, default reasoning effort, context and tools here; Save changes below submits them together with the account settings. Hidden models remain usable by existing sessions; tool changes apply to new sessions.",
59
+ modelsHint: "Configure model visibility, default reasoning effort, subagent speed, context and tools here; Save changes below submits them together with the account settings. Hidden models remain usable by existing sessions; tool changes apply to new sessions.",
60
60
  modelsAutomatic: "Automatically show all models, including new models",
61
61
  modelsSelectAll: "Select all",
62
62
  modelsSelectNone: "Clear selection",
@@ -64,6 +64,8 @@ const en = {
64
64
  modelsContextBounds: "Default {default} · Maximum {max}",
65
65
  modelsContextHint: "Leave blank to follow the provider. The requested window is capped by the account’s advertised maximum; account pools use the smallest member window. This controls local history and compaction, not the API’s capacity.",
66
66
  modelsContextInvalid: "Enter a positive whole number of tokens for {model}.",
67
+ modelsSubagentSpeed: "Subagent default speed",
68
+ modelsSubagentSpeedHint: "Applies to subsequent subagent requests using this model, regardless of the parent model. Main conversations keep their own Speed setting. Fast requires model support.",
67
69
  modelsTools: "Provider tools",
68
70
  modelsToolsHint: "Changes apply only to sessions created after saving. Existing sessions keep their tools. Shared tools remain available if any supporting provider is enabled.",
69
71
  modelsImage: "Image generation",
@@ -72,6 +74,11 @@ const en = {
72
74
  modelsWebSearch: "Web search",
73
75
  modelsSave: "Save changes",
74
76
  modelsUnavailable: "Currently unavailable",
77
+ claudeCacheTtl: "Prompt cache lifetime",
78
+ claudeCacheTtlFollowConfig: "Follow config",
79
+ claudeCacheTtl5m: "5 minutes",
80
+ claudeCacheTtl1h: "1 hour",
81
+ 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
82
  nav: "Subscriptions",
76
83
  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
84
  unavailable: "Connection unavailable; subscription status cannot be loaded.",
@@ -108,6 +115,7 @@ const en = {
108
115
  usageRefresh: "Refresh",
109
116
  usageLoading: "Loading usage…",
110
117
  usageEmpty: "No usage windows reported.",
118
+ usageNoVisibleModels: "No usage windows for the models visible in the picker.",
111
119
  usageUnsupported: "This endpoint has no usage surface.",
112
120
  usageError: "Usage lookup failed: {message}",
113
121
  usageSession: "5-hour window",
@@ -206,6 +214,9 @@ const en = {
206
214
  modelDefaultsSummaryEmpty: "No model advertises reasoning levels",
207
215
  modelDefaultsFilterPlaceholder: "Filter models",
208
216
  modelDefaultsFilterEmpty: "No model matches “{query}”.",
217
+ modelDefaultsNoModels: "This provider reports no models.",
218
+ modelDefaultsNoAccount: "No connected accounts. Connect one on the provider card first.",
219
+ modelDefaultsDiscoveryFailed: "Model discovery failed or timed out. Use Refresh to try again.",
209
220
  generating: "Generating image…",
210
221
  image: "image",
211
222
  viewImage: "View image",
@@ -291,7 +302,7 @@ const zh = {
291
302
  accountsCatalogUnavailable: "暂时无法获取模型目录,已保存的模型选择会被保留。",
292
303
  modelsPartialSave: "部分推理档已保存,其余更改仍保留在草稿中,请重试完成保存。",
293
304
  modelsEdit: "编辑模型列表",
294
- modelsHint: "在此配置模型显示、默认推理档、上下文与工具,底部的「保存更改」会连同账号设置一起提交。隐藏模型不影响已有会话;工具开关对新建会话生效。",
305
+ modelsHint: "在此配置模型显示、默认推理档、子代理速度、上下文与工具,底部的「保存更改」会连同账号设置一起提交。隐藏模型不影响已有会话;工具开关对新建会话生效。",
295
306
  modelsAutomatic: "自动显示全部模型(包含以后新增的模型)",
296
307
  modelsSelectAll: "全选",
297
308
  modelsSelectNone: "清空选择",
@@ -299,6 +310,8 @@ const zh = {
299
310
  modelsContextBounds: "默认 {default} · 最大 {max}",
300
311
  modelsContextHint: "留空跟随服务商。配置值按账号返回的最大窗口限制;账号池取各成员的最小窗口。此设置控制本地历史保留和压缩时机,不会扩大 API 本身的容量。",
301
312
  modelsContextInvalid: "请为 {model} 输入正整数 token 数。",
313
+ modelsSubagentSpeed: "子代理默认速度",
314
+ modelsSubagentSpeedHint: "对使用此模型的子代理后续请求生效,与父会话模型无关。主会话仍使用自己的速度设置;快速档仅在模型支持时生效。",
302
315
  modelsTools: "Provider 工具",
303
316
  modelsToolsHint: "仅对保存后新建的会话生效,已有会话保留原工具。任一支持的服务商启用共享工具时,该工具仍会显示。",
304
317
  modelsImage: "图片生成",
@@ -307,6 +320,11 @@ const zh = {
307
320
  modelsWebSearch: "网页搜索",
308
321
  modelsSave: "保存更改",
309
322
  modelsUnavailable: "当前不可用",
323
+ claudeCacheTtl: "提示词缓存时长",
324
+ claudeCacheTtlFollowConfig: "跟随配置",
325
+ claudeCacheTtl5m: "5 分钟",
326
+ claudeCacheTtl1h: "1 小时",
327
+ claudeCacheTtlHint: "覆盖此 Claude 路由的 claudePromptCacheTtl 配置;选择「跟随配置」时使用配置值。保存后自下一次请求生效。1 小时可更久保留缓存前缀,但写入按更高的输入价格计费;压缩与会话标题请求仍为 5 分钟。",
310
328
  nav: "订阅",
311
329
  intro: "在此登录或退出订阅服务商。点击登录会在新标签页打开服务商的授权页面;无浏览器环境可改为粘贴回调 URL 或授权码。",
312
330
  unavailable: "连接不可用,无法加载订阅状态。",
@@ -343,6 +361,7 @@ const zh = {
343
361
  usageRefresh: "刷新",
344
362
  usageLoading: "用量加载中…",
345
363
  usageEmpty: "服务商未返回任何用量窗口。",
364
+ usageNoVisibleModels: "选择器中的可见模型没有任何用量窗口。",
346
365
  usageUnsupported: "该端点不提供用量查询。",
347
366
  usageError: "用量查询失败:{message}",
348
367
  usageSession: "5 小时窗口",
@@ -441,6 +460,9 @@ const zh = {
441
460
  modelDefaultsSummaryEmpty: "没有模型声明推理档",
442
461
  modelDefaultsFilterPlaceholder: "筛选模型",
443
462
  modelDefaultsFilterEmpty: "没有匹配「{query}」的模型。",
463
+ modelDefaultsNoModels: "这个服务商没有报告任何模型。",
464
+ modelDefaultsNoAccount: "还没有连接账号。请先在卡片上连接一个账号。",
465
+ modelDefaultsDiscoveryFailed: "模型列表发现失败或超时。可以点「刷新」重试。",
444
466
  generating: "正在生成图片…",
445
467
  image: "图片",
446
468
  viewImage: "查看图片",
@@ -517,6 +539,19 @@ function resetActionBlock(usage, now = Date.now()) {
517
539
  if (!usage.resetCredits.some((c) => c.id && (c.expiresAt === void 0 || c.expiresAt > now))) return "resetUseRefresh";
518
540
  }
519
541
 
542
+ //#endregion
543
+ //#region src/client/usage-window-filter.ts
544
+ /**
545
+ * Keep model-scoped Google quotas aligned with the saved model-picker selection.
546
+ * Account-wide windows and other providers are unaffected; an absent selection
547
+ * follows discovery, while an empty selection hides every model-scoped window.
548
+ */
549
+ function visibleUsageWindows(provider, windows, visibleModels) {
550
+ if (windows === void 0) return [];
551
+ if (provider !== "antigravity" || visibleModels === void 0) return windows;
552
+ return windows.filter((window$1) => !window$1.scope || visibleModels.includes(window$1.scope));
553
+ }
554
+
520
555
  //#endregion
521
556
  //#region src/client/subscriptions-rpc.ts
522
557
  /**
@@ -576,6 +611,19 @@ function mergeLatestAccounts(draft, latest) {
576
611
  ...latest.accounts === void 0 ? {} : { accounts: latest.accounts }
577
612
  };
578
613
  }
614
+ /**
615
+ * The account manager owns the Claude prompt-cache TTL picker. The model
616
+ * editor's draft carries the TTL it loaded, so the dialog always replaces it
617
+ * with the picker's choice when touched, or the freshly re-read value
618
+ * otherwise.
619
+ */
620
+ function mergePromptCacheTtl(settings, promptCacheTtl) {
621
+ const { promptCacheTtl: _stale,...rest } = settings;
622
+ return promptCacheTtl === void 0 ? rest : {
623
+ ...rest,
624
+ promptCacheTtl
625
+ };
626
+ }
579
627
  /** Only touched accounts replace their latest preferences; other settings survive. */
580
628
  function mergeAccountChanges(latest, changes) {
581
629
  return {
@@ -587,6 +635,43 @@ function mergeAccountChanges(latest, changes) {
587
635
  };
588
636
  }
589
637
 
638
+ //#endregion
639
+ //#region src/client/model-filter.ts
640
+ /** Distinguish an empty provider from a query that filtered out every row. */
641
+ function modelListState(total, matched) {
642
+ if (total === 0) return "provider-empty";
643
+ return matched === 0 ? "filter-empty" : "rows";
644
+ }
645
+ /**
646
+ * Retain saved model ids when discovery returns no rows. A temporary catalog
647
+ * failure must not hide the visibility or subagent-speed settings they edit.
648
+ */
649
+ function savedModelIds(visibleModels, subagentSpeeds) {
650
+ return [...new Set([...visibleModels ?? [], ...Object.keys(subagentSpeeds ?? {})])];
651
+ }
652
+ /**
653
+ * Use account discovery evidence to explain an empty provider. Connected
654
+ * accounts with failed discovery need a retry, not another login prompt.
655
+ */
656
+ function modelEditorNotice(accounts) {
657
+ if (accounts === void 0) return "noModels";
658
+ if (accounts.length === 0) return "noAccount";
659
+ return accounts.every((account) => account.unavailable === true) ? "discoveryFailed" : "noModels";
660
+ }
661
+ /** Locale keys for the three empty states. */
662
+ const NOTICE_KEYS = {
663
+ noAccount: "modelDefaultsNoAccount",
664
+ discoveryFailed: "modelDefaultsDiscoveryFailed",
665
+ noModels: "modelDefaultsNoModels"
666
+ };
667
+ /** Combine discovered and saved rows, filter results, and discovery evidence. */
668
+ function modelEditorState(input) {
669
+ return {
670
+ listState: modelListState(input.discovered + input.saved, input.matched),
671
+ noticeKey: NOTICE_KEYS[modelEditorNotice(input.accounts)]
672
+ };
673
+ }
674
+
590
675
  //#endregion
591
676
  //#region src/client/ProviderModelEditor.tsx
592
677
  /** Providers whose runtime honours a local context-window override. */
@@ -683,16 +768,23 @@ const ProviderModelEditor = (0, react.forwardRef)(function ProviderModelEditor$1
683
768
  draft,
684
769
  contexts,
685
770
  efforts,
686
- provider
771
+ provider,
772
+ t
687
773
  ]);
688
774
  const allModels = catalog?.models ?? [];
689
775
  const known = new Set(allModels.map((model) => model.id));
690
- const missing = (draft.visibleModels ?? []).filter((id) => !known.has(id)).map((id) => ({
776
+ const missing = savedModelIds(draft.visibleModels, draft.subagentSpeeds).filter((id) => !known.has(id)).map((id) => ({
691
777
  id,
692
778
  name: id
693
779
  }));
694
780
  const models = [...allModels, ...missing].filter((model) => `${model.name} ${model.id}`.toLowerCase().includes(query.trim().toLowerCase()));
695
- const selected = new Set(draft.visibleModels ?? allModels.map((model) => model.id));
781
+ const editorState = modelEditorState({
782
+ discovered: allModels.length,
783
+ saved: missing.length,
784
+ matched: models.length,
785
+ accounts: catalog?.accounts
786
+ });
787
+ const selected = new Set(draft.visibleModels ?? [...allModels, ...missing].map((model) => model.id));
696
788
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
697
789
  style: {
698
790
  borderTop: border$2,
@@ -732,7 +824,7 @@ const ProviderModelEditor = (0, react.forwardRef)(function ProviderModelEditor$1
732
824
  children: t("modelDefaultsLoading")
733
825
  })]
734
826
  }),
735
- catalog && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("fieldset", {
827
+ !busy && catalog && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("fieldset", {
736
828
  disabled: busy || disabled,
737
829
  style: {
738
830
  border: 0,
@@ -757,7 +849,10 @@ const ProviderModelEditor = (0, react.forwardRef)(function ProviderModelEditor$1
757
849
  " ",
758
850
  t("modelsAutomatic")
759
851
  ] }),
760
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
852
+ editorState.listState === "provider-empty" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
853
+ className: "dsh-subscription-hint",
854
+ children: t(editorState.noticeKey)
855
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
761
856
  style: actions$2,
762
857
  children: [
763
858
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
@@ -784,8 +879,7 @@ const ProviderModelEditor = (0, react.forwardRef)(function ProviderModelEditor$1
784
879
  children: t("modelsSelectNone")
785
880
  })
786
881
  ]
787
- }),
788
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
882
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
789
883
  style: {
790
884
  maxHeight: 360,
791
885
  overflowY: "auto",
@@ -799,87 +893,118 @@ const ProviderModelEditor = (0, react.forwardRef)(function ProviderModelEditor$1
799
893
  display: "grid",
800
894
  gap: 8
801
895
  },
802
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
803
- style: { overflowWrap: "anywhere" },
804
- children: [
805
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
806
- type: "checkbox",
807
- checked: selected.has(model.id),
808
- onChange: (event) => {
809
- const next = new Set(selected);
810
- if (event.target.checked) next.add(model.id);
811
- else next.delete(model.id);
812
- edit({
813
- ...draft,
814
- visibleModels: [...next]
815
- });
816
- }
817
- }),
818
- " ",
819
- model.name,
820
- !known.has(model.id) && ` (${t("modelsUnavailable")})`
821
- ]
822
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
823
- style: actions$2,
824
- children: [(model.efforts?.length ?? 0) > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
825
- style: actions$2,
826
- children: [t("modelDefaultsTitle"), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
827
- "aria-label": `${model.name} ${t("modelDefaultsTitle")}`,
828
- value: efforts[model.id] ?? "",
829
- onChange: (event) => {
830
- setEfforts({
831
- ...efforts,
832
- [model.id]: event.target.value
833
- });
834
- setDirty(true);
835
- },
836
- children: [
837
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
838
- value: "",
839
- children: t("modelDefaultsFollowProvider")
840
- }),
841
- model.configured && !model.efforts?.some((effort) => effort.id === model.configured) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
842
- value: model.configured,
843
- disabled: true,
844
- children: [
845
- model.configured,
846
- " (",
847
- t("modelsUnavailable"),
848
- ")"
849
- ]
850
- }),
851
- model.efforts?.map((effort) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
852
- value: effort.id,
853
- children: effort.name
854
- }, effort.id))
855
- ]
856
- })]
857
- }), supportsContext(provider) && model.maxContextWindow !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
896
+ children: [
897
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
898
+ style: { overflowWrap: "anywhere" },
899
+ children: [
900
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
901
+ type: "checkbox",
902
+ checked: selected.has(model.id),
903
+ onChange: (event) => {
904
+ const next = new Set(selected);
905
+ if (event.target.checked) next.add(model.id);
906
+ else next.delete(model.id);
907
+ edit({
908
+ ...draft,
909
+ visibleModels: [...next]
910
+ });
911
+ }
912
+ }),
913
+ " ",
914
+ model.name,
915
+ !known.has(model.id) && ` (${t("modelsUnavailable")})`
916
+ ]
917
+ }),
918
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
858
919
  style: actions$2,
859
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
920
+ children: [(model.efforts?.length ?? 0) > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
860
921
  style: actions$2,
861
- children: [t("modelsContext"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
862
- style: { width: 140 },
863
- inputMode: "numeric",
864
- value: contexts[model.id] ?? "",
865
- "aria-label": `${model.name} ${t("modelsContext")}`,
866
- placeholder: String(model.defaultContextWindow),
922
+ children: [t("modelDefaultsTitle"), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
923
+ "aria-label": `${model.name} ${t("modelDefaultsTitle")}`,
924
+ value: efforts[model.id] ?? "",
867
925
  onChange: (event) => {
868
- setContexts({
869
- ...contexts,
926
+ setEfforts({
927
+ ...efforts,
870
928
  [model.id]: event.target.value
871
929
  });
872
930
  setDirty(true);
873
- }
931
+ },
932
+ children: [
933
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
934
+ value: "",
935
+ children: t("modelDefaultsFollowProvider")
936
+ }),
937
+ model.configured && !model.efforts?.some((effort) => effort.id === model.configured) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
938
+ value: model.configured,
939
+ disabled: true,
940
+ children: [
941
+ model.configured,
942
+ " (",
943
+ t("modelsUnavailable"),
944
+ ")"
945
+ ]
946
+ }),
947
+ model.efforts?.map((effort) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
948
+ value: effort.id,
949
+ children: effort.name
950
+ }, effort.id))
951
+ ]
874
952
  })]
875
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: t("modelsContextBounds", {
876
- default: model.defaultContextWindow,
877
- max: model.maxContextWindow
878
- }) })]
879
- })]
880
- })]
953
+ }), supportsContext(provider) && model.maxContextWindow !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
954
+ style: actions$2,
955
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
956
+ style: actions$2,
957
+ children: [t("modelsContext"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
958
+ style: { width: 140 },
959
+ inputMode: "numeric",
960
+ value: contexts[model.id] ?? "",
961
+ "aria-label": `${model.name} ${t("modelsContext")}`,
962
+ placeholder: String(model.defaultContextWindow),
963
+ onChange: (event) => {
964
+ setContexts({
965
+ ...contexts,
966
+ [model.id]: event.target.value
967
+ });
968
+ setDirty(true);
969
+ }
970
+ })]
971
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: t("modelsContextBounds", {
972
+ default: model.defaultContextWindow,
973
+ max: model.maxContextWindow
974
+ }) })]
975
+ })]
976
+ }),
977
+ provider === "codex" && (model.fastTier === true || Object.hasOwn(draft.subagentSpeeds ?? {}, model.id)) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
978
+ style: {
979
+ display: "grid",
980
+ gap: 6
981
+ },
982
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
983
+ style: actions$2,
984
+ children: [t("modelsSubagentSpeed"), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
985
+ "aria-label": `${model.name} ${t("modelsSubagentSpeed")}`,
986
+ value: Object.hasOwn(draft.subagentSpeeds ?? {}, model.id) ? draft.subagentSpeeds[model.id] : "standard",
987
+ onChange: (event) => edit({
988
+ ...draft,
989
+ subagentSpeeds: {
990
+ ...draft.subagentSpeeds,
991
+ [model.id]: event.target.value
992
+ }
993
+ }),
994
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
995
+ value: "standard",
996
+ children: t("speedStandard")
997
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
998
+ value: "fast",
999
+ disabled: !model.fastTier,
1000
+ children: [t("speedFast"), !model.fastTier && ` (${t("modelsUnavailable")})`]
1001
+ })]
1002
+ })]
1003
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: t("modelsSubagentSpeedHint") })]
1004
+ })
1005
+ ]
881
1006
  }, model.id)), models.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("modelDefaultsFilterEmpty", { query }) })]
882
- }),
1007
+ })] }),
883
1008
  supportsContext(provider) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: t("modelsContextHint") }),
884
1009
  catalog.tools.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
885
1010
  style: {
@@ -1149,6 +1274,8 @@ function ProviderAccountManager({ provider, name, rpc, t, onClose }) {
1149
1274
  const [saveError, setSaveError] = (0, react.useState)("");
1150
1275
  const [attempt, setAttempt] = (0, react.useState)(0);
1151
1276
  const [modelsDirty, setModelsDirty] = (0, react.useState)(false);
1277
+ const [ttlDraft, setTtlDraft] = (0, react.useState)();
1278
+ const [ttlDirty, setTtlDirty] = (0, react.useState)(false);
1152
1279
  const alive = (0, react.useRef)(true);
1153
1280
  const saveLock = (0, react.useRef)(false);
1154
1281
  const editor = (0, react.useRef)(null);
@@ -1168,7 +1295,11 @@ function ProviderAccountManager({ provider, name, rpc, t, onClose }) {
1168
1295
  setLoading(true);
1169
1296
  setError("");
1170
1297
  callSubscriptionsAuth(rpc, provider === "cursor-subscription" ? "cursorSettings" : "providerSettings", { provider }).then((data) => {
1171
- if (current) setCatalog(data);
1298
+ if (current) {
1299
+ setCatalog(data);
1300
+ setTtlDraft(data.settings.promptCacheTtl);
1301
+ setTtlDirty(false);
1302
+ }
1172
1303
  }).catch((error$1) => {
1173
1304
  if (current) setError(t("accountsLoadFailed", { message: error$1 instanceof Error ? error$1.message : String(error$1) }));
1174
1305
  }).finally(() => {
@@ -1216,7 +1347,7 @@ function ProviderAccountManager({ provider, name, rpc, t, onClose }) {
1216
1347
  if (provider === "cursor-subscription") await callSubscriptionsAuth(rpc, "cursorSetSettings", { settings: models.settings ?? latest.settings });
1217
1348
  else await callSubscriptionsAuth(rpc, "setProviderSettings", {
1218
1349
  provider,
1219
- settings: mergeAccountChanges(models.settings === void 0 ? latest.settings : mergeLatestAccounts(models.settings, latest.settings), changes)
1350
+ settings: mergeAccountChanges(mergePromptCacheTtl(models.settings === void 0 ? latest.settings : mergeLatestAccounts(models.settings, latest.settings), ttlDirty ? ttlDraft : latest.settings.promptCacheTtl), changes)
1220
1351
  });
1221
1352
  if (alive.current) onClose();
1222
1353
  } catch (error$1) {
@@ -1392,7 +1523,8 @@ function ProviderAccountManager({ provider, name, rpc, t, onClose }) {
1392
1523
  },
1393
1524
  children: [
1394
1525
  t("accountsModels"),
1395
- " · ",
1526
+ " ·",
1527
+ " ",
1396
1528
  preferences.poolModels === void 0 ? t("accountsModelsAll") : t("accountsModelsCount", { count: preferences.poolModels.length })
1397
1529
  ]
1398
1530
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -1484,6 +1616,52 @@ function ProviderAccountManager({ provider, name, rpc, t, onClose }) {
1484
1616
  disabled: saving,
1485
1617
  onDirtyChange: setModelsDirty
1486
1618
  }),
1619
+ provider === "claude" && catalog && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1620
+ style: {
1621
+ ...stack$1,
1622
+ gap: 6,
1623
+ borderTop: border$1,
1624
+ marginTop: 12,
1625
+ paddingTop: 12
1626
+ },
1627
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1628
+ style: {
1629
+ ...stack$1,
1630
+ gap: 6
1631
+ },
1632
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1633
+ className: "dsh-subscription-label",
1634
+ children: t("claudeCacheTtl")
1635
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1636
+ "aria-label": t("claudeCacheTtl"),
1637
+ style: { width: 220 },
1638
+ value: ttlDraft ?? "",
1639
+ disabled: saving,
1640
+ onChange: (event) => {
1641
+ const value = event.target.value;
1642
+ setTtlDraft(value === "" ? void 0 : value);
1643
+ setTtlDirty(true);
1644
+ },
1645
+ children: [
1646
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1647
+ value: "",
1648
+ children: t("claudeCacheTtlFollowConfig")
1649
+ }),
1650
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1651
+ value: "5m",
1652
+ children: t("claudeCacheTtl5m")
1653
+ }),
1654
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1655
+ value: "1h",
1656
+ children: t("claudeCacheTtl1h")
1657
+ })
1658
+ ]
1659
+ })]
1660
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1661
+ className: "dsh-subscription-hint",
1662
+ children: t("claudeCacheTtlHint")
1663
+ })]
1664
+ }),
1487
1665
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("footer", {
1488
1666
  style: {
1489
1667
  ...stack$1,
@@ -1510,7 +1688,7 @@ function ProviderAccountManager({ provider, name, rpc, t, onClose }) {
1510
1688
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1511
1689
  type: "button",
1512
1690
  className: "dsh-subscription-primary",
1513
- disabled: loading || saving || !Object.keys(changes).length && !modelsDirty,
1691
+ disabled: loading || saving || !Object.keys(changes).length && !modelsDirty && !ttlDirty,
1514
1692
  onClick: () => {
1515
1693
  save();
1516
1694
  },
@@ -3588,7 +3766,12 @@ function ResetCreditsDisclosure({ mode, credits, t, now = Date.now(), nextCredit
3588
3766
  }),
3589
3767
  mode === "grants" && state === "notUsable" && credit.cooldownUntil !== void 0 && credit.cooldownUntil > now && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3590
3768
  "aria-hidden": "true",
3591
- children: [t("resetCreditCooldownUntil", { date: new Date(credit.cooldownUntil).toLocaleString() }), " · "]
3769
+ children: [
3770
+ t("resetCreditCooldownUntil", { date: new Date(credit.cooldownUntil).toLocaleString() }),
3771
+ " ",
3772
+ "·",
3773
+ " "
3774
+ ]
3592
3775
  }),
3593
3776
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3594
3777
  "aria-hidden": mode === "grants" ? true : void 0,
@@ -3734,7 +3917,7 @@ const styles$6 = {
3734
3917
  //#endregion
3735
3918
  //#region src/client/SubscriptionsSection.tsx
3736
3919
  /** Poll cadence while a provider login attempt is busy. */
3737
- const POLL_INTERVAL_MS$1 = 2e3;
3920
+ const POLL_INTERVAL_MS = 2e3;
3738
3921
  const CLIENT_VERSION_SOURCES = {
3739
3922
  npm: "clientVersionNpm",
3740
3923
  local: "clientVersionLocal",
@@ -4063,7 +4246,7 @@ function SubscriptionsSection(props) {
4063
4246
  if (pollersRef.current.has(provider)) return;
4064
4247
  pollersRef.current.set(provider, setInterval(() => {
4065
4248
  refresh();
4066
- }, POLL_INTERVAL_MS$1));
4249
+ }, POLL_INTERVAL_MS));
4067
4250
  }, [refresh]);
4068
4251
  (0, react.useEffect)(() => {
4069
4252
  mountedRef.current = true;
@@ -4225,7 +4408,8 @@ function SubscriptionsSection(props) {
4225
4408
  rpc,
4226
4409
  t,
4227
4410
  setProviderError,
4228
- startPolling
4411
+ startPolling,
4412
+ refresh
4229
4413
  ]);
4230
4414
  const cancel = (0, react.useCallback)(async (provider) => {
4231
4415
  if (rpc === void 0) return;
@@ -4369,6 +4553,7 @@ function SubscriptionsSection(props) {
4369
4553
  accounts.map((account) => {
4370
4554
  const usageKey = `${id}:${account.key}`;
4371
4555
  const usage = usages[usageKey];
4556
+ const shownWindows = visibleUsageWindows(id, usage?.windows, status?.visibleModels);
4372
4557
  const usageError = usageErrors[usageKey];
4373
4558
  const display = account.account ?? account.key;
4374
4559
  const sortedCredits = [...usage?.resetCredits ?? []].sort((a, b) => (a.expiresAt ?? Infinity) - (b.expiresAt ?? Infinity));
@@ -4451,7 +4636,11 @@ function SubscriptionsSection(props) {
4451
4636
  style: styles$5.statusLine,
4452
4637
  children: t("usageEmpty")
4453
4638
  }),
4454
- (usage?.windows ?? []).map((window$1, index) => {
4639
+ usage?.windows !== void 0 && usage.windows.length > 0 && shownWindows.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4640
+ style: styles$5.statusLine,
4641
+ children: t("usageNoVisibleModels")
4642
+ }),
4643
+ shownWindows.map((window$1, index) => {
4455
4644
  const percent = displayUsedPercent(window$1.usedPercent);
4456
4645
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4457
4646
  style: styles$5.usageRow,
@@ -4522,7 +4711,7 @@ function SubscriptionsSection(props) {
4522
4711
  },
4523
4712
  children: t("addAccount")
4524
4713
  }),
4525
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4714
+ !busy && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4526
4715
  type: "button",
4527
4716
  "aria-haspopup": "dialog",
4528
4717
  onClick: () => setManagedProvider({
@@ -4614,7 +4803,10 @@ function SubscriptionsSection(props) {
4614
4803
  name: managedProvider.name,
4615
4804
  rpc,
4616
4805
  t,
4617
- onClose: () => setManagedProvider(void 0)
4806
+ onClose: () => {
4807
+ setManagedProvider(void 0);
4808
+ refresh();
4809
+ }
4618
4810
  }),
4619
4811
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: ".subscriptions-reset-dialog::backdrop { background: var(--dsw-alias-bg-mask-1, #0006); backdrop-filter: var(--dsw-mask-blur, none); }" }),
4620
4812
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dialog", {
@@ -4972,7 +5164,8 @@ const HOST_CONTRACT = {
4972
5164
  slots: "@deepseek-ai/dsh-client-ui-renderer"
4973
5165
  },
4974
5166
  markers: { composerStats: {
4975
- attribute: "data-composer-stats",
5167
+ row: "data-composer-stats",
5168
+ dock: "data-composer-stat",
4976
5169
  package: "@deepseek-ai/dsh-client-ui-chat"
4977
5170
  } },
4978
5171
  diagnostics: {
@@ -5025,6 +5218,10 @@ const HOST_CONTRACT = {
5025
5218
  ]
5026
5219
  }
5027
5220
  };
5221
+ /** Attribute selector for a host DOM marker (`data-composer-stats` → `[data-composer-stats]`). */
5222
+ function markerSelector(attribute) {
5223
+ return `[${attribute}]`;
5224
+ }
5028
5225
 
5029
5226
  //#endregion
5030
5227
  //#region src/client/host-icons.ts
@@ -5435,9 +5632,10 @@ function VideoGenerateToolview(props) {
5435
5632
  //#region src/client/SpeedSelect.tsx
5436
5633
  /**
5437
5634
  * 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.
5635
+ * the host's current model selection (the visibility gate). A session whose
5636
+ * model selection is unavailable (an addressed subagent) answers hidden.
5637
+ * Any other model-directory failure throws, so the caller keeps its last
5638
+ * known state instead of locking the toggle away.
5441
5639
  *
5442
5640
  * `sessionId` is a plain string: slot and command contexts brand it through
5443
5641
  * different dsh-session copies, and only the service boundary needs one.
@@ -5448,19 +5646,127 @@ function VideoGenerateToolview(props) {
5448
5646
  */
5449
5647
  function createSpeedLoader(connection, models, sessionId) {
5450
5648
  return async () => {
5451
- const state = await callSubscriptionsAuth(connection.rpc, "speed", { sessionId });
5452
5649
  const directories = models();
5453
- if (directories === void 0) return {
5454
- visible: false,
5455
- tier: state.tier
5456
- };
5457
- const { current } = await directories.directoryFor(sessionId).load();
5650
+ let current = null;
5651
+ if (directories !== void 0) try {
5652
+ current = (await directories.directoryFor(sessionId).load()).current;
5653
+ } catch (error) {
5654
+ if (!modelSelectionUnavailable(error)) throw error;
5655
+ }
5656
+ const state = await callSubscriptionsAuth(connection.rpc, "speed", {
5657
+ sessionId,
5658
+ ...current?.provider === "codex" ? { model: current.model } : {}
5659
+ });
5458
5660
  return {
5459
5661
  visible: current !== null && current.provider === "codex" && state.fastModels.includes(current.model),
5460
5662
  tier: state.tier
5461
5663
  };
5462
5664
  };
5463
5665
  }
5666
+ /** Host copy for a session that has no model seat (an addressed subagent). */
5667
+ const MODEL_SELECTION_UNAVAILABLE = "model selection is unavailable";
5668
+ function modelSelectionUnavailable(error) {
5669
+ return (error instanceof Error ? error.message : String(error)).includes(MODEL_SELECTION_UNAVAILABLE);
5670
+ }
5671
+ /** Stable identity of a model selection. Empty when nothing is selected. */
5672
+ function modelSelectionKey(current) {
5673
+ if (current == null) return "";
5674
+ return JSON.stringify([current.provider, current.model]);
5675
+ }
5676
+ /**
5677
+ * How often a host without a directory store is re-read, and how often a
5678
+ * missing directory service is retried. Neither interval calls
5679
+ * `subscriptions-auth.speed`.
5680
+ */
5681
+ const MODEL_DIRECTORY_POLL_INTERVAL_MS = 3e3;
5682
+ /**
5683
+ * Mount reads of `speed`, including the first attempt. Further failures stop
5684
+ * instead of polling for the life of the composer.
5685
+ */
5686
+ const SPEED_MOUNT_READ_ATTEMPTS = 4;
5687
+ /** Fired after `/fast` writes a tier, so an open composer re-reads `speed`. */
5688
+ const SPEED_CHANGED_EVENT = "dsh:subscriptions:speed-changed";
5689
+ /** Tell mounted Speed controls that this page changed a session tier. */
5690
+ function notifySpeedChanged() {
5691
+ if (typeof window === "undefined") return;
5692
+ window.dispatchEvent(new Event(SPEED_CHANGED_EVENT));
5693
+ }
5694
+ /**
5695
+ * Subscribe to the session's selected model. A directory store notifies the
5696
+ * listener only when `provider` or `model` changes. A directory with no store
5697
+ * is re-read on {@link MODEL_DIRECTORY_POLL_INTERVAL_MS}; a service that is
5698
+ * not registered yet, or whose `directoryFor` throws, is retried on that same
5699
+ * interval. The first successful attach after a miss notifies once, so a
5700
+ * mount that ran before the service existed reads `speed` again.
5701
+ */
5702
+ function createModelSelectionWatch(models, sessionId) {
5703
+ return (listener) => {
5704
+ let stopped = false;
5705
+ let unsubscribeStore;
5706
+ let timer;
5707
+ let mode = "seek";
5708
+ let lastKey;
5709
+ const stopTimer = () => {
5710
+ if (timer === void 0) return;
5711
+ clearInterval(timer);
5712
+ timer = void 0;
5713
+ };
5714
+ const changed = (current) => {
5715
+ const next = modelSelectionKey(current);
5716
+ if (lastKey === void 0) {
5717
+ lastKey = next;
5718
+ return false;
5719
+ }
5720
+ if (next === lastKey) return false;
5721
+ lastKey = next;
5722
+ return true;
5723
+ };
5724
+ const attach = (late) => {
5725
+ if (stopped || mode !== "seek") return;
5726
+ let directory;
5727
+ try {
5728
+ directory = models()?.directoryFor(sessionId);
5729
+ } catch {
5730
+ return;
5731
+ }
5732
+ if (directory === void 0) return;
5733
+ const store = directory.store;
5734
+ if (store !== void 0) {
5735
+ mode = "store";
5736
+ stopTimer();
5737
+ lastKey = modelSelectionKey(store.getSnapshot().current);
5738
+ unsubscribeStore = store.subscribe(() => {
5739
+ if (!stopped && changed(store.getSnapshot().current)) listener();
5740
+ });
5741
+ if (late) listener();
5742
+ return;
5743
+ }
5744
+ mode = "poll";
5745
+ stopTimer();
5746
+ let force = late;
5747
+ const seed = () => {
5748
+ directory.load().then((state) => {
5749
+ if (stopped || mode !== "poll") return;
5750
+ const identityChanged = changed(state.current);
5751
+ const forced = force;
5752
+ force = false;
5753
+ if (identityChanged || forced) listener();
5754
+ }, () => {});
5755
+ };
5756
+ seed();
5757
+ timer = setInterval(seed, MODEL_DIRECTORY_POLL_INTERVAL_MS);
5758
+ };
5759
+ attach(false);
5760
+ if (mode === "seek") timer = setInterval(() => {
5761
+ attach(true);
5762
+ }, MODEL_DIRECTORY_POLL_INTERVAL_MS);
5763
+ return () => {
5764
+ stopped = true;
5765
+ unsubscribeStore?.();
5766
+ stopTimer();
5767
+ };
5768
+ };
5769
+ }
5464
5770
  /** The `setSpeed` half of the inject face: boolean outcome for the component's busy state. */
5465
5771
  function createSpeedSetter(connection, sessionId) {
5466
5772
  return (tier) => callSubscriptionsAuth(connection.rpc, "setSpeed", {
@@ -5476,20 +5782,15 @@ const TIERS = ["standard", "fast"];
5476
5782
  /**
5477
5783
  * The composer Speed control: a trigger reading `速度 · 快速`/`速度 · 标准`
5478
5784
  * 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.
5481
- */
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).
5785
+ * the current tier). It reads `subscriptions-auth.speed` on mount, when the
5786
+ * menu opens, when {@link SpeedSelectInjected.watchModel} reports a new
5787
+ * selected model, and when {@link notifySpeedChanged} fires. Until the first
5788
+ * read succeeds, a failure is retried on
5789
+ * {@link MODEL_DIRECTORY_POLL_INTERVAL_MS}, at most
5790
+ * {@link SPEED_MOUNT_READ_ATTEMPTS} times; after that, a failed read keeps
5791
+ * the last known state.
5491
5792
  */
5492
- function SpeedSelect({ loadSpeed, setSpeed, t }) {
5793
+ function SpeedSelect({ loadSpeed, setSpeed, watchModel, t }) {
5493
5794
  const translate = t ?? fallbackTranslate$1;
5494
5795
  const [state, setState] = (0, react.useState)(null);
5495
5796
  const [open, setOpen] = (0, react.useState)(false);
@@ -5497,25 +5798,58 @@ function SpeedSelect({ loadSpeed, setSpeed, t }) {
5497
5798
  const rootRef = (0, react.useRef)(null);
5498
5799
  const loadRef = (0, react.useRef)(loadSpeed);
5499
5800
  loadRef.current = loadSpeed;
5801
+ const watchRef = (0, react.useRef)(watchModel);
5802
+ watchRef.current = watchModel;
5500
5803
  (0, react.useEffect)(() => {
5501
5804
  if (loadRef.current === void 0) return;
5502
5805
  let cancelled = false;
5503
5806
  let inflight = false;
5807
+ let again = false;
5808
+ let loadedOnce = false;
5809
+ let failures = 0;
5810
+ let retry;
5811
+ const stopRetry = () => {
5812
+ if (retry === void 0) return;
5813
+ clearInterval(retry);
5814
+ retry = void 0;
5815
+ };
5504
5816
  const reload = () => {
5505
5817
  const load = loadRef.current;
5506
- if (load === void 0 || inflight) return;
5818
+ if (load === void 0) return;
5819
+ if (inflight) {
5820
+ again = true;
5821
+ return;
5822
+ }
5507
5823
  inflight = true;
5508
5824
  load().then((loaded) => {
5509
- if (!cancelled) setState(loaded);
5510
- }, () => {}).finally(() => {
5825
+ if (cancelled) return;
5826
+ loadedOnce = true;
5827
+ stopRetry();
5828
+ setState(loaded);
5829
+ }, () => {
5830
+ failures += 1;
5831
+ if (failures >= SPEED_MOUNT_READ_ATTEMPTS) stopRetry();
5832
+ }).finally(() => {
5511
5833
  inflight = false;
5834
+ if (cancelled || !again) return;
5835
+ again = false;
5836
+ reload();
5512
5837
  });
5513
5838
  };
5514
5839
  reload();
5515
- const timer = setInterval(reload, POLL_INTERVAL_MS);
5840
+ retry = setInterval(() => {
5841
+ if (!loadedOnce) reload();
5842
+ }, MODEL_DIRECTORY_POLL_INTERVAL_MS);
5843
+ const unsubscribe = watchRef.current?.(reload);
5844
+ const onSpeedChanged = () => {
5845
+ reload();
5846
+ };
5847
+ window.addEventListener(SPEED_CHANGED_EVENT, onSpeedChanged);
5516
5848
  return () => {
5517
5849
  cancelled = true;
5518
- clearInterval(timer);
5850
+ unsubscribe?.();
5851
+ stopRetry();
5852
+ window.removeEventListener(SPEED_CHANGED_EVENT, onSpeedChanged);
5519
5853
  };
5520
5854
  }, []);
5521
5855
  (0, react.useEffect)(() => {
@@ -5711,7 +6045,7 @@ const PROVIDER_NAMES = {
5711
6045
  "cursor-subscription": "Cursor",
5712
6046
  "opencode-go": "OpenCode Go",
5713
6047
  "kimi-coding": "Kimi Code",
5714
- "minimax": "MiniMax",
6048
+ minimax: "MiniMax",
5715
6049
  "minimax-cn": "MiniMax CN",
5716
6050
  "ollama-cloud": "Ollama Cloud"
5717
6051
  };
@@ -5946,6 +6280,8 @@ function usageWindowLabel(t, window$1) {
5946
6280
  * every provider's accounts and their windows. Returns null when no data is
5947
6281
  * available.
5948
6282
  */
6283
+ const STATS_ROW_SELECTOR = markerSelector(HOST_CONTRACT.markers.composerStats.row);
6284
+ const STATS_DOCK_SELECTOR = markerSelector(HOST_CONTRACT.markers.composerStats.dock);
5949
6285
  function SubscriptionUsageBadge({ rpc, currentModel, t, rotationMs }) {
5950
6286
  const translate = t ?? fallbackTranslate;
5951
6287
  const [displays, setDisplays] = (0, react.useState)([]);
@@ -6105,15 +6441,18 @@ function SubscriptionUsageBadge({ rpc, currentModel, t, rotationMs }) {
6105
6441
  if (seat$1 === null) return;
6106
6442
  const scope = statsScopeOf(seat$1);
6107
6443
  if (scope === null) return;
6108
- const find = () => scope.querySelector("[data-composer-stats]");
6109
- setStatsRow(find());
6110
- const observer = new MutationObserver(() => {
6111
- setStatsRow(find());
6112
- });
6444
+ const find = () => scope.querySelector(STATS_ROW_SELECTOR);
6445
+ const settle = () => {
6446
+ const row = find();
6447
+ setStatsRow(row);
6448
+ if (row === null && scope.querySelector(STATS_DOCK_SELECTOR) !== null) observer.disconnect();
6449
+ };
6450
+ const observer = new MutationObserver(settle);
6113
6451
  observer.observe(scope, {
6114
6452
  childList: true,
6115
6453
  subtree: true
6116
6454
  });
6455
+ settle();
6117
6456
  return () => {
6118
6457
  observer.disconnect();
6119
6458
  };
@@ -6237,15 +6576,19 @@ function SubscriptionUsageBadge({ rpc, currentModel, t, rotationMs }) {
6237
6576
  });
6238
6577
  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
6578
  }
6579
+ /** How far statsScopeOf walks looking for `composerStats.row`. */
6580
+ const LEGACY_STATS_SCOPE_DEPTH = 4;
6240
6581
  /**
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.
6582
+ * Scope for the legacy stats row. The nearest ancestor within
6583
+ * LEGACY_STATS_SCOPE_DEPTH that contains `composerStats.row`, so a badge
6584
+ * does not adopt another composer's pills. When none does, the seat's
6585
+ * parent: the slot outlet, which holds sibling `composerStats.dock` anchors
6586
+ * on hosts whose pills are dock entries.
6244
6587
  */
6245
6588
  function statsScopeOf(seat) {
6246
6589
  let node = seat.parentElement;
6247
- for (let depth = 0; node !== null && depth < 4; depth++) {
6248
- if (node.querySelector("[data-composer-stats]") !== null) return node;
6590
+ for (let depth = 0; node !== null && depth < LEGACY_STATS_SCOPE_DEPTH; depth++) {
6591
+ if (node.querySelector(STATS_ROW_SELECTOR) !== null) return node;
6249
6592
  node = node.parentElement;
6250
6593
  }
6251
6594
  return seat.parentElement;
@@ -6260,10 +6603,10 @@ function AccountMeta({ account, translate }) {
6260
6603
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
6261
6604
  style: styles.providerMeta,
6262
6605
  title: account.account,
6263
- children: [account.isDefault && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6606
+ children: [account.isDefault && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
6264
6607
  style: styles.defaultStar,
6265
6608
  "aria-label": "default",
6266
- children: "★ "
6609
+ children: ["★", " "]
6267
6610
  }), parts.join(" · ")]
6268
6611
  });
6269
6612
  }
@@ -6545,7 +6888,8 @@ function apply(ctx) {
6545
6888
  locale: NS,
6546
6889
  inject: (sessionId) => ({
6547
6890
  loadSpeed: createSpeedLoader(connection, models, sessionId),
6548
- setSpeed: createSpeedSetter(connection, sessionId)
6891
+ setSpeed: createSpeedSetter(connection, sessionId),
6892
+ watchModel: createModelSelectionWatch(models, sessionId)
6549
6893
  })
6550
6894
  }, SpeedSelect));
6551
6895
  ctx.slots.inject("conversation.composer.dock", () => ctx.slots.register({
@@ -6583,7 +6927,7 @@ function apply(ctx) {
6583
6927
  }));
6584
6928
  },
6585
6929
  onSelect: async (option, session) => {
6586
- await createSpeedSetter(connection, session.sessionId)(option.id);
6930
+ if (await createSpeedSetter(connection, session.sessionId)(option.id)) notifySpeedChanged();
6587
6931
  }
6588
6932
  }
6589
6933
  }), "dsh-subscriptions: /fast contribution");