dsh-subscriptions 0.1.5 → 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 (110) hide show
  1. package/README.md +212 -154
  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 +73 -2
  6. package/lib/auth/rpc.js +124 -23
  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 +13 -0
  16. package/lib/client/OllamaCard.js +94 -0
  17. package/lib/client/OllamaSettingsManager.d.ts +24 -0
  18. package/lib/client/OllamaSettingsManager.js +329 -0
  19. package/lib/client/ProviderAccountManager.d.ts +1 -1
  20. package/lib/client/ProviderAccountManager.js +164 -49
  21. package/lib/client/ProviderModelEditor.js +109 -26
  22. package/lib/client/SpeedSelect.d.ts +62 -14
  23. package/lib/client/SpeedSelect.js +257 -46
  24. package/lib/client/SubscriptionUsageBadge.d.ts +5 -11
  25. package/lib/client/SubscriptionUsageBadge.js +301 -112
  26. package/lib/client/SubscriptionsSection.d.ts +2 -0
  27. package/lib/client/SubscriptionsSection.js +259 -85
  28. package/lib/client/UsageBadgeDisplaySetting.js +35 -12
  29. package/lib/client/UsageMeter.d.ts +1 -1
  30. package/lib/client/UsageMeter.js +69 -27
  31. package/lib/client/VideoGenerateToolview.js +69 -17
  32. package/lib/client/account-preferences.d.ts +8 -0
  33. package/lib/client/account-preferences.js +20 -5
  34. package/lib/client/host-contract.d.ts +11 -2
  35. package/lib/client/host-contract.js +20 -3
  36. package/lib/client/index.d.ts +5 -5
  37. package/lib/client/index.js +34 -12
  38. package/lib/client/locales.d.ts +78 -0
  39. package/lib/client/locales.js +121 -34
  40. package/lib/client/ollama-draft.d.ts +229 -0
  41. package/lib/client/ollama-draft.js +255 -0
  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.d.ts +7 -0
  48. package/lib/client/usage-pace.js +43 -10
  49. package/lib/client.js +1647 -266
  50. package/lib/client.js.map +1 -1
  51. package/lib/http.js +1 -1
  52. package/lib/index.d.ts +28 -3
  53. package/lib/index.js +9329 -8217
  54. package/lib/model-defaults.js +4 -2
  55. package/lib/provider-settings.d.ts +14 -1
  56. package/lib/provider-settings.js +64 -20
  57. package/lib/providers/account-preferences.js +51 -22
  58. package/lib/providers/accounts.js +15 -9
  59. package/lib/providers/antigravity-oauth-client.js +2 -2
  60. package/lib/providers/antigravity.js +141 -68
  61. package/lib/providers/catalog-store.js +120 -41
  62. package/lib/providers/claude-cli-version.js +3 -3
  63. package/lib/providers/claude.d.ts +23 -3
  64. package/lib/providers/claude.js +167 -92
  65. package/lib/providers/codex-client-version.js +3 -3
  66. package/lib/providers/codex-search.js +40 -13
  67. package/lib/providers/codex.js +196 -112
  68. package/lib/providers/common.d.ts +14 -0
  69. package/lib/providers/common.js +118 -38
  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.d.ts +9 -2
  77. package/lib/providers/external-usage-controller.js +30 -7
  78. package/lib/providers/external-usage.d.ts +8 -0
  79. package/lib/providers/external-usage.js +140 -20
  80. package/lib/providers/grok.js +152 -89
  81. package/lib/providers/image-pool.js +29 -11
  82. package/lib/providers/minimax-usage.js +31 -11
  83. package/lib/providers/npm-cli-version.js +10 -3
  84. package/lib/providers/ollama-settings.d.ts +90 -0
  85. package/lib/providers/ollama-settings.js +213 -0
  86. package/lib/providers/ollama-web.d.ts +46 -0
  87. package/lib/providers/ollama-web.js +231 -0
  88. package/lib/providers/ollama.d.ts +107 -0
  89. package/lib/providers/ollama.js +608 -0
  90. package/lib/providers/pool-family.js +11 -3
  91. package/lib/providers/pool-health.js +28 -8
  92. package/lib/providers/pool-usage.js +16 -9
  93. package/lib/providers/pool.d.ts +8 -0
  94. package/lib/providers/pool.js +81 -42
  95. package/lib/providers/rate-limit.js +8 -2
  96. package/lib/providers/replay.js +40 -17
  97. package/lib/providers/reset-redemption.js +18 -6
  98. package/lib/tools/image-generate.js +123 -65
  99. package/lib/tools/registration.js +5 -2
  100. package/lib/tools/video-generate.js +51 -31
  101. package/lib/tools/x-search.js +50 -22
  102. package/lib/translate/anthropic.js +85 -37
  103. package/lib/translate/antigravity-schema.js +71 -28
  104. package/lib/translate/antigravity-thinking.js +22 -9
  105. package/lib/translate/antigravity.js +141 -50
  106. package/lib/translate/chat-completions.js +67 -23
  107. package/lib/translate/resolved.js +55 -21
  108. package/lib/translate/responses.js +69 -29
  109. package/lib/translate/sse.js +4 -1
  110. package/package.json +26 -4
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.",
@@ -108,6 +113,7 @@ const en = {
108
113
  usageRefresh: "Refresh",
109
114
  usageLoading: "Loading usage…",
110
115
  usageEmpty: "No usage windows reported.",
116
+ usageUnsupported: "This endpoint has no usage surface.",
111
117
  usageError: "Usage lookup failed: {message}",
112
118
  usageSession: "5-hour window",
113
119
  usageWeekly: "Weekly",
@@ -233,7 +239,40 @@ const en = {
233
239
  speedFast: "Fast",
234
240
  speedFastDescription: "1.5x speed, more usage",
235
241
  commandFast: "Switch the Codex speed tier (Standard/Fast)",
236
- commandFastUnavailable: "The current model has no fast tier; /fast only works on Codex models whose catalog advertises one"
242
+ commandFastUnavailable: "The current model has no fast tier; /fast only works on Codex models whose catalog advertises one",
243
+ ollamaTitle: "Ollama Cloud",
244
+ ollamaStatus: "{count} model(s) in the picker",
245
+ ollamaStatusNone: "No models configured",
246
+ ollamaWebOn: "Web search/fetch enabled",
247
+ ollamaWebOff: "Web search/fetch off",
248
+ ollamaManageTitle: "Manage Ollama Cloud",
249
+ ollamaManageHint: "Fetch the Cloud list, tick the models this route serves, then Save. The picker shows the ticked models in order; the API key itself stays in DSH credentials.",
250
+ ollamaFetch: "Fetch cloud models",
251
+ ollamaFetching: "Fetching…",
252
+ ollamaKeyPlaceholder: "One-time API key (optional)",
253
+ ollamaKeyHint: "A key typed here is used for this fetch only and is never saved. Leave it blank to use the stored credential.",
254
+ ollamaFetchFailed: "Could not fetch cloud models: {message}",
255
+ ollamaCandidatesCount: "{count} returned",
256
+ ollamaCandidatesEmpty: "No cloud models returned.",
257
+ ollamaModelsEmpty: "No models configured. Fetch the cloud list and tick the ones to keep.",
258
+ ollamaModelsLoadFailed: "Could not load Ollama settings: {message}",
259
+ ollamaModelsSaveFailed: "Could not save Ollama settings: {message}",
260
+ ollamaConfigureHint: "No API key found at {ref}. Add it to the credentials file (~/.dsh/.credentials.yaml).",
261
+ ollamaDisplayName: "Display name",
262
+ ollamaRowDetails: "Details",
263
+ ollamaEffortsUnknown: "This model reasons; its level set will come from live discovery.",
264
+ ollamaMoveUp: "Move up",
265
+ ollamaMoveDown: "Move down",
266
+ ollamaBaseURL: "Base URL",
267
+ ollamaApiKeyRef: "API key reference",
268
+ ollamaConnectionHint: "The base URL receives this route's requests and the credential lookup; the reference names the DSH credential, not the key itself. Requests send the resolved key to that address, so point it only at a host you trust.",
269
+ ollamaWebHint: "Enables the Ollama Cloud web search and fetch providers. The host still has to select them before the web tools use them.",
270
+ ollamaCatalogOverride: "A profile pins this route's model list; the picker ignores store models until that override is removed.",
271
+ ollamaResetDefaults: "Restore deployment defaults",
272
+ ollamaResetConfirm: "Clear every saved Ollama override? The profile values and built-in defaults apply again.",
273
+ ollamaCapVision: "vision",
274
+ ollamaCapThinking: "thinking",
275
+ ollamaContext: "context {tokens}"
237
276
  };
238
277
  /** zh strings, one per {@link en} key. */
239
278
  const zh = {
@@ -273,6 +312,11 @@ const zh = {
273
312
  modelsWebSearch: "网页搜索",
274
313
  modelsSave: "保存更改",
275
314
  modelsUnavailable: "当前不可用",
315
+ claudeCacheTtl: "提示词缓存时长",
316
+ claudeCacheTtlFollowConfig: "跟随配置",
317
+ claudeCacheTtl5m: "5 分钟",
318
+ claudeCacheTtl1h: "1 小时",
319
+ claudeCacheTtlHint: "覆盖此 Claude 路由的 claudePromptCacheTtl 配置;选择「跟随配置」时使用配置值。保存后自下一次请求生效。1 小时可更久保留缓存前缀,但写入按更高的输入价格计费;压缩与会话标题请求仍为 5 分钟。",
276
320
  nav: "订阅",
277
321
  intro: "在此登录或退出订阅服务商。点击登录会在新标签页打开服务商的授权页面;无浏览器环境可改为粘贴回调 URL 或授权码。",
278
322
  unavailable: "连接不可用,无法加载订阅状态。",
@@ -309,6 +353,7 @@ const zh = {
309
353
  usageRefresh: "刷新",
310
354
  usageLoading: "用量加载中…",
311
355
  usageEmpty: "服务商未返回任何用量窗口。",
356
+ usageUnsupported: "该端点不提供用量查询。",
312
357
  usageError: "用量查询失败:{message}",
313
358
  usageSession: "5 小时窗口",
314
359
  usageWeekly: "每周",
@@ -434,7 +479,40 @@ const zh = {
434
479
  speedFast: "快速",
435
480
  speedFastDescription: "约 1.5 倍速度,消耗更多用量",
436
481
  commandFast: "切换 Codex 速度档(标准/快速)",
437
- commandFastUnavailable: "当前模型不支持快速档;/fast 仅对目录声明了 fast tier 的 Codex 模型可用"
482
+ commandFastUnavailable: "当前模型不支持快速档;/fast 仅对目录声明了 fast tier 的 Codex 模型可用",
483
+ ollamaTitle: "Ollama Cloud",
484
+ ollamaStatus: "选择器中 {count} 个模型",
485
+ ollamaStatusNone: "尚未配置模型",
486
+ ollamaWebOn: "网页搜索/抓取已启用",
487
+ ollamaWebOff: "网页搜索/抓取已关闭",
488
+ ollamaManageTitle: "管理 Ollama Cloud",
489
+ ollamaManageHint: "选择此路由提供的云端模型后保存。模型选择器按此顺序显示这份列表;API 密钥本身仍保存在 DSH 凭据中。",
490
+ ollamaFetch: "拉取云端模型",
491
+ ollamaFetching: "拉取中…",
492
+ ollamaKeyPlaceholder: "一次性 API 密钥(可选)",
493
+ ollamaKeyHint: "在此输入的密钥仅用于本次拉取,不会保存。留空则使用已存凭据。",
494
+ ollamaFetchFailed: "拉取云端模型失败:{message}",
495
+ ollamaCandidatesCount: "返回 {count} 个",
496
+ ollamaCandidatesEmpty: "未返回任何云端模型。",
497
+ ollamaModelsEmpty: "尚未配置模型。请拉取云端列表并勾选需要保留的模型。",
498
+ ollamaModelsLoadFailed: "Ollama 设置加载失败:{message}",
499
+ ollamaModelsSaveFailed: "Ollama 设置保存失败:{message}",
500
+ ollamaConfigureHint: "未在 {ref} 找到 API 密钥。请在凭据文件(~/.dsh/.credentials.yaml)中添加。",
501
+ ollamaDisplayName: "显示名",
502
+ ollamaRowDetails: "详情",
503
+ ollamaEffortsUnknown: "该模型支持思考,推理档位将由实时发现补充。",
504
+ ollamaMoveUp: "上移",
505
+ ollamaMoveDown: "下移",
506
+ ollamaBaseURL: "基础 URL",
507
+ ollamaApiKeyRef: "API 密钥引用",
508
+ ollamaConnectionHint: "基础 URL 用于此路由的请求与凭据查询;引用是 DSH 凭据的名称,不是密钥本身。请求会把解析出的密钥发往该地址,请只填写可信主机。",
509
+ ollamaWebHint: "启用 Ollama Cloud 的网页搜索与抓取能力。宿主仍需选择它们,网页工具才会使用。",
510
+ ollamaCatalogOverride: "配置档案固定了此路由的模型列表;在移除该覆盖前,选择器会忽略存储中的模型。",
511
+ ollamaResetDefaults: "恢复部署默认值",
512
+ ollamaResetConfirm: "清除全部已保存的 Ollama 覆盖项?将重新使用配置档案值与内置默认值。",
513
+ ollamaCapVision: "视觉",
514
+ ollamaCapThinking: "思考",
515
+ ollamaContext: "上下文 {tokens}"
438
516
  };
439
517
 
440
518
  //#endregion
@@ -508,6 +586,19 @@ function mergeLatestAccounts(draft, latest) {
508
586
  ...latest.accounts === void 0 ? {} : { accounts: latest.accounts }
509
587
  };
510
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
+ }
511
602
  /** Only touched accounts replace their latest preferences; other settings survive. */
512
603
  function mergeAccountChanges(latest, changes) {
513
604
  return {
@@ -521,8 +612,10 @@ function mergeAccountChanges(latest, changes) {
521
612
 
522
613
  //#endregion
523
614
  //#region src/client/ProviderModelEditor.tsx
524
- const border$1 = "0.5px solid var(--dsw-alias-border-l2, #ddd)";
525
- const actions$1 = {
615
+ /** Providers whose runtime honours a local context-window override. */
616
+ const supportsContext = (provider) => provider === "codex" || provider === "claude";
617
+ const border$2 = "0.5px solid var(--dsw-alias-border-l2, #ddd)";
618
+ const actions$2 = {
526
619
  display: "flex",
527
620
  flexWrap: "wrap",
528
621
  gap: 8,
@@ -595,7 +688,7 @@ const ProviderModelEditor = (0, react.forwardRef)(function ProviderModelEditor$1
595
688
  setError("");
596
689
  const { accounts: _accounts,...settings } = {
597
690
  ...draft,
598
- ...provider === "codex" ? { contextWindows: windows } : {}
691
+ ...supportsContext(provider) ? { contextWindows: windows } : {}
599
692
  };
600
693
  return {
601
694
  settings,
@@ -613,7 +706,8 @@ const ProviderModelEditor = (0, react.forwardRef)(function ProviderModelEditor$1
613
706
  draft,
614
707
  contexts,
615
708
  efforts,
616
- provider
709
+ provider,
710
+ t
617
711
  ]);
618
712
  const allModels = catalog?.models ?? [];
619
713
  const known = new Set(allModels.map((model) => model.id));
@@ -625,7 +719,7 @@ const ProviderModelEditor = (0, react.forwardRef)(function ProviderModelEditor$1
625
719
  const selected = new Set(draft.visibleModels ?? allModels.map((model) => model.id));
626
720
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
627
721
  style: {
628
- borderTop: border$1,
722
+ borderTop: border$2,
629
723
  marginTop: 12,
630
724
  paddingTop: 12
631
725
  },
@@ -648,7 +742,7 @@ const ProviderModelEditor = (0, react.forwardRef)(function ProviderModelEditor$1
648
742
  children: error
649
743
  }),
650
744
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
651
- style: actions$1,
745
+ style: actions$2,
652
746
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
653
747
  type: "button",
654
748
  className: "dsh-subscription-quiet",
@@ -688,7 +782,7 @@ const ProviderModelEditor = (0, react.forwardRef)(function ProviderModelEditor$1
688
782
  t("modelsAutomatic")
689
783
  ] }),
690
784
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
691
- style: actions$1,
785
+ style: actions$2,
692
786
  children: [
693
787
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
694
788
  style: { flex: "1 1 180px" },
@@ -724,7 +818,7 @@ const ProviderModelEditor = (0, react.forwardRef)(function ProviderModelEditor$1
724
818
  },
725
819
  children: [models.map((model) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
726
820
  style: {
727
- borderBottom: border$1,
821
+ borderBottom: border$2,
728
822
  padding: "8px 2px",
729
823
  display: "grid",
730
824
  gap: 8
@@ -750,9 +844,9 @@ const ProviderModelEditor = (0, react.forwardRef)(function ProviderModelEditor$1
750
844
  !known.has(model.id) && ` (${t("modelsUnavailable")})`
751
845
  ]
752
846
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
753
- style: actions$1,
847
+ style: actions$2,
754
848
  children: [(model.efforts?.length ?? 0) > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
755
- style: actions$1,
849
+ style: actions$2,
756
850
  children: [t("modelDefaultsTitle"), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
757
851
  "aria-label": `${model.name} ${t("modelDefaultsTitle")}`,
758
852
  value: efforts[model.id] ?? "",
@@ -784,10 +878,10 @@ const ProviderModelEditor = (0, react.forwardRef)(function ProviderModelEditor$1
784
878
  }, effort.id))
785
879
  ]
786
880
  })]
787
- }), provider === "codex" && model.maxContextWindow !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
788
- style: actions$1,
881
+ }), supportsContext(provider) && model.maxContextWindow !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
882
+ style: actions$2,
789
883
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
790
- style: actions$1,
884
+ style: actions$2,
791
885
  children: [t("modelsContext"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
792
886
  style: { width: 140 },
793
887
  inputMode: "numeric",
@@ -810,7 +904,7 @@ const ProviderModelEditor = (0, react.forwardRef)(function ProviderModelEditor$1
810
904
  })]
811
905
  }, model.id)), models.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("modelDefaultsFilterEmpty", { query }) })]
812
906
  }),
813
- provider === "codex" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: t("modelsContextHint") }),
907
+ supportsContext(provider) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: t("modelsContextHint") }),
814
908
  catalog.tools.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
815
909
  style: {
816
910
  display: "grid",
@@ -1054,13 +1148,13 @@ const subscriptionChromeCss = `
1054
1148
 
1055
1149
  //#endregion
1056
1150
  //#region src/client/ProviderAccountManager.tsx
1057
- const border = "0.5px solid var(--dsw-alias-border-l2)";
1058
- const stack = {
1151
+ const border$1 = "0.5px solid var(--dsw-alias-border-l2)";
1152
+ const stack$1 = {
1059
1153
  display: "grid",
1060
1154
  gap: 12,
1061
1155
  minWidth: 0
1062
1156
  };
1063
- const actions = {
1157
+ const actions$1 = {
1064
1158
  display: "flex",
1065
1159
  flexWrap: "wrap",
1066
1160
  gap: 8,
@@ -1079,6 +1173,8 @@ function ProviderAccountManager({ provider, name, rpc, t, onClose }) {
1079
1173
  const [saveError, setSaveError] = (0, react.useState)("");
1080
1174
  const [attempt, setAttempt] = (0, react.useState)(0);
1081
1175
  const [modelsDirty, setModelsDirty] = (0, react.useState)(false);
1176
+ const [ttlDraft, setTtlDraft] = (0, react.useState)();
1177
+ const [ttlDirty, setTtlDirty] = (0, react.useState)(false);
1082
1178
  const alive = (0, react.useRef)(true);
1083
1179
  const saveLock = (0, react.useRef)(false);
1084
1180
  const editor = (0, react.useRef)(null);
@@ -1098,7 +1194,11 @@ function ProviderAccountManager({ provider, name, rpc, t, onClose }) {
1098
1194
  setLoading(true);
1099
1195
  setError("");
1100
1196
  callSubscriptionsAuth(rpc, provider === "cursor-subscription" ? "cursorSettings" : "providerSettings", { provider }).then((data) => {
1101
- if (current) setCatalog(data);
1197
+ if (current) {
1198
+ setCatalog(data);
1199
+ setTtlDraft(data.settings.promptCacheTtl);
1200
+ setTtlDirty(false);
1201
+ }
1102
1202
  }).catch((error$1) => {
1103
1203
  if (current) setError(t("accountsLoadFailed", { message: error$1 instanceof Error ? error$1.message : String(error$1) }));
1104
1204
  }).finally(() => {
@@ -1146,13 +1246,13 @@ function ProviderAccountManager({ provider, name, rpc, t, onClose }) {
1146
1246
  if (provider === "cursor-subscription") await callSubscriptionsAuth(rpc, "cursorSetSettings", { settings: models.settings ?? latest.settings });
1147
1247
  else await callSubscriptionsAuth(rpc, "setProviderSettings", {
1148
1248
  provider,
1149
- 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)
1150
1250
  });
1151
1251
  if (alive.current) onClose();
1152
1252
  } catch (error$1) {
1153
1253
  if (alive.current) {
1154
- const message$1 = error$1 instanceof Error ? error$1.message : String(error$1);
1155
- setSaveError((savedEfforts ? t("modelsPartialSave") + " " : "") + t("accountsSaveFailed", { message: message$1 }));
1254
+ const message$3 = error$1 instanceof Error ? error$1.message : String(error$1);
1255
+ setSaveError((savedEfforts ? t("modelsPartialSave") + " " : "") + t("accountsSaveFailed", { message: message$3 }));
1156
1256
  }
1157
1257
  } finally {
1158
1258
  saveLock.current = false;
@@ -1176,13 +1276,13 @@ function ProviderAccountManager({ provider, name, rpc, t, onClose }) {
1176
1276
  },
1177
1277
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: providerSettingsCss }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1178
1278
  style: {
1179
- ...stack,
1279
+ ...stack$1,
1180
1280
  padding: 20
1181
1281
  },
1182
1282
  children: [
1183
1283
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
1184
1284
  style: {
1185
- ...actions,
1285
+ ...actions$1,
1186
1286
  justifyContent: "space-between"
1187
1287
  },
1188
1288
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
@@ -1219,7 +1319,7 @@ function ProviderAccountManager({ provider, name, rpc, t, onClose }) {
1219
1319
  catalog && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("fieldset", {
1220
1320
  disabled: saving || loading,
1221
1321
  style: {
1222
- ...stack,
1322
+ ...stack$1,
1223
1323
  border: 0,
1224
1324
  margin: 0,
1225
1325
  padding: 0
@@ -1231,7 +1331,7 @@ function ProviderAccountManager({ provider, name, rpc, t, onClose }) {
1231
1331
  }),
1232
1332
  provider === "cursor-subscription" && catalog.accounts.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("fieldset", {
1233
1333
  style: {
1234
- border,
1334
+ border: border$1,
1235
1335
  borderRadius: "var(--dsw-radius-lg, 12px)",
1236
1336
  padding: 14
1237
1337
  },
@@ -1249,8 +1349,8 @@ function ProviderAccountManager({ provider, name, rpc, t, onClose }) {
1249
1349
  const models = accountModelRows(account, preferences);
1250
1350
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("fieldset", {
1251
1351
  style: {
1252
- ...stack,
1253
- border,
1352
+ ...stack$1,
1353
+ border: border$1,
1254
1354
  borderRadius: "var(--dsw-radius-lg, 12px)",
1255
1355
  padding: 14,
1256
1356
  margin: 0
@@ -1270,7 +1370,7 @@ function ProviderAccountManager({ provider, name, rpc, t, onClose }) {
1270
1370
  }),
1271
1371
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1272
1372
  style: {
1273
- ...stack,
1373
+ ...stack$1,
1274
1374
  gap: 6
1275
1375
  },
1276
1376
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -1322,12 +1422,13 @@ function ProviderAccountManager({ provider, name, rpc, t, onClose }) {
1322
1422
  },
1323
1423
  children: [
1324
1424
  t("accountsModels"),
1325
- " · ",
1425
+ " ·",
1426
+ " ",
1326
1427
  preferences.poolModels === void 0 ? t("accountsModelsAll") : t("accountsModelsCount", { count: preferences.poolModels.length })
1327
1428
  ]
1328
1429
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1329
1430
  style: {
1330
- ...stack,
1431
+ ...stack$1,
1331
1432
  marginTop: 12
1332
1433
  },
1333
1434
  children: [
@@ -1350,7 +1451,7 @@ function ProviderAccountManager({ provider, name, rpc, t, onClose }) {
1350
1451
  t("accountsModelsAutomatic")
1351
1452
  ] }),
1352
1453
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1353
- style: actions,
1454
+ style: actions$1,
1354
1455
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1355
1456
  type: "button",
1356
1457
  onClick: () => edit(account.key, {
@@ -1369,7 +1470,7 @@ function ProviderAccountManager({ provider, name, rpc, t, onClose }) {
1369
1470
  }),
1370
1471
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1371
1472
  style: {
1372
- ...stack,
1473
+ ...stack$1,
1373
1474
  gap: 8,
1374
1475
  maxHeight: 240,
1375
1476
  overflowY: "auto"
@@ -1414,11 +1515,57 @@ function ProviderAccountManager({ provider, name, rpc, t, onClose }) {
1414
1515
  disabled: saving,
1415
1516
  onDirtyChange: setModelsDirty
1416
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
+ }),
1417
1564
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("footer", {
1418
1565
  style: {
1419
- ...stack,
1566
+ ...stack$1,
1420
1567
  gap: 8,
1421
- borderTop: border,
1568
+ borderTop: border$1,
1422
1569
  padding: "14px 0 8px",
1423
1570
  position: "sticky",
1424
1571
  bottom: 0,
@@ -1429,7 +1576,7 @@ function ProviderAccountManager({ provider, name, rpc, t, onClose }) {
1429
1576
  children: saveError
1430
1577
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1431
1578
  style: {
1432
- ...actions,
1579
+ ...actions$1,
1433
1580
  justifyContent: "flex-end"
1434
1581
  },
1435
1582
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
@@ -1440,7 +1587,7 @@ function ProviderAccountManager({ provider, name, rpc, t, onClose }) {
1440
1587
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1441
1588
  type: "button",
1442
1589
  className: "dsh-subscription-primary",
1443
- disabled: loading || saving || !Object.keys(changes).length && !modelsDirty,
1590
+ disabled: loading || saving || !Object.keys(changes).length && !modelsDirty && !ttlDirty,
1444
1591
  onClick: () => {
1445
1592
  save();
1446
1593
  },
@@ -1704,6 +1851,19 @@ function usageBarColor(used, elapsed, preset = "standard", fresh = true) {
1704
1851
  const state = usageColorState(used, elapsed, preset, fresh);
1705
1852
  return state === "red" ? "var(--dsw-alias-state-error-primary)" : state === "yellow" ? "var(--dsw-alias-state-warn-label)" : "var(--dsw-alias-state-success-primary)";
1706
1853
  }
1854
+ /**
1855
+ * Marker position from the window's display-only assumed start
1856
+ * (`displayStartsAt`): the reading and its age are irrelevant, exactly
1857
+ * like the verified marker. Returns undefined when the window carries no
1858
+ * assumed start or its reset has passed.
1859
+ */
1860
+ function displayStartPercent(window$1, now = Date.now()) {
1861
+ const end = window$1.resetsAt;
1862
+ const start = window$1.displayStartsAt;
1863
+ if (end === void 0 || !Number.isFinite(end) || end <= now) return void 0;
1864
+ if (start === void 0 || !Number.isFinite(start) || start >= end || start > now) return void 0;
1865
+ return Math.min(100, Math.max(0, (now - start) / (end - start) * 100));
1866
+ }
1707
1867
 
1708
1868
  //#endregion
1709
1869
  //#region src/client/usage-color-preferences.ts
@@ -1765,6 +1925,7 @@ function UsageMeter({ window: quota, t, style, observedAt, stale }) {
1765
1925
  stale
1766
1926
  }, current);
1767
1927
  const elapsed = elapsedPercent(quota, current);
1928
+ const marker = elapsed ?? displayStartPercent(quota, current);
1768
1929
  const pace = fresh ? elapsed : void 0;
1769
1930
  const state = usageColorState(quota.usedPercent, pace, preset, fresh);
1770
1931
  const lead = pace === void 0 ? 0 : Math.round(used - pace);
@@ -1774,7 +1935,7 @@ function UsageMeter({ window: quota, t, style, observedAt, stale }) {
1774
1935
  remaining: Math.round(100 - used)
1775
1936
  }) : t("usageMeterInvalid"),
1776
1937
  !valid ? "" : !fresh ? t("usageMeterStale") : state === "red" ? t("usageMeterNearLimit") : pace === void 0 && preset !== "remaining" ? t("usageMeterUnknownTime") : state === "yellow" ? t("usageMeterAhead") : t("usageMeterNoWarning"),
1777
- elapsed === void 0 ? "" : t("usageMeterPace", { elapsed: Math.round(elapsed) }),
1938
+ marker === void 0 ? "" : t("usageMeterPace", { elapsed: Math.round(marker) }),
1778
1939
  pace === void 0 ? "" : lead > 0 ? t("usageMeterPaceAhead", { points: lead }) : lead < 0 ? t("usageMeterPaceBehind", { points: -lead }) : t("usageMeterPaceEven"),
1779
1940
  quota.resetsAt !== void 0 && quota.resetsAt > current ? t("usageMeterReset", { duration: resetCountdownParts(quota.resetsAt - current).map((part) => t(UNIT_KEYS[part.unit][part.count === 1 ? 0 : 1], { count: part.count })).join(" ") }) : ""
1780
1941
  ].filter(Boolean).join(" · ");
@@ -1797,12 +1958,12 @@ function UsageMeter({ window: quota, t, style, observedAt, stale }) {
1797
1958
  borderRadius: "inherit",
1798
1959
  width: `${used}%`,
1799
1960
  background: usageBarColor(quota.usedPercent, pace, preset, fresh)
1800
- } }), elapsed !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1961
+ } }), marker !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1801
1962
  "aria-hidden": "true",
1802
1963
  "data-usage-time-marker": true,
1803
1964
  style: {
1804
1965
  position: "absolute",
1805
- left: `clamp(0px, calc(${elapsed}% - 1px), calc(100% - 2px))`,
1966
+ left: `clamp(0px, calc(${marker}% - 1px), calc(100% - 2px))`,
1806
1967
  top: 0,
1807
1968
  bottom: 0,
1808
1969
  width: 2,
@@ -1981,140 +2142,1206 @@ function ExternalUsageCards({ rpc, t }) {
1981
2142
  style: subscriptionCardStyles.error,
1982
2143
  children: t("usageError", { message: errors[id] })
1983
2144
  }),
1984
- snapshot?.windows?.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1985
- style: subscriptionCardStyles.status,
1986
- children: t("usageEmpty")
2145
+ snapshot !== void 0 && snapshot.supported === false && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2146
+ style: subscriptionCardStyles.status,
2147
+ children: t("usageUnsupported")
2148
+ }),
2149
+ snapshot?.windows?.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2150
+ style: subscriptionCardStyles.status,
2151
+ children: t("usageEmpty")
2152
+ }),
2153
+ snapshot?.windows?.map((window$1, index) => {
2154
+ const percent = displayUsedPercent(window$1.usedPercent);
2155
+ const label = externalUsageWindowLabel(t, window$1);
2156
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2157
+ style: subscriptionCardStyles.usageRow,
2158
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2159
+ style: subscriptionCardStyles.usageMeta,
2160
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [percent === void 0 ? t("usageMeterInvalid") : `${percent}%`, window$1.resetsAt === void 0 ? "" : ` · ${t("usageResets", { date: new Date(window$1.resetsAt).toLocaleString() })}`] })]
2161
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageMeter, {
2162
+ window: window$1,
2163
+ t,
2164
+ observedAt: observedAt[id],
2165
+ stale: errors[id] !== void 0 || snapshot?.stale === true
2166
+ })]
2167
+ }, index);
2168
+ })
2169
+ ]
2170
+ })]
2171
+ }),
2172
+ !configured && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2173
+ style: subscriptionCardStyles.actions,
2174
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2175
+ type: "button",
2176
+ style: subscriptionCardStyles.button,
2177
+ onClick: () => {
2178
+ refreshStatus();
2179
+ },
2180
+ children: t("usageRefresh")
2181
+ })
2182
+ })
2183
+ ]
2184
+ }, id);
2185
+ }) });
2186
+ }
2187
+
2188
+ //#endregion
2189
+ //#region src/client/CursorCard.tsx
2190
+ function message$2(error) {
2191
+ return error instanceof Error ? error.message : String(error);
2192
+ }
2193
+ /** Cursor's single connected account, rendered with the same card and Manage dialog as the other providers. */
2194
+ function CursorCard({ rpc, t }) {
2195
+ const [status, setStatus] = (0, react.useState)();
2196
+ const [usage, setUsage] = (0, react.useState)();
2197
+ const [observedAt, setObservedAt] = (0, react.useState)();
2198
+ const [usageStale, setUsageStale] = (0, react.useState)(false);
2199
+ const [authorizeUrl, setAuthorizeUrl] = (0, react.useState)();
2200
+ const [error, setError] = (0, react.useState)();
2201
+ const [loading, setLoading] = (0, react.useState)(false);
2202
+ const [manage, setManage] = (0, react.useState)(false);
2203
+ const authenticated = (0, react.useRef)();
2204
+ const refreshStatus = (0, react.useCallback)(async () => {
2205
+ try {
2206
+ const value = await callSubscriptionsAuth(rpc, "cursorStatus", {});
2207
+ setStatus(value);
2208
+ if (authenticated.current !== value.authenticated) {
2209
+ authenticated.current = value.authenticated;
2210
+ window.dispatchEvent(new Event(USAGE_BADGE_REFRESH_EVENT));
2211
+ }
2212
+ if (value.authenticated) setAuthorizeUrl(void 0);
2213
+ if (value.error) setError(value.error);
2214
+ } catch (failure) {
2215
+ setError(message$2(failure));
2216
+ }
2217
+ }, [rpc]);
2218
+ const refreshUsage = (0, react.useCallback)(async () => {
2219
+ setLoading(true);
2220
+ try {
2221
+ const value = await callSubscriptionsAuth(rpc, "cursorUsage", {});
2222
+ setUsage(value);
2223
+ setObservedAt(value.observedAt ?? Date.now());
2224
+ setUsageStale(value.stale === true);
2225
+ setError(void 0);
2226
+ window.dispatchEvent(new Event(USAGE_BADGE_REFRESH_EVENT));
2227
+ } catch (failure) {
2228
+ setError(message$2(failure));
2229
+ setUsageStale(true);
2230
+ } finally {
2231
+ setLoading(false);
2232
+ }
2233
+ }, [rpc]);
2234
+ (0, react.useEffect)(() => {
2235
+ refreshStatus();
2236
+ }, [refreshStatus]);
2237
+ (0, react.useEffect)(() => {
2238
+ if (status?.busy !== true) return;
2239
+ const timer = setInterval(() => {
2240
+ refreshStatus();
2241
+ }, 2e3);
2242
+ return () => clearInterval(timer);
2243
+ }, [status?.busy, refreshStatus]);
2244
+ (0, react.useEffect)(() => {
2245
+ if (status?.authenticated === true) refreshUsage();
2246
+ }, [status?.authenticated, refreshUsage]);
2247
+ const login = async () => {
2248
+ setError(void 0);
2249
+ try {
2250
+ const value = await callSubscriptionsAuth(rpc, "cursorLogin", {});
2251
+ setAuthorizeUrl(value.authorizeUrl);
2252
+ window.open(value.authorizeUrl, "_blank", "noopener");
2253
+ await refreshStatus();
2254
+ } catch (failure) {
2255
+ setError(message$2(failure));
2256
+ }
2257
+ };
2258
+ const cancel = async () => {
2259
+ try {
2260
+ await callSubscriptionsAuth(rpc, "cursorCancel", {});
2261
+ setAuthorizeUrl(void 0);
2262
+ await refreshStatus();
2263
+ } catch (failure) {
2264
+ setError(message$2(failure));
2265
+ }
2266
+ };
2267
+ const logout = async () => {
2268
+ if (!window.confirm(t("logoutAccountConfirm", {
2269
+ account: "Cursor",
2270
+ provider: "Cursor"
2271
+ }))) return;
2272
+ try {
2273
+ await callSubscriptionsAuth(rpc, "cursorLogout", {});
2274
+ setUsage(void 0);
2275
+ setObservedAt(void 0);
2276
+ setUsageStale(false);
2277
+ await refreshStatus();
2278
+ } catch (failure) {
2279
+ setError(message$2(failure));
2280
+ }
2281
+ };
2282
+ const connected = status?.authenticated === true;
2283
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2284
+ style: subscriptionCardStyles.card,
2285
+ children: [
2286
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2287
+ style: subscriptionCardStyles.header,
2288
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: {
2289
+ ...subscriptionCardStyles.dot,
2290
+ background: status?.busy ? "var(--dsw-alias-state-warn-label)" : connected ? "var(--dsw-alias-state-success-primary)" : "var(--dsw-alias-label-dimmed)"
2291
+ } }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2292
+ style: subscriptionCardStyles.name,
2293
+ children: t("cursorTitle")
2294
+ })]
2295
+ }),
2296
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2297
+ style: subscriptionCardStyles.status,
2298
+ children: status === void 0 ? t("checking") : status.busy ? t("loginInProgress") : connected ? t("loggedInCount", { count: 1 }) : t("notLoggedIn")
2299
+ }),
2300
+ error !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2301
+ style: subscriptionCardStyles.error,
2302
+ children: error
2303
+ }),
2304
+ connected && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2305
+ style: subscriptionCardStyles.account,
2306
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2307
+ style: subscriptionCardStyles.accountHeader,
2308
+ children: [
2309
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2310
+ style: subscriptionCardStyles.defaultStar,
2311
+ title: t("defaultBadge"),
2312
+ children: "★"
2313
+ }),
2314
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2315
+ style: subscriptionCardStyles.accountName,
2316
+ children: "Cursor"
2317
+ }),
2318
+ status.expiresAt !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2319
+ style: subscriptionCardStyles.status,
2320
+ children: t("accountExpires", { date: new Date(status.expiresAt).toLocaleString() })
2321
+ }),
2322
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2323
+ type: "button",
2324
+ style: {
2325
+ ...subscriptionCardStyles.button,
2326
+ marginLeft: "auto",
2327
+ flexShrink: 0
2328
+ },
2329
+ onClick: () => {
2330
+ logout();
2331
+ },
2332
+ children: t("logout")
2333
+ })
2334
+ ]
2335
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2336
+ style: subscriptionCardStyles.usage,
2337
+ children: [
2338
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2339
+ style: subscriptionCardStyles.usageHeader,
2340
+ children: [
2341
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2342
+ style: subscriptionCardStyles.usageTitle,
2343
+ children: t("usageTitle")
2344
+ }),
2345
+ usage?.plan !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2346
+ style: subscriptionCardStyles.status,
2347
+ children: t("usagePlan", { plan: usage.plan })
2348
+ }),
2349
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2350
+ type: "button",
2351
+ style: subscriptionCardStyles.usageRefresh,
2352
+ disabled: loading,
2353
+ onClick: () => {
2354
+ refreshUsage();
2355
+ },
2356
+ children: t("usageRefresh")
2357
+ })
2358
+ ]
2359
+ }),
2360
+ loading && usage === void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2361
+ style: subscriptionCardStyles.status,
2362
+ children: t("usageLoading")
2363
+ }),
2364
+ usage?.windows?.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2365
+ style: subscriptionCardStyles.status,
2366
+ children: t("usageEmpty")
2367
+ }),
2368
+ usage?.windows?.map((window$1, index) => {
2369
+ const percent = displayUsedPercent(window$1.usedPercent);
2370
+ const label = window$1.scope === "Included" ? t("cursorIncluded") : window$1.scope === "Cursor Models" ? t("cursorModels") : window$1.scope === "Other Models" ? t("cursorOtherModels") : window$1.scope === "Included requests" ? t("cursorIncludedRequests") : window$1.scope ?? t("usageWindow");
2371
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2372
+ style: subscriptionCardStyles.usageRow,
2373
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2374
+ style: subscriptionCardStyles.usageMeta,
2375
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [percent === void 0 ? t("usageMeterInvalid") : `${percent}%`, window$1.resetsAt === void 0 ? "" : ` · ${t("usageResets", { date: new Date(window$1.resetsAt).toLocaleString() })}`] })]
2376
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageMeter, {
2377
+ window: window$1,
2378
+ t,
2379
+ observedAt,
2380
+ stale: usageStale
2381
+ })]
2382
+ }, index);
2383
+ })
2384
+ ]
2385
+ })]
2386
+ }),
2387
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2388
+ style: subscriptionCardStyles.actions,
2389
+ children: [
2390
+ status?.authenticated === false && !status.busy && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2391
+ type: "button",
2392
+ style: subscriptionCardStyles.button,
2393
+ onClick: () => {
2394
+ login();
2395
+ },
2396
+ children: t("login")
2397
+ }),
2398
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2399
+ type: "button",
2400
+ style: subscriptionCardStyles.button,
2401
+ "aria-haspopup": "dialog",
2402
+ onClick: () => setManage(true),
2403
+ children: t("accountsManage")
2404
+ }),
2405
+ status?.busy === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2406
+ type: "button",
2407
+ style: subscriptionCardStyles.button,
2408
+ onClick: () => {
2409
+ cancel();
2410
+ },
2411
+ children: t("cancel")
2412
+ })
2413
+ ]
2414
+ }),
2415
+ status?.busy === true && authorizeUrl !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
2416
+ href: authorizeUrl,
2417
+ target: "_blank",
2418
+ rel: "noopener noreferrer",
2419
+ children: t("cursorOpenLogin")
2420
+ }),
2421
+ manage && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProviderAccountManager, {
2422
+ provider: "cursor-subscription",
2423
+ name: "Cursor",
2424
+ rpc,
2425
+ t,
2426
+ onClose: () => setManage(false)
2427
+ })
2428
+ ]
2429
+ });
2430
+ }
2431
+
2432
+ //#endregion
2433
+ //#region src/client/ollama-draft.ts
2434
+ /**
2435
+ * Append the checked candidates to the draft's model list. Existing rows are
2436
+ * never replaced or reordered, ids already present are skipped, and adopted
2437
+ * rows keep the candidate order. Optional fields are set only when the
2438
+ * candidate advertises a value, because `exactOptionalPropertyTypes` (and the
2439
+ * store) must not see an explicit `undefined`.
2440
+ * @param current - the draft list before adoption.
2441
+ * @param candidates - the cloud list as fetched, in display order.
2442
+ * @param selectedIds - ids the user checked.
2443
+ * @returns a new list; the input array is not mutated.
2444
+ */
2445
+ function adoptCandidates(current, candidates, selectedIds) {
2446
+ const models = [...current ?? []];
2447
+ const present = new Set(models.map((model) => model.id));
2448
+ const selected = new Set(selectedIds);
2449
+ for (const candidate of candidates) {
2450
+ if (!selected.has(candidate.id) || present.has(candidate.id)) continue;
2451
+ present.add(candidate.id);
2452
+ models.push({
2453
+ id: candidate.id,
2454
+ name: candidate.name,
2455
+ ...candidate.contextWindow === void 0 ? {} : { contextWindow: candidate.contextWindow },
2456
+ ...candidate.vision ? { vision: true } : {},
2457
+ ...candidate.thinking ? { thinking: true } : {},
2458
+ ...candidate.efforts.length ? { efforts: [...candidate.efforts] } : {}
2459
+ });
2460
+ }
2461
+ return models;
2462
+ }
2463
+ /**
2464
+ * Swap one model with its neighbour. An id that is absent or a move past
2465
+ * either end returns the same order.
2466
+ * @param models - the current list.
2467
+ * @param id - the row to move.
2468
+ * @param delta - `-1` moves up, `1` moves down.
2469
+ * @returns a new list; the input array is not mutated.
2470
+ */
2471
+ function moveModel(models, id, delta) {
2472
+ const next = [...models];
2473
+ const index = next.findIndex((model) => model.id === id);
2474
+ const target = index + delta;
2475
+ if (index === -1 || target < 0 || target >= next.length) return next;
2476
+ const moved = next[index];
2477
+ next[index] = next[target];
2478
+ next[target] = moved;
2479
+ return next;
2480
+ }
2481
+ /**
2482
+ * Drop one model from the list.
2483
+ * @param models - the current list.
2484
+ * @param id - the row to drop; an unknown id leaves the list unchanged.
2485
+ * @returns a new list; the input array is not mutated.
2486
+ */
2487
+ function removeModel(models, id) {
2488
+ return models.filter((model) => model.id !== id);
2489
+ }
2490
+ /**
2491
+ * Short context-window label for a model row.
2492
+ * @param contextWindow - the advertised window in tokens.
2493
+ * @returns `'1M'`, `'262K'`, the bare number below 1000, or `undefined` when unknown.
2494
+ */
2495
+ function contextLabel(contextWindow) {
2496
+ if (contextWindow === void 0) return void 0;
2497
+ if (contextWindow < 1e3) return String(contextWindow);
2498
+ if (contextWindow >= 1e6) {
2499
+ const millions = contextWindow / 1e6;
2500
+ return `${Number.isInteger(millions) ? millions : Number(millions.toFixed(1))}M`;
2501
+ }
2502
+ return `${Math.round(contextWindow / 1e3)}K`;
2503
+ }
2504
+ /**
2505
+ * Rename one model row. An empty display name deletes the field so the
2506
+ * validator keeps accepting the draft; a blank id leaves the list unchanged.
2507
+ * @param models - the current list.
2508
+ * @param id - the row to rename; an unknown id leaves the list unchanged.
2509
+ * @param name - the new display name; whitespace trims before storing.
2510
+ * @returns a new list; the input array is not mutated.
2511
+ */
2512
+ function renameModel(models, id, name) {
2513
+ return models.map((model) => {
2514
+ if (model.id !== id) return model;
2515
+ const next = { ...model };
2516
+ if (name === "") delete next.name;
2517
+ else next.name = name;
2518
+ return next;
2519
+ });
2520
+ }
2521
+ /**
2522
+ * Set or clear one model row's default reasoning effort. An empty choice
2523
+ * deletes the field so the resolver falls back to the vendor or family
2524
+ * default.
2525
+ * @param models - the current list.
2526
+ * @param id - the row to edit; an unknown id leaves the list unchanged.
2527
+ * @param effort - the chosen level, or `''` to clear.
2528
+ * @returns a new list; the input array is not mutated.
2529
+ */
2530
+ function setModelEffort(models, id, effort) {
2531
+ return models.map((model) => {
2532
+ if (model.id !== id) return model;
2533
+ if (effort === "") {
2534
+ const next = { ...model };
2535
+ delete next.defaultEffort;
2536
+ return next;
2537
+ }
2538
+ return {
2539
+ ...model,
2540
+ defaultEffort: effort
2541
+ };
2542
+ });
2543
+ }
2544
+ /**
2545
+ * Set or clear one model row's local context-window override. `undefined`
2546
+ * deletes the field so the model follows the endpoint again; the runtime
2547
+ * caps a stored value at the advertised window.
2548
+ * @param models - the current list.
2549
+ * @param id - the row to edit; an unknown id leaves the list unchanged.
2550
+ * @param tokens - a positive safe integer, or `undefined` to clear.
2551
+ * @returns a new list; the input array is not mutated.
2552
+ */
2553
+ function setModelContext(models, id, tokens) {
2554
+ return models.map((model) => {
2555
+ if (model.id !== id) return model;
2556
+ const next = { ...model };
2557
+ if (tokens === void 0) delete next.contextOverride;
2558
+ else next.contextOverride = tokens;
2559
+ return next;
2560
+ });
2561
+ }
2562
+ /**
2563
+ * Parse the context input's text. Only a positive safe integer is a value;
2564
+ * an empty string clears the override.
2565
+ * @param text - what the user typed.
2566
+ * @returns the number, `undefined` for blank, or `null` for text to reject.
2567
+ */
2568
+ function parseContextInput(text) {
2569
+ const trimmed = text.trim();
2570
+ if (trimmed === "") return void 0;
2571
+ if (!/^\d+$/.test(trimmed)) return null;
2572
+ const value = Number(trimmed);
2573
+ return Number.isSafeInteger(value) && value > 0 ? value : null;
2574
+ }
2575
+ /**
2576
+ * Untick one model: drop it from the list and keep its full row (display
2577
+ * name, default effort, context, ...) in the stash so ticking it again is
2578
+ * lossless, even when the cloud listing no longer carries the id.
2579
+ * @param models - the current list.
2580
+ * @param stash - the stash before this untick.
2581
+ * @param id - the row to untick; an absent id changes nothing.
2582
+ * @returns the new list and stash; neither input is mutated.
2583
+ */
2584
+ function untickModel(models, stash, id) {
2585
+ const row = models.find((model) => model.id === id);
2586
+ if (row === void 0) return {
2587
+ models: [...models],
2588
+ stash
2589
+ };
2590
+ return {
2591
+ models: removeModel(models, id),
2592
+ stash: new Map(stash).set(id, row)
2593
+ };
2594
+ }
2595
+ /**
2596
+ * Tick one model: restore its stashed row when this dialog unticked it,
2597
+ * otherwise adopt it from the fetched candidates (which never copies the
2598
+ * vendor default effort). A model already in the list, or unknown to both
2599
+ * sources, leaves the list unchanged.
2600
+ * @param models - the current list.
2601
+ * @param stash - rows unticked earlier in this dialog.
2602
+ * @param candidates - the cloud list as fetched.
2603
+ * @param id - the row to tick.
2604
+ * @returns a new list and the stash without the restored row.
2605
+ */
2606
+ function tickModel(models, stash, candidates, id) {
2607
+ if (models.some((model) => model.id === id)) return {
2608
+ models: [...models],
2609
+ stash
2610
+ };
2611
+ const stashed = stash.get(id);
2612
+ if (stashed === void 0) return {
2613
+ models: adoptCandidates(models, candidates, [id]),
2614
+ stash
2615
+ };
2616
+ const next = new Map(stash);
2617
+ next.delete(id);
2618
+ return {
2619
+ models: [...models, stashed],
2620
+ stash: next
2621
+ };
2622
+ }
2623
+ /**
2624
+ * Tick every listed id in order. Rows already in the list stay where they
2625
+ * are; restored and adopted rows append in the given order.
2626
+ * @param models - the current list.
2627
+ * @param stash - rows unticked earlier in this dialog.
2628
+ * @param candidates - the cloud list as fetched.
2629
+ * @param ids - the ids to tick, typically the filtered view.
2630
+ * @returns the new list and stash; neither input is mutated.
2631
+ */
2632
+ function tickModels(models, stash, candidates, ids) {
2633
+ let state = {
2634
+ models: [...models],
2635
+ stash
2636
+ };
2637
+ for (const id of ids) state = tickModel(state.models, state.stash, candidates, id);
2638
+ return state;
2639
+ }
2640
+ /**
2641
+ * Untick every listed id, stashing each removed row. Ids outside the list,
2642
+ * such as filtered-out rows, are untouched.
2643
+ * @param models - the current list.
2644
+ * @param stash - the stash before this untick.
2645
+ * @param ids - the ids to untick, typically the filtered view.
2646
+ * @returns the new list and stash; neither input is mutated.
2647
+ */
2648
+ function untickModels(models, stash, ids) {
2649
+ let state = {
2650
+ models: [...models],
2651
+ stash
2652
+ };
2653
+ for (const id of ids) state = untickModel(state.models, state.stash, id);
2654
+ return state;
2655
+ }
2656
+ /**
2657
+ * The effort choices for one ticked row. The row's own `efforts` decide the
2658
+ * ids because the store rejects a default outside them, so the selector must
2659
+ * never offer anything else. A snapshot of the endpoint (`labels`) may only
2660
+ * supply friendlier names for the same ids. A saved default the list no
2661
+ * longer contains is kept visible and marked unavailable instead of
2662
+ * silently reading as "follow the provider".
2663
+ * @param row - the draft row.
2664
+ * @param fetched - efforts from this dialog's latest fetch, when it returned the row.
2665
+ * @param labels - the opened dialog's snapshot of named efforts for the id.
2666
+ * @returns the options in picker order.
2667
+ */
2668
+ function effortOptions(row, fetched, labels) {
2669
+ const ids = row.efforts ?? fetched ?? labels?.map((entry) => entry.id) ?? [];
2670
+ const options = ids.map((id) => ({
2671
+ id,
2672
+ name: labels?.find((entry) => entry.id === id)?.name ?? id
2673
+ }));
2674
+ if (row.defaultEffort !== void 0 && !ids.includes(row.defaultEffort)) options.push({
2675
+ id: row.defaultEffort,
2676
+ name: row.defaultEffort,
2677
+ unavailable: true
2678
+ });
2679
+ return options;
2680
+ }
2681
+
2682
+ //#endregion
2683
+ //#region src/client/OllamaSettingsManager.tsx
2684
+ const border = "0.5px solid var(--dsw-alias-border-l2)";
2685
+ const stack = {
2686
+ display: "grid",
2687
+ gap: 12,
2688
+ minWidth: 0
2689
+ };
2690
+ const actions = {
2691
+ display: "flex",
2692
+ flexWrap: "wrap",
2693
+ gap: 8,
2694
+ alignItems: "center"
2695
+ };
2696
+ function message$1(error) {
2697
+ return error instanceof Error ? error.message : String(error);
2698
+ }
2699
+ /** `vision · thinking · context 262K`, dropping whatever the model does not advertise. */
2700
+ function capabilities(t, model) {
2701
+ const context = contextLabel(model.contextWindow);
2702
+ return [
2703
+ model.vision === true ? t("ollamaCapVision") : false,
2704
+ model.thinking === true ? t("ollamaCapThinking") : false,
2705
+ context === void 0 ? false : t("ollamaContext", { tokens: context })
2706
+ ].filter(Boolean).join(" · ");
2707
+ }
2708
+ /**
2709
+ * Ollama Cloud settings dialog. It owns one draft (model list, web toggle,
2710
+ * base URL and credential reference) that Save writes in a single
2711
+ * `ollamaSetSettings` call, so a failure keeps every edit. Fetching the cloud
2712
+ * catalog is a read-only side trip, and a key typed for the fetch never
2713
+ * leaves this component. The model list follows the shared model editor: a
2714
+ * filter, select all/none over the filtered rows, and one checkbox per
2715
+ * model, where ticking edits the draft and enables Save. Ollama adds what the
2716
+ * shared editor lacks: a display name, ordering, and a local context window.
2717
+ * Unticking keeps the row's full settings in a dialog-local stash, so ticking
2718
+ * it again restores them even when the cloud listing no longer returns it.
2719
+ */
2720
+ function OllamaSettingsManager({ rpc, t, onClose, onSaved }) {
2721
+ const dialog = (0, react.useRef)(null);
2722
+ const title = (0, react.useId)();
2723
+ const description = (0, react.useId)();
2724
+ const [view, setView] = (0, react.useState)();
2725
+ const [draft, setDraft] = (0, react.useState)({});
2726
+ const [dirty, setDirty] = (0, react.useState)(false);
2727
+ const [loading, setLoading] = (0, react.useState)(true);
2728
+ const [saving, setSaving] = (0, react.useState)(false);
2729
+ const [error, setError] = (0, react.useState)("");
2730
+ const [saveError, setSaveError] = (0, react.useState)("");
2731
+ const [attempt, setAttempt] = (0, react.useState)(0);
2732
+ const [apiKey, setApiKey] = (0, react.useState)("");
2733
+ const [candidates, setCandidates] = (0, react.useState)();
2734
+ const [fetching, setFetching] = (0, react.useState)(false);
2735
+ const [fetchError, setFetchError] = (0, react.useState)("");
2736
+ const [query, setQuery] = (0, react.useState)("");
2737
+ const [expanded, setExpanded] = (0, react.useState)(/* @__PURE__ */ new Set());
2738
+ const [stash, setStash] = (0, react.useState)(/* @__PURE__ */ new Map());
2739
+ const [contextText, setContextText] = (0, react.useState)({});
2740
+ const alive = (0, react.useRef)(true);
2741
+ const saveLock = (0, react.useRef)(false);
2742
+ const fetchLock = (0, react.useRef)(false);
2743
+ (0, react.useEffect)(() => {
2744
+ alive.current = true;
2745
+ const element = dialog.current;
2746
+ const previous = document.activeElement;
2747
+ element.showModal();
2748
+ return () => {
2749
+ alive.current = false;
2750
+ element.close();
2751
+ if (previous instanceof HTMLElement && previous.isConnected) previous.focus();
2752
+ };
2753
+ }, []);
2754
+ (0, react.useEffect)(() => {
2755
+ let current = true;
2756
+ setLoading(true);
2757
+ setError("");
2758
+ callSubscriptionsAuth(rpc, "ollamaSettings", {}).then((data) => {
2759
+ if (!current) return;
2760
+ setView(data);
2761
+ setDraft({ ...data.settings });
2762
+ setStash(/* @__PURE__ */ new Map());
2763
+ setContextText({});
2764
+ setDirty(false);
2765
+ }).catch((failure) => {
2766
+ if (current) setError(t("ollamaModelsLoadFailed", { message: message$1(failure) }));
2767
+ }).finally(() => {
2768
+ if (current) setLoading(false);
2769
+ });
2770
+ return () => {
2771
+ current = false;
2772
+ };
2773
+ }, [rpc, attempt]);
2774
+ function edit(next) {
2775
+ setDraft(next);
2776
+ setDirty(true);
2777
+ }
2778
+ /** Fetch the cloud catalog; a failure keeps the previous list and the draft. */
2779
+ async function fetchCandidates() {
2780
+ if (fetchLock.current) return;
2781
+ fetchLock.current = true;
2782
+ setFetching(true);
2783
+ setFetchError("");
2784
+ try {
2785
+ const value = await callSubscriptionsAuth(rpc, "ollamaModels", {
2786
+ ...apiKey ? { apiKey } : {},
2787
+ ...draft.baseURL ? { baseURL: draft.baseURL } : {}
2788
+ });
2789
+ if (!alive.current) return;
2790
+ setCandidates(value.models);
2791
+ } catch (failure) {
2792
+ if (alive.current) setFetchError(t("ollamaFetchFailed", { message: message$1(failure) }));
2793
+ } finally {
2794
+ fetchLock.current = false;
2795
+ if (alive.current) setFetching(false);
2796
+ }
2797
+ }
2798
+ /** Save the whole draft; a failure keeps the dialog open with every edit. */
2799
+ async function save() {
2800
+ if (saveLock.current) return;
2801
+ saveLock.current = true;
2802
+ setSaving(true);
2803
+ setSaveError("");
2804
+ try {
2805
+ await callSubscriptionsAuth(rpc, "ollamaSetSettings", { settings: draft });
2806
+ if (alive.current) {
2807
+ onSaved();
2808
+ onClose();
2809
+ }
2810
+ } catch (failure) {
2811
+ if (alive.current) setSaveError(t("ollamaModelsSaveFailed", { message: message$1(failure) }));
2812
+ } finally {
2813
+ saveLock.current = false;
2814
+ if (alive.current) setSaving(false);
2815
+ }
2816
+ }
2817
+ /** Drop every stored override, then reload the draft from the deployment values. */
2818
+ async function resetDefaults() {
2819
+ if (saveLock.current) return;
2820
+ if (!window.confirm(t("ollamaResetConfirm"))) return;
2821
+ saveLock.current = true;
2822
+ setSaving(true);
2823
+ setSaveError("");
2824
+ try {
2825
+ await callSubscriptionsAuth(rpc, "ollamaSetSettings", { settings: {} });
2826
+ if (alive.current) {
2827
+ setAttempt((value) => value + 1);
2828
+ onSaved();
2829
+ }
2830
+ } catch (failure) {
2831
+ if (alive.current) setSaveError(t("ollamaModelsSaveFailed", { message: message$1(failure) }));
2832
+ } finally {
2833
+ saveLock.current = false;
2834
+ if (alive.current) setSaving(false);
2835
+ }
2836
+ }
2837
+ function editOverride(field, value) {
2838
+ const next = { ...draft };
2839
+ if (value.trim()) next[field] = value;
2840
+ else delete next[field];
2841
+ edit(next);
2842
+ }
2843
+ /**
2844
+ * One list for both sources, like the shared editor: the ticked models in
2845
+ * draft order first (so Move up/down is visible), then the unticked ones
2846
+ * (stashed in this dialog, or fetched and not yet adopted). A kept model
2847
+ * the fetch did not return stays listed, ticked or not.
2848
+ */
2849
+ const fetchedById = new Map((candidates ?? []).map((candidate) => [candidate.id, candidate]));
2850
+ const present = new Set((draft.models ?? []).map((model) => model.id));
2851
+ const asRow = (model, ticked) => {
2852
+ const fetched = fetchedById.get(model.id);
2853
+ const contextWindow = fetched?.contextWindow ?? model.contextWindow;
2854
+ return {
2855
+ id: model.id,
2856
+ name: model.name ?? fetched?.name ?? model.id,
2857
+ ticked,
2858
+ vision: model.vision === true || fetched?.vision === true,
2859
+ thinking: model.thinking === true || fetched?.thinking === true,
2860
+ ...contextWindow === void 0 ? {} : { contextWindow },
2861
+ efforts: model.efforts ?? fetched?.efforts ?? []
2862
+ };
2863
+ };
2864
+ const shown = [
2865
+ ...(draft.models ?? []).map((model) => asRow(model, true)),
2866
+ ...[...stash.values()].filter((model) => !present.has(model.id)).map((model) => asRow(model, false)),
2867
+ ...(candidates ?? []).filter((candidate) => !present.has(candidate.id) && !stash.has(candidate.id)).map((candidate) => asRow({ id: candidate.id }, false))
2868
+ ].filter((row) => `${row.name} ${row.id}`.toLowerCase().includes(query.trim().toLowerCase()));
2869
+ /** Rename and default-effort edits ride the tested draft helpers. */
2870
+ function rename(id, name) {
2871
+ edit({
2872
+ ...draft,
2873
+ models: renameModel(draft.models ?? [], id, name)
2874
+ });
2875
+ }
2876
+ function setEffort(id, effort) {
2877
+ edit({
2878
+ ...draft,
2879
+ models: setModelEffort(draft.models ?? [], id, effort)
2880
+ });
2881
+ }
2882
+ /** Keep the typed text; only a valid number (or blank) reaches the draft. */
2883
+ function setContext(id, text) {
2884
+ setContextText((current) => ({
2885
+ ...current,
2886
+ [id]: text
2887
+ }));
2888
+ const tokens = parseContextInput(text);
2889
+ if (tokens !== null) edit({
2890
+ ...draft,
2891
+ models: setModelContext(draft.models ?? [], id, tokens)
2892
+ });
2893
+ }
2894
+ function applyTicks(next) {
2895
+ setStash(next.stash);
2896
+ edit({
2897
+ ...draft,
2898
+ models: next.models
2899
+ });
2900
+ }
2901
+ const invalidContext = Object.entries(contextText).filter(([id, text]) => present.has(id) && parseContextInput(text) === null).map(([id]) => id);
2902
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dialog", {
2903
+ className: "dsh-subscription-manager",
2904
+ ref: dialog,
2905
+ "aria-labelledby": title,
2906
+ "aria-describedby": description,
2907
+ onCancel: (event) => {
2908
+ if (saveLock.current) event.preventDefault();
2909
+ },
2910
+ onClose,
2911
+ style: {
2912
+ width: 620,
2913
+ maxWidth: "calc(100vw - 32px)",
2914
+ maxHeight: "calc(100dvh - 32px)",
2915
+ overflow: "auto"
2916
+ },
2917
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: providerSettingsCss }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2918
+ style: {
2919
+ ...stack,
2920
+ padding: 20
2921
+ },
2922
+ children: [
2923
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
2924
+ style: {
2925
+ ...actions,
2926
+ justifyContent: "space-between"
2927
+ },
2928
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
2929
+ id: title,
2930
+ style: { margin: 0 },
2931
+ children: t("ollamaManageTitle")
2932
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2933
+ type: "button",
2934
+ autoFocus: true,
2935
+ disabled: saving,
2936
+ onClick: onClose,
2937
+ children: t("imageClose")
2938
+ })]
2939
+ }),
2940
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2941
+ id: description,
2942
+ className: "dsh-subscription-hint",
2943
+ children: t("ollamaManageHint")
2944
+ }),
2945
+ view?.configOverridesModels === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2946
+ className: "dsh-subscription-hint",
2947
+ children: t("ollamaCatalogOverride")
2948
+ }),
2949
+ error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2950
+ role: "alert",
2951
+ children: error
2952
+ }),
2953
+ loading && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2954
+ role: "status",
2955
+ className: "dsh-subscription-hint",
2956
+ children: t("modelDefaultsLoading")
2957
+ }),
2958
+ !loading && !view && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2959
+ type: "button",
2960
+ onClick: () => setAttempt((value) => value + 1),
2961
+ children: t("modelDefaultsRetry")
2962
+ }),
2963
+ view && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("fieldset", {
2964
+ disabled: saving,
2965
+ style: {
2966
+ ...stack,
2967
+ border: 0,
2968
+ margin: 0,
2969
+ padding: 0,
2970
+ minWidth: 0
2971
+ },
2972
+ children: [
2973
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2974
+ style: {
2975
+ borderTop: border,
2976
+ paddingTop: 12,
2977
+ display: "grid",
2978
+ gap: 12
2979
+ },
2980
+ children: [
2981
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
2982
+ style: { margin: 0 },
2983
+ children: t("modelsEdit")
2984
+ }),
2985
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2986
+ style: actions,
2987
+ children: [
2988
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2989
+ type: "password",
2990
+ autoComplete: "off",
2991
+ style: { flex: "1 1 180px" },
2992
+ "aria-label": t("ollamaKeyPlaceholder"),
2993
+ placeholder: t("ollamaKeyPlaceholder"),
2994
+ value: apiKey,
2995
+ onChange: (event) => setApiKey(event.target.value)
2996
+ }),
2997
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2998
+ type: "button",
2999
+ className: "dsh-subscription-quiet",
3000
+ disabled: fetching,
3001
+ onClick: () => {
3002
+ fetchCandidates();
3003
+ },
3004
+ children: fetching ? t("ollamaFetching") : t("ollamaFetch")
3005
+ }),
3006
+ candidates !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3007
+ className: "dsh-subscription-hint",
3008
+ children: t("ollamaCandidatesCount", { count: candidates.length })
3009
+ })
3010
+ ]
3011
+ }),
3012
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3013
+ className: "dsh-subscription-hint",
3014
+ children: t("ollamaKeyHint")
3015
+ }),
3016
+ fetchError && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3017
+ role: "alert",
3018
+ children: fetchError
3019
+ }),
3020
+ candidates?.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3021
+ className: "dsh-subscription-hint",
3022
+ children: t("ollamaCandidatesEmpty")
3023
+ }),
3024
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3025
+ style: actions,
3026
+ children: [
3027
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3028
+ style: { flex: "1 1 180px" },
3029
+ value: query,
3030
+ onChange: (event) => setQuery(event.target.value),
3031
+ placeholder: t("modelDefaultsFilterPlaceholder"),
3032
+ "aria-label": t("modelDefaultsFilterPlaceholder")
3033
+ }),
3034
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3035
+ type: "button",
3036
+ onClick: () => applyTicks(tickModels(draft.models ?? [], stash, candidates ?? [], shown.map((row) => row.id))),
3037
+ children: t("modelsSelectAll")
3038
+ }),
3039
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3040
+ type: "button",
3041
+ onClick: () => applyTicks(untickModels(draft.models ?? [], stash, shown.map((row) => row.id))),
3042
+ children: t("modelsSelectNone")
3043
+ })
3044
+ ]
3045
+ }),
3046
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3047
+ style: {
3048
+ maxHeight: 360,
3049
+ overflowY: "auto",
3050
+ display: "grid",
3051
+ gap: 8
3052
+ },
3053
+ children: [shown.map((row) => {
3054
+ const stored = (draft.models ?? []).find((entry) => entry.id === row.id);
3055
+ const index = (draft.models ?? []).findIndex((entry) => entry.id === row.id);
3056
+ const open = expanded.has(row.id);
3057
+ const details = `${row.id}-details`;
3058
+ const efforts = stored === void 0 ? [] : effortOptions(stored, fetchedById.get(row.id)?.efforts, view.models.find((entry) => entry.id === row.id)?.efforts);
3059
+ const contextProblem = invalidContext.includes(row.id);
3060
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3061
+ style: {
3062
+ borderBottom: border,
3063
+ padding: "8px 2px",
3064
+ display: "grid",
3065
+ gap: 8
3066
+ },
3067
+ children: [
3068
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3069
+ style: { overflowWrap: "anywhere" },
3070
+ children: [
3071
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3072
+ type: "checkbox",
3073
+ checked: row.ticked,
3074
+ onChange: (event) => {
3075
+ applyTicks(event.target.checked ? tickModel(draft.models ?? [], stash, candidates ?? [], row.id) : untickModel(draft.models ?? [], stash, row.id));
3076
+ }
3077
+ }),
3078
+ " ",
3079
+ row.name,
3080
+ " ",
3081
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3082
+ style: subscriptionCardStyles.status,
3083
+ children: capabilities(t, row)
3084
+ })
3085
+ ]
3086
+ }),
3087
+ stored !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3088
+ style: actions,
3089
+ children: [
3090
+ row.thinking && efforts.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3091
+ style: actions,
3092
+ children: [t("modelDefaultsTitle"), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
3093
+ "aria-label": `${row.name} ${t("modelDefaultsTitle")}`,
3094
+ value: stored.defaultEffort ?? "",
3095
+ onChange: (event) => setEffort(row.id, event.target.value),
3096
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3097
+ value: "",
3098
+ children: t("modelDefaultsFollowProvider")
3099
+ }), efforts.map((effort) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3100
+ value: effort.id,
3101
+ disabled: effort.unavailable === true,
3102
+ children: effort.unavailable === true ? `${effort.name} (${t("modelsUnavailable")})` : effort.name
3103
+ }, effort.id))]
3104
+ })]
3105
+ }),
3106
+ row.thinking && efforts.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: t("ollamaEffortsUnknown") }),
3107
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3108
+ style: actions,
3109
+ children: [t("modelsContext"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3110
+ style: { width: 140 },
3111
+ inputMode: "numeric",
3112
+ "aria-invalid": contextProblem,
3113
+ value: contextText[row.id] ?? (stored.contextOverride === void 0 ? "" : String(stored.contextOverride)),
3114
+ "aria-label": `${row.name} ${t("modelsContext")}`,
3115
+ placeholder: row.contextWindow === void 0 ? "" : String(row.contextWindow),
3116
+ onChange: (event) => setContext(row.id, event.target.value)
3117
+ })]
3118
+ }),
3119
+ row.contextWindow !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: t("modelsContextBounds", {
3120
+ default: row.contextWindow,
3121
+ max: row.contextWindow
3122
+ }) }),
3123
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3124
+ type: "button",
3125
+ className: "dsh-subscription-quiet",
3126
+ "aria-expanded": open,
3127
+ "aria-controls": details,
3128
+ "aria-label": `${row.name} ${t("ollamaRowDetails")}`,
3129
+ onClick: () => {
3130
+ const next = new Set(expanded);
3131
+ if (open) next.delete(row.id);
3132
+ else next.add(row.id);
3133
+ setExpanded(next);
3134
+ },
3135
+ children: [
3136
+ t("ollamaRowDetails"),
3137
+ " ",
3138
+ open ? "▾" : "▸"
3139
+ ]
3140
+ })
3141
+ ]
3142
+ }),
3143
+ contextProblem && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3144
+ role: "alert",
3145
+ children: t("modelsContextInvalid", { model: row.name })
3146
+ }),
3147
+ stored !== void 0 && open && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3148
+ id: details,
3149
+ style: actions,
3150
+ children: [
3151
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3152
+ style: actions,
3153
+ children: [t("ollamaDisplayName"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3154
+ value: stored.name ?? "",
3155
+ placeholder: row.id,
3156
+ "aria-label": `${row.name} ${t("ollamaDisplayName")}`,
3157
+ onChange: (event) => rename(row.id, event.target.value),
3158
+ onBlur: (event) => rename(row.id, event.target.value.trim())
3159
+ })]
3160
+ }),
3161
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3162
+ type: "button",
3163
+ disabled: index === 0,
3164
+ "aria-label": `${row.name} ${t("ollamaMoveUp")}`,
3165
+ onClick: () => edit({
3166
+ ...draft,
3167
+ models: moveModel(draft.models ?? [], row.id, -1)
3168
+ }),
3169
+ children: t("ollamaMoveUp")
3170
+ }),
3171
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3172
+ type: "button",
3173
+ disabled: index === (draft.models ?? []).length - 1,
3174
+ "aria-label": `${row.name} ${t("ollamaMoveDown")}`,
3175
+ onClick: () => edit({
3176
+ ...draft,
3177
+ models: moveModel(draft.models ?? [], row.id, 1)
3178
+ }),
3179
+ children: t("ollamaMoveDown")
3180
+ })
3181
+ ]
3182
+ })
3183
+ ]
3184
+ }, row.id);
3185
+ }), shown.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3186
+ className: "dsh-subscription-hint",
3187
+ children: query.trim() ? t("modelDefaultsFilterEmpty", { query }) : t("ollamaModelsEmpty")
3188
+ })]
3189
+ }),
3190
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: t("modelsContextHint") })
3191
+ ]
3192
+ }),
3193
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3194
+ style: {
3195
+ ...stack,
3196
+ gap: 6
3197
+ },
3198
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3199
+ className: "dsh-subscription-label",
3200
+ children: t("ollamaBaseURL")
3201
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3202
+ value: draft.baseURL ?? "",
3203
+ placeholder: view.effective.baseURL,
3204
+ onChange: (event) => editOverride("baseURL", event.target.value)
3205
+ })]
3206
+ }),
3207
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3208
+ style: {
3209
+ ...stack,
3210
+ gap: 6
3211
+ },
3212
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3213
+ className: "dsh-subscription-label",
3214
+ children: t("ollamaApiKeyRef")
3215
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3216
+ value: draft.apiKeyRef ?? "",
3217
+ placeholder: view.effective.apiKeyRef,
3218
+ onChange: (event) => editOverride("apiKeyRef", event.target.value)
3219
+ })]
3220
+ }),
3221
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3222
+ className: "dsh-subscription-hint",
3223
+ children: t("ollamaConnectionHint")
3224
+ }),
3225
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [
3226
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3227
+ type: "checkbox",
3228
+ checked: (draft.web ?? view.effective.web) === true,
3229
+ onChange: (event) => edit({
3230
+ ...draft,
3231
+ web: event.target.checked
3232
+ })
3233
+ }),
3234
+ " ",
3235
+ t("modelsWebSearch")
3236
+ ] }),
3237
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3238
+ className: "dsh-subscription-hint",
3239
+ children: t("ollamaWebHint")
3240
+ })
3241
+ ]
3242
+ }),
3243
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("footer", {
3244
+ style: {
3245
+ ...stack,
3246
+ gap: 8,
3247
+ borderTop: border,
3248
+ padding: "14px 0 8px",
3249
+ position: "sticky",
3250
+ bottom: 0,
3251
+ background: "var(--dsw-alias-bg-layer-2, var(--dsw-alias-bg-layer-1))"
3252
+ },
3253
+ children: [saveError && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3254
+ role: "alert",
3255
+ children: saveError
3256
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3257
+ style: {
3258
+ ...actions,
3259
+ justifyContent: "flex-end"
3260
+ },
3261
+ children: [
3262
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3263
+ type: "button",
3264
+ style: { marginRight: "auto" },
3265
+ disabled: loading || saving,
3266
+ onClick: () => {
3267
+ resetDefaults();
3268
+ },
3269
+ children: t("ollamaResetDefaults")
3270
+ }),
3271
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3272
+ type: "button",
3273
+ disabled: saving,
3274
+ onClick: onClose,
3275
+ children: t("cancel")
1987
3276
  }),
1988
- snapshot?.windows?.map((window$1, index) => {
1989
- const percent = displayUsedPercent(window$1.usedPercent);
1990
- const label = externalUsageWindowLabel(t, window$1);
1991
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1992
- style: subscriptionCardStyles.usageRow,
1993
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1994
- style: subscriptionCardStyles.usageMeta,
1995
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [percent === void 0 ? t("usageMeterInvalid") : `${percent}%`, window$1.resetsAt === void 0 ? "" : ` · ${t("usageResets", { date: new Date(window$1.resetsAt).toLocaleString() })}`] })]
1996
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageMeter, {
1997
- window: window$1,
1998
- t,
1999
- observedAt: observedAt[id],
2000
- stale: errors[id] !== void 0 || snapshot?.stale === true
2001
- })]
2002
- }, index);
3277
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3278
+ type: "button",
3279
+ className: "dsh-subscription-primary",
3280
+ disabled: loading || saving || !dirty || invalidContext.length > 0,
3281
+ onClick: () => {
3282
+ save();
3283
+ },
3284
+ children: saving ? t("modelDefaultsSaving") : t("modelsSave")
2003
3285
  })
2004
3286
  ]
2005
3287
  })]
2006
- }),
2007
- !configured && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2008
- style: subscriptionCardStyles.actions,
2009
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2010
- type: "button",
2011
- style: subscriptionCardStyles.button,
2012
- onClick: () => {
2013
- refreshStatus();
2014
- },
2015
- children: t("usageRefresh")
2016
- })
2017
3288
  })
2018
3289
  ]
2019
- }, id);
2020
- }) });
3290
+ })]
3291
+ });
2021
3292
  }
2022
3293
 
2023
3294
  //#endregion
2024
- //#region src/client/CursorCard.tsx
3295
+ //#region src/client/OllamaCard.tsx
2025
3296
  function message(error) {
2026
3297
  return error instanceof Error ? error.message : String(error);
2027
3298
  }
2028
- /** Cursor's single connected account, rendered with the same card and Manage dialog as the other providers. */
2029
- function CursorCard({ rpc, t }) {
2030
- const [status, setStatus] = (0, react.useState)();
3299
+ /**
3300
+ * Ollama Cloud route card: the catalog the model picker shows, the web
3301
+ * capability toggle, and the API-key usage row. The settings dialog owns
3302
+ * every write; the card only reads, so a save is reflected by re-reading.
3303
+ */
3304
+ function OllamaCard({ rpc, t }) {
3305
+ const [view, setView] = (0, react.useState)();
3306
+ const [configured, setConfigured] = (0, react.useState)();
2031
3307
  const [usage, setUsage] = (0, react.useState)();
2032
3308
  const [observedAt, setObservedAt] = (0, react.useState)();
2033
3309
  const [usageStale, setUsageStale] = (0, react.useState)(false);
2034
- const [authorizeUrl, setAuthorizeUrl] = (0, react.useState)();
2035
- const [error, setError] = (0, react.useState)();
2036
3310
  const [loading, setLoading] = (0, react.useState)(false);
3311
+ const [statusError, setStatusError] = (0, react.useState)();
3312
+ const [usageError, setUsageError] = (0, react.useState)();
2037
3313
  const [manage, setManage] = (0, react.useState)(false);
2038
- const authenticated = (0, react.useRef)();
2039
- const refreshStatus = (0, react.useCallback)(async () => {
2040
- try {
2041
- const value = await callSubscriptionsAuth(rpc, "cursorStatus", {});
2042
- setStatus(value);
2043
- if (authenticated.current !== value.authenticated) {
2044
- authenticated.current = value.authenticated;
2045
- window.dispatchEvent(new Event(USAGE_BADGE_REFRESH_EVENT));
2046
- }
2047
- if (value.authenticated) setAuthorizeUrl(void 0);
2048
- if (value.error) setError(value.error);
2049
- } catch (failure) {
2050
- setError(message(failure));
2051
- }
2052
- }, [rpc]);
2053
3314
  const refreshUsage = (0, react.useCallback)(async () => {
2054
3315
  setLoading(true);
2055
3316
  try {
2056
- const value = await callSubscriptionsAuth(rpc, "cursorUsage", {});
3317
+ const value = await callSubscriptionsAuth(rpc, "externalUsage", { source: "ollama-cloud" });
2057
3318
  setUsage(value);
2058
3319
  setObservedAt(value.observedAt ?? Date.now());
2059
3320
  setUsageStale(value.stale === true);
2060
- setError(void 0);
3321
+ setUsageError(void 0);
2061
3322
  window.dispatchEvent(new Event(USAGE_BADGE_REFRESH_EVENT));
2062
3323
  } catch (failure) {
2063
- setError(message(failure));
3324
+ setUsageError(message(failure));
2064
3325
  setUsageStale(true);
2065
3326
  } finally {
2066
3327
  setLoading(false);
2067
3328
  }
2068
3329
  }, [rpc]);
2069
- (0, react.useEffect)(() => {
2070
- refreshStatus();
2071
- }, [refreshStatus]);
2072
- (0, react.useEffect)(() => {
2073
- if (status?.busy !== true) return;
2074
- const timer = setInterval(() => {
2075
- refreshStatus();
2076
- }, 2e3);
2077
- return () => clearInterval(timer);
2078
- }, [status?.busy, refreshStatus]);
2079
- (0, react.useEffect)(() => {
2080
- if (status?.authenticated === true) refreshUsage();
2081
- }, [status?.authenticated, refreshUsage]);
2082
- const login = async () => {
2083
- setError(void 0);
2084
- try {
2085
- const value = await callSubscriptionsAuth(rpc, "cursorLogin", {});
2086
- setAuthorizeUrl(value.authorizeUrl);
2087
- window.open(value.authorizeUrl, "_blank", "noopener");
2088
- await refreshStatus();
2089
- } catch (failure) {
2090
- setError(message(failure));
2091
- }
2092
- };
2093
- const cancel = async () => {
2094
- try {
2095
- await callSubscriptionsAuth(rpc, "cursorCancel", {});
2096
- setAuthorizeUrl(void 0);
2097
- await refreshStatus();
2098
- } catch (failure) {
2099
- setError(message(failure));
2100
- }
2101
- };
2102
- const logout = async () => {
2103
- if (!window.confirm(t("logoutAccountConfirm", {
2104
- account: "Cursor",
2105
- provider: "Cursor"
2106
- }))) return;
3330
+ const refreshStatus = (0, react.useCallback)(async () => {
2107
3331
  try {
2108
- await callSubscriptionsAuth(rpc, "cursorLogout", {});
2109
- setUsage(void 0);
2110
- setObservedAt(void 0);
2111
- setUsageStale(false);
2112
- await refreshStatus();
3332
+ const [settings, status] = await Promise.all([callSubscriptionsAuth(rpc, "ollamaSettings", {}), callSubscriptionsAuth(rpc, "externalStatus", {})]);
3333
+ setView(settings);
3334
+ setStatusError(void 0);
3335
+ const isConfigured = status["ollama-cloud"]?.configured === true;
3336
+ setConfigured(isConfigured);
3337
+ if (isConfigured) refreshUsage();
2113
3338
  } catch (failure) {
2114
- setError(message(failure));
3339
+ setStatusError(message(failure));
2115
3340
  }
2116
- };
2117
- const connected = status?.authenticated === true;
3341
+ }, [rpc, refreshUsage]);
3342
+ (0, react.useEffect)(() => {
3343
+ refreshStatus();
3344
+ }, [refreshStatus]);
2118
3345
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2119
3346
  style: subscriptionCardStyles.card,
2120
3347
  children: [
@@ -2122,51 +3349,40 @@ function CursorCard({ rpc, t }) {
2122
3349
  style: subscriptionCardStyles.header,
2123
3350
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: {
2124
3351
  ...subscriptionCardStyles.dot,
2125
- background: status?.busy ? "var(--dsw-alias-state-warn-label)" : connected ? "var(--dsw-alias-state-success-primary)" : "var(--dsw-alias-label-dimmed)"
3352
+ background: configured === true ? "var(--dsw-alias-state-success-primary)" : "var(--dsw-alias-label-dimmed)"
2126
3353
  } }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2127
3354
  style: subscriptionCardStyles.name,
2128
- children: t("cursorTitle")
3355
+ children: t("ollamaTitle")
2129
3356
  })]
2130
3357
  }),
2131
3358
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2132
3359
  style: subscriptionCardStyles.status,
2133
- children: status === void 0 ? t("checking") : status.busy ? t("loginInProgress") : connected ? t("loggedInCount", { count: 1 }) : t("notLoggedIn")
3360
+ children: view === void 0 ? t("checking") : view.models.length > 0 ? t("ollamaStatus", { count: view.models.length }) : t("ollamaStatusNone")
2134
3361
  }),
2135
- error !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3362
+ view !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3363
+ style: subscriptionCardStyles.status,
3364
+ children: view.effective.web ? t("ollamaWebOn") : t("ollamaWebOff")
3365
+ }),
3366
+ statusError !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2136
3367
  style: subscriptionCardStyles.error,
2137
- children: error
3368
+ children: t("ollamaModelsLoadFailed", { message: statusError })
2138
3369
  }),
2139
- connected && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3370
+ view !== void 0 && configured === false && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3371
+ style: subscriptionCardStyles.status,
3372
+ children: t("ollamaConfigureHint", { ref: view.effective.apiKeyRef })
3373
+ }),
3374
+ configured === true && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2140
3375
  style: subscriptionCardStyles.account,
2141
3376
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2142
3377
  style: subscriptionCardStyles.accountHeader,
2143
- children: [
2144
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2145
- style: subscriptionCardStyles.defaultStar,
2146
- title: t("defaultBadge"),
2147
- children: "★"
2148
- }),
2149
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2150
- style: subscriptionCardStyles.accountName,
2151
- children: "Cursor"
2152
- }),
2153
- status.expiresAt !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2154
- style: subscriptionCardStyles.status,
2155
- children: t("accountExpires", { date: new Date(status.expiresAt).toLocaleString() })
2156
- }),
2157
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2158
- type: "button",
2159
- style: {
2160
- ...subscriptionCardStyles.button,
2161
- marginLeft: "auto",
2162
- flexShrink: 0
2163
- },
2164
- onClick: () => {
2165
- logout();
2166
- },
2167
- children: t("logout")
2168
- })
2169
- ]
3378
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3379
+ style: subscriptionCardStyles.defaultStar,
3380
+ title: t("defaultBadge"),
3381
+ children: "★"
3382
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3383
+ style: subscriptionCardStyles.accountName,
3384
+ children: t("externalUsageApiKey")
3385
+ })]
2170
3386
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2171
3387
  style: subscriptionCardStyles.usage,
2172
3388
  children: [
@@ -2178,7 +3394,7 @@ function CursorCard({ rpc, t }) {
2178
3394
  children: t("usageTitle")
2179
3395
  }),
2180
3396
  usage?.plan !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2181
- style: subscriptionCardStyles.status,
3397
+ style: subscriptionCardStyles.usagePlan,
2182
3398
  children: t("usagePlan", { plan: usage.plan })
2183
3399
  }),
2184
3400
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
@@ -2196,13 +3412,21 @@ function CursorCard({ rpc, t }) {
2196
3412
  style: subscriptionCardStyles.status,
2197
3413
  children: t("usageLoading")
2198
3414
  }),
3415
+ usageError !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3416
+ style: subscriptionCardStyles.error,
3417
+ children: t("usageError", { message: usageError })
3418
+ }),
3419
+ usage !== void 0 && usage.supported === false && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3420
+ style: subscriptionCardStyles.status,
3421
+ children: t("usageUnsupported")
3422
+ }),
2199
3423
  usage?.windows?.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2200
3424
  style: subscriptionCardStyles.status,
2201
3425
  children: t("usageEmpty")
2202
3426
  }),
2203
3427
  usage?.windows?.map((window$1, index) => {
2204
3428
  const percent = displayUsedPercent(window$1.usedPercent);
2205
- const label = window$1.scope === "Included" ? t("cursorIncluded") : window$1.scope === "Cursor Models" ? t("cursorModels") : window$1.scope === "Other Models" ? t("cursorOtherModels") : window$1.scope === "Included requests" ? t("cursorIncludedRequests") : window$1.scope ?? t("usageWindow");
3429
+ const label = externalUsageWindowLabel(t, window$1);
2206
3430
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2207
3431
  style: subscriptionCardStyles.usageRow,
2208
3432
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -2219,46 +3443,34 @@ function CursorCard({ rpc, t }) {
2219
3443
  ]
2220
3444
  })]
2221
3445
  }),
3446
+ view?.configOverridesModels === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3447
+ style: subscriptionCardStyles.status,
3448
+ children: t("ollamaCatalogOverride")
3449
+ }),
2222
3450
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2223
3451
  style: subscriptionCardStyles.actions,
2224
- children: [
2225
- status?.authenticated === false && !status.busy && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2226
- type: "button",
2227
- style: subscriptionCardStyles.button,
2228
- onClick: () => {
2229
- login();
2230
- },
2231
- children: t("login")
2232
- }),
2233
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2234
- type: "button",
2235
- style: subscriptionCardStyles.button,
2236
- "aria-haspopup": "dialog",
2237
- onClick: () => setManage(true),
2238
- children: t("accountsManage")
2239
- }),
2240
- status?.busy === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2241
- type: "button",
2242
- style: subscriptionCardStyles.button,
2243
- onClick: () => {
2244
- cancel();
2245
- },
2246
- children: t("cancel")
2247
- })
2248
- ]
2249
- }),
2250
- status?.busy === true && authorizeUrl !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
2251
- href: authorizeUrl,
2252
- target: "_blank",
2253
- rel: "noopener noreferrer",
2254
- children: t("cursorOpenLogin")
3452
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3453
+ type: "button",
3454
+ style: subscriptionCardStyles.button,
3455
+ "aria-haspopup": "dialog",
3456
+ onClick: () => setManage(true),
3457
+ children: t("accountsManage")
3458
+ }), configured !== true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3459
+ type: "button",
3460
+ style: subscriptionCardStyles.button,
3461
+ onClick: () => {
3462
+ refreshStatus();
3463
+ },
3464
+ children: t("usageRefresh")
3465
+ })]
2255
3466
  }),
2256
- manage && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProviderAccountManager, {
2257
- provider: "cursor-subscription",
2258
- name: "Cursor",
3467
+ manage && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OllamaSettingsManager, {
2259
3468
  rpc,
2260
3469
  t,
2261
- onClose: () => setManage(false)
3470
+ onClose: () => setManage(false),
3471
+ onSaved: () => {
3472
+ refreshStatus();
3473
+ }
2262
3474
  })
2263
3475
  ]
2264
3476
  });
@@ -2333,15 +3545,15 @@ function showsResetCredits(provider, credits) {
2333
3545
  return (credits?.length ?? 0) > 0;
2334
3546
  }
2335
3547
  /** The error line the settings card mounts. Claude never interpolates provider text. */
2336
- function ResetCreditsErrorLine({ provider, message: message$1, t }) {
3548
+ function ResetCreditsErrorLine({ provider, message: message$3, t }) {
2337
3549
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2338
3550
  style: subscriptionCardStyles.error,
2339
- children: resetCreditsErrorText(provider, message$1, t)
3551
+ children: resetCreditsErrorText(provider, message$3, t)
2340
3552
  });
2341
3553
  }
2342
3554
  /** Fixed sentence for Claude. Codex may include the lookup message; Claude must not. */
2343
- function resetCreditsErrorText(provider, message$1, t) {
2344
- return provider === "claude" ? t("resetCreditsErrorFixed") : t("resetCreditsError", { message: message$1 });
3555
+ function resetCreditsErrorText(provider, message$3, t) {
3556
+ return provider === "claude" ? t("resetCreditsErrorFixed") : t("resetCreditsError", { message: message$3 });
2345
3557
  }
2346
3558
  const summaryStyle = {
2347
3559
  ...subscriptionCardStyles.usageMeta,
@@ -2453,7 +3665,12 @@ function ResetCreditsDisclosure({ mode, credits, t, now = Date.now(), nextCredit
2453
3665
  }),
2454
3666
  mode === "grants" && state === "notUsable" && credit.cooldownUntil !== void 0 && credit.cooldownUntil > now && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2455
3667
  "aria-hidden": "true",
2456
- children: [t("resetCreditCooldownUntil", { date: new Date(credit.cooldownUntil).toLocaleString() }), " · "]
3668
+ children: [
3669
+ t("resetCreditCooldownUntil", { date: new Date(credit.cooldownUntil).toLocaleString() }),
3670
+ " ",
3671
+ "·",
3672
+ " "
3673
+ ]
2457
3674
  }),
2458
3675
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2459
3676
  "aria-hidden": mode === "grants" ? true : void 0,
@@ -2599,7 +3816,7 @@ const styles$6 = {
2599
3816
  //#endregion
2600
3817
  //#region src/client/SubscriptionsSection.tsx
2601
3818
  /** Poll cadence while a provider login attempt is busy. */
2602
- const POLL_INTERVAL_MS$1 = 2e3;
3819
+ const POLL_INTERVAL_MS = 2e3;
2603
3820
  const CLIENT_VERSION_SOURCES = {
2604
3821
  npm: "clientVersionNpm",
2605
3822
  local: "clientVersionLocal",
@@ -2872,12 +4089,12 @@ function SubscriptionsSection(props) {
2872
4089
  const usageInflightRef = (0, react.useRef)(/* @__PURE__ */ new Map());
2873
4090
  const usageRosterSignatureRef = (0, react.useRef)();
2874
4091
  const [managedProvider, setManagedProvider] = (0, react.useState)();
2875
- const setProviderError = (0, react.useCallback)((provider, message$1) => {
4092
+ const setProviderError = (0, react.useCallback)((provider, message$3) => {
2876
4093
  if (!mountedRef.current) return;
2877
4094
  setErrors((prev) => {
2878
4095
  const next = { ...prev };
2879
- if (message$1 === void 0) delete next[provider];
2880
- else next[provider] = message$1;
4096
+ if (message$3 === void 0) delete next[provider];
4097
+ else next[provider] = message$3;
2881
4098
  return next;
2882
4099
  });
2883
4100
  }, []);
@@ -2895,8 +4112,8 @@ function SubscriptionsSection(props) {
2895
4112
  try {
2896
4113
  response = await callSubscriptionsAuth(rpc, "status", {});
2897
4114
  } catch (error) {
2898
- const message$1 = error instanceof Error ? error.message : String(error);
2899
- for (const { id } of PROVIDERS) setProviderError(id, message$1);
4115
+ const message$3 = error instanceof Error ? error.message : String(error);
4116
+ for (const { id } of PROVIDERS) setProviderError(id, message$3);
2900
4117
  return;
2901
4118
  }
2902
4119
  if (!mountedRef.current) return;
@@ -2928,7 +4145,7 @@ function SubscriptionsSection(props) {
2928
4145
  if (pollersRef.current.has(provider)) return;
2929
4146
  pollersRef.current.set(provider, setInterval(() => {
2930
4147
  refresh();
2931
- }, POLL_INTERVAL_MS$1));
4148
+ }, POLL_INTERVAL_MS));
2932
4149
  }, [refresh]);
2933
4150
  (0, react.useEffect)(() => {
2934
4151
  mountedRef.current = true;
@@ -3090,7 +4307,8 @@ function SubscriptionsSection(props) {
3090
4307
  rpc,
3091
4308
  t,
3092
4309
  setProviderError,
3093
- startPolling
4310
+ startPolling,
4311
+ refresh
3094
4312
  ]);
3095
4313
  const cancel = (0, react.useCallback)(async (provider) => {
3096
4314
  if (rpc === void 0) return;
@@ -3466,6 +4684,10 @@ function SubscriptionsSection(props) {
3466
4684
  rpc,
3467
4685
  t
3468
4686
  }),
4687
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OllamaCard, {
4688
+ rpc,
4689
+ t
4690
+ }),
3469
4691
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ExternalUsageCards, {
3470
4692
  rpc,
3471
4693
  t
@@ -3833,7 +5055,8 @@ const HOST_CONTRACT = {
3833
5055
  slots: "@deepseek-ai/dsh-client-ui-renderer"
3834
5056
  },
3835
5057
  markers: { composerStats: {
3836
- attribute: "data-composer-stats",
5058
+ row: "data-composer-stats",
5059
+ dock: "data-composer-stat",
3837
5060
  package: "@deepseek-ai/dsh-client-ui-chat"
3838
5061
  } },
3839
5062
  diagnostics: {
@@ -3886,6 +5109,10 @@ const HOST_CONTRACT = {
3886
5109
  ]
3887
5110
  }
3888
5111
  };
5112
+ /** Attribute selector for a host DOM marker (`data-composer-stats` → `[data-composer-stats]`). */
5113
+ function markerSelector(attribute) {
5114
+ return `[${attribute}]`;
5115
+ }
3889
5116
 
3890
5117
  //#endregion
3891
5118
  //#region src/client/host-icons.ts
@@ -4296,9 +5523,10 @@ function VideoGenerateToolview(props) {
4296
5523
  //#region src/client/SpeedSelect.tsx
4297
5524
  /**
4298
5525
  * The `loadSpeed` half of the inject face: the plugin's own speed state plus
4299
- * the host's current model selection (the visibility gate). A model-RPC
4300
- * failure throws rather than answering "hidden" — the caller keeps its last
4301
- * 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.
4302
5530
  *
4303
5531
  * `sessionId` is a plain string: slot and command contexts brand it through
4304
5532
  * different dsh-session copies, and only the service boundary needs one.
@@ -4315,13 +5543,126 @@ function createSpeedLoader(connection, models, sessionId) {
4315
5543
  visible: false,
4316
5544
  tier: state.tier
4317
5545
  };
4318
- 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
+ }
4319
5556
  return {
4320
5557
  visible: current !== null && current.provider === "codex" && state.fastModels.includes(current.model),
4321
5558
  tier: state.tier
4322
5559
  };
4323
5560
  };
4324
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
+ }
4325
5666
  /** The `setSpeed` half of the inject face: boolean outcome for the component's busy state. */
4326
5667
  function createSpeedSetter(connection, sessionId) {
4327
5668
  return (tier) => callSubscriptionsAuth(connection.rpc, "setSpeed", {
@@ -4337,20 +5678,15 @@ const TIERS = ["standard", "fast"];
4337
5678
  /**
4338
5679
  * The composer Speed control: a trigger reading `速度 · 快速`/`速度 · 标准`
4339
5680
  * that opens a two-row menu (standard/fast with descriptions, check mark on
4340
- * the current tier). Mount and every open reload the host state so a model
4341
- * switch made since the last open self-corrects.
4342
- */
4343
- /** How often the control re-reads the host state (model switches arrive only by asking). */
4344
- const POLL_INTERVAL_MS = 3e3;
4345
- /**
4346
- * The composer Speed control: a trigger reading `速度 · 快速`/`速度 · 标准`
4347
- * that opens a two-row menu (standard/fast with descriptions, check mark on
4348
- * the current tier). The host pushes nothing on a model switch, so the
4349
- * control re-reads on a slow poll with a single-flight guard; a failed read
4350
- * keeps the last known state, so a transient RPC failure can never lock the
4351
- * toggle away (the earlier mount-only load had no recovery path).
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.
4352
5688
  */
4353
- function SpeedSelect({ loadSpeed, setSpeed, t }) {
5689
+ function SpeedSelect({ loadSpeed, setSpeed, watchModel, t }) {
4354
5690
  const translate = t ?? fallbackTranslate$1;
4355
5691
  const [state, setState] = (0, react.useState)(null);
4356
5692
  const [open, setOpen] = (0, react.useState)(false);
@@ -4358,25 +5694,58 @@ function SpeedSelect({ loadSpeed, setSpeed, t }) {
4358
5694
  const rootRef = (0, react.useRef)(null);
4359
5695
  const loadRef = (0, react.useRef)(loadSpeed);
4360
5696
  loadRef.current = loadSpeed;
5697
+ const watchRef = (0, react.useRef)(watchModel);
5698
+ watchRef.current = watchModel;
4361
5699
  (0, react.useEffect)(() => {
4362
5700
  if (loadRef.current === void 0) return;
4363
5701
  let cancelled = false;
4364
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
+ };
4365
5712
  const reload = () => {
4366
5713
  const load = loadRef.current;
4367
- if (load === void 0 || inflight) return;
5714
+ if (load === void 0) return;
5715
+ if (inflight) {
5716
+ again = true;
5717
+ return;
5718
+ }
4368
5719
  inflight = true;
4369
5720
  load().then((loaded) => {
4370
- if (!cancelled) setState(loaded);
4371
- }, () => {}).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(() => {
4372
5729
  inflight = false;
5730
+ if (cancelled || !again) return;
5731
+ again = false;
5732
+ reload();
4373
5733
  });
4374
5734
  };
4375
5735
  reload();
4376
- 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);
4377
5744
  return () => {
4378
5745
  cancelled = true;
4379
- clearInterval(timer);
5746
+ unsubscribe?.();
5747
+ stopRetry();
5748
+ window.removeEventListener(SPEED_CHANGED_EVENT, onSpeedChanged);
4380
5749
  };
4381
5750
  }, []);
4382
5751
  (0, react.useEffect)(() => {
@@ -4572,8 +5941,9 @@ const PROVIDER_NAMES = {
4572
5941
  "cursor-subscription": "Cursor",
4573
5942
  "opencode-go": "OpenCode Go",
4574
5943
  "kimi-coding": "Kimi Code",
4575
- "minimax": "MiniMax",
4576
- "minimax-cn": "MiniMax CN"
5944
+ minimax: "MiniMax",
5945
+ "minimax-cn": "MiniMax CN",
5946
+ "ollama-cloud": "Ollama Cloud"
4577
5947
  };
4578
5948
  /** Key of one account's cached windows: `provider:accountKey`. */
4579
5949
  function usageKeyOf(provider, account) {
@@ -4649,7 +6019,8 @@ async function loadBadgeRoster(rpc) {
4649
6019
  ["opencode-go", "opencode-go"],
4650
6020
  ["kimi-code", "kimi-coding"],
4651
6021
  ["minimax", "minimax"],
4652
- ["minimax-cn", "minimax-cn"]
6022
+ ["minimax-cn", "minimax-cn"],
6023
+ ["ollama-cloud", "ollama-cloud"]
4653
6024
  ]) {
4654
6025
  refreshed.add(provider);
4655
6026
  if (external.value[source]?.configured) roster.push({
@@ -4667,7 +6038,7 @@ async function loadBadgeRoster(rpc) {
4667
6038
  }
4668
6039
  async function usageOf(rpc, { provider, account }) {
4669
6040
  if (provider === "cursor-subscription") return callSubscriptionsAuth(rpc, "cursorUsage", {});
4670
- if (provider === "opencode-go" || provider === "kimi-coding" || provider === "minimax" || provider === "minimax-cn") return callSubscriptionsAuth(rpc, "externalUsage", { source: provider === "kimi-coding" ? "kimi-code" : provider });
6041
+ if (provider === "opencode-go" || provider === "kimi-coding" || provider === "minimax" || provider === "minimax-cn" || provider === "ollama-cloud") return callSubscriptionsAuth(rpc, "externalUsage", { source: provider === "kimi-coding" ? "kimi-code" : provider });
4671
6042
  return callSubscriptionsAuth(rpc, "usage", {
4672
6043
  provider,
4673
6044
  account: account.key
@@ -4805,6 +6176,8 @@ function usageWindowLabel(t, window$1) {
4805
6176
  * every provider's accounts and their windows. Returns null when no data is
4806
6177
  * available.
4807
6178
  */
6179
+ const STATS_ROW_SELECTOR = markerSelector(HOST_CONTRACT.markers.composerStats.row);
6180
+ const STATS_DOCK_SELECTOR = markerSelector(HOST_CONTRACT.markers.composerStats.dock);
4808
6181
  function SubscriptionUsageBadge({ rpc, currentModel, t, rotationMs }) {
4809
6182
  const translate = t ?? fallbackTranslate;
4810
6183
  const [displays, setDisplays] = (0, react.useState)([]);
@@ -4964,15 +6337,18 @@ function SubscriptionUsageBadge({ rpc, currentModel, t, rotationMs }) {
4964
6337
  if (seat$1 === null) return;
4965
6338
  const scope = statsScopeOf(seat$1);
4966
6339
  if (scope === null) return;
4967
- const find = () => scope.querySelector("[data-composer-stats]");
4968
- setStatsRow(find());
4969
- const observer = new MutationObserver(() => {
4970
- setStatsRow(find());
4971
- });
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);
4972
6347
  observer.observe(scope, {
4973
6348
  childList: true,
4974
6349
  subtree: true
4975
6350
  });
6351
+ settle();
4976
6352
  return () => {
4977
6353
  observer.disconnect();
4978
6354
  };
@@ -5096,15 +6472,19 @@ function SubscriptionUsageBadge({ rpc, currentModel, t, rotationMs }) {
5096
6472
  });
5097
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] });
5098
6474
  }
6475
+ /** How far statsScopeOf walks looking for `composerStats.row`. */
6476
+ const LEGACY_STATS_SCOPE_DEPTH = 4;
5099
6477
  /**
5100
- * Nearest ancestor of the dock seat that can contain the host's stats row:
5101
- * the composer bar. Bounded so a badge in an unfamiliar layout never adopts
5102
- * 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.
5103
6483
  */
5104
6484
  function statsScopeOf(seat) {
5105
6485
  let node = seat.parentElement;
5106
- for (let depth = 0; node !== null && depth < 4; depth++) {
5107
- 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;
5108
6488
  node = node.parentElement;
5109
6489
  }
5110
6490
  return seat.parentElement;
@@ -5119,10 +6499,10 @@ function AccountMeta({ account, translate }) {
5119
6499
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
5120
6500
  style: styles.providerMeta,
5121
6501
  title: account.account,
5122
- children: [account.isDefault && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6502
+ children: [account.isDefault && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
5123
6503
  style: styles.defaultStar,
5124
6504
  "aria-label": "default",
5125
- children: "★ "
6505
+ children: ["★", " "]
5126
6506
  }), parts.join(" · ")]
5127
6507
  });
5128
6508
  }
@@ -5404,7 +6784,8 @@ function apply(ctx) {
5404
6784
  locale: NS,
5405
6785
  inject: (sessionId) => ({
5406
6786
  loadSpeed: createSpeedLoader(connection, models, sessionId),
5407
- setSpeed: createSpeedSetter(connection, sessionId)
6787
+ setSpeed: createSpeedSetter(connection, sessionId),
6788
+ watchModel: createModelSelectionWatch(models, sessionId)
5408
6789
  })
5409
6790
  }, SpeedSelect));
5410
6791
  ctx.slots.inject("conversation.composer.dock", () => ctx.slots.register({
@@ -5442,7 +6823,7 @@ function apply(ctx) {
5442
6823
  }));
5443
6824
  },
5444
6825
  onSelect: async (option, session) => {
5445
- await createSpeedSetter(connection, session.sessionId)(option.id);
6826
+ if (await createSpeedSetter(connection, session.sessionId)(option.id)) notifySpeedChanged();
5446
6827
  }
5447
6828
  }
5448
6829
  }), "dsh-subscriptions: /fast contribution");